From 0214910c16f98979e46f01bd9af55b43beb4cdaf Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 13 May 2026 16:02:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs(api):=20=E6=9B=B4=E6=96=B0=20API=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=B9=B6=E7=A7=BB=E9=99=A4=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 README.md 和 README_en.md API 表格中的 "✅ Box3" 和 "🆕 MC Extension" 标识 - 将文档表格中的扩展标识改为客户端 API 使用 "Client",服务端 API 使用 "Server" - 新增 gui API 文档,包含 openGUI 方法及 GuiController 相关方法 - 新增客户端 API 方法:getFPS、getPlayer、getLookingAt、getServerInfo - 新增输入 API 方法:getMouseX、getMouseY、onMouseClick - 新增 UI API 方法:getScreenSize、drawText、removeDrawText、clearDrawTexts - 移除所有客户端 API 方法描述中的 "🆕 MC Extension" 前缀 - 更新 BOX3_API_COMPARISON.md 中的图例说明,移除 MC 相关引用 - 移除关于 setTimeout/setInterval 的 Box3 平台扩展说明 - 更新多个 API 属性描述,移除 Box3 API 标识 --- .../docs/BOX3_API_COMPARISON.md | 4 +- Box3JS-NeoForge-1.21.1/docs/api/README.md | 40 +-- Box3JS-NeoForge-1.21.1/docs/api/README_en.md | 40 +-- Box3JS-NeoForge-1.21.1/docs/api/client.md | 220 ++++++++++++- Box3JS-NeoForge-1.21.1/docs/api/client_en.md | 220 ++++++++++++- Box3JS-NeoForge-1.21.1/docs/api/entity.md | 34 +- Box3JS-NeoForge-1.21.1/docs/api/entity_en.md | 34 +- Box3JS-NeoForge-1.21.1/docs/api/math.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/math_en.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/player.md | 108 +++++-- Box3JS-NeoForge-1.21.1/docs/api/player_en.md | 103 ++++-- Box3JS-NeoForge-1.21.1/docs/api/storage.md | 22 +- Box3JS-NeoForge-1.21.1/docs/api/storage_en.md | 22 +- Box3JS-NeoForge-1.21.1/docs/api/voxels.md | 22 +- Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md | 22 +- Box3JS-NeoForge-1.21.1/docs/api/world.md | 56 ++-- Box3JS-NeoForge-1.21.1/docs/api/world_en.md | 56 ++-- .../event/RegisterMenuScreensEvent.java | 34 ++ .../main/java/com/box3lab/box3js/Box3JS.java | 62 ++++ .../com/box3lab/box3js/Box3JSNetwork.java | 68 ++++ .../box3js/client/Box3JSClientEngine.java | 300 ++++++++++++++++++ .../box3lab/box3js/client/Box3JSGuiProxy.java | 123 +++++++ .../screen/Box3JSScriptContainerScreen.java | 63 ++++ .../box3js/script/Box3JSGuiController.java | 88 +++++ .../box3js/script/Box3JSGuiServerHandler.java | 127 ++++++++ .../box3lab/box3js/script/Box3JSPlayer.java | 5 - .../script/Box3JSScriptContainerMenu.java | 143 +++++++++ .../box3js/script/Box3ScriptEngine.java | 2 +- .../box3js/template/types/client/client.d.ts | 56 ++++ .../box3js/template/types/client/gui.d.ts | 74 +++++ .../box3js/template/types/client/input.d.ts | 29 ++ .../box3js/template/types/client/ui.d.ts | 43 +++ 32 files changed, 1954 insertions(+), 270 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/net/neoforged/neoforge/client/event/RegisterMenuScreensEvent.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md index a685b6c..8e989ce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md @@ -2,7 +2,7 @@ 本文档详细对比官方 Box3 平台 API 与 Box3JS 模组(NeoForge 1.21.1)的实现差异。仅涉及**服务端 API**,因为客户端 API(ClientUI、ClientAudio、ClientMedia 等)在 Box3JS 中完全不可用——Minecraft 模组运行在服务端,没有 Box3 平台的客户端渲染环境。 -> **图例**: ✅ 已实现 | ⚠️ 部分实现 | ❌ 未实现 | ⬆ MC 独有扩展 +> **图例**: ✅ 已实现 | ⚠️ 部分实现 | ❌ 未实现 | ⬆ 独有扩展 --- @@ -217,7 +217,7 @@ Zone 可以设置局部天气/光照/力场参数,这在 MC 服务端无法实 | `world.clearTimeout(id)` | `world.clearTimeout(id)` | ✅ | 一致 | | `world.clearInterval(id)` | `world.clearInterval(id)` | ✅ | 一致 | -**注意**: Box3 的 setTimeout/setInterval 在官方文档中标注为 ⬆ MC 扩展,但 Box3JS 在 `world` 全局对象上直接提供,用法一致。 +**注意**: Box3 的 setTimeout/setInterval 在官方文档中标注为 ,但 Box3JS 在 `world` 全局对象上直接提供,用法一致。 ### 1.15 视觉效果 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 1847990..5f6bde3 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -246,26 +246,26 @@ Box3JS API 按运行环境分为三大类: | 对象 | 类型 | 说明 | |------|------|------| -| `world` | ✅ Box3 | 世界控制,见 [world.md](world.md) | -| `entity` | ✅ Box3 | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | -| `player` | ✅ Box3 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | -| `voxels` | ✅ Box3 | 方块操作,见 [voxels.md](voxels.md) | -| `storage` | ✅ Box3 | 数据持久化,见 [storage.md](storage.md) | -| `db` | ✅ Box3 | SQLite 数据库,见 [database.md](database.md) | -| `http` | 🆕 MC 扩展 | HTTP 请求,见 [http.md](http.md) | -| `audio` | 🆕 MC 扩展 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | -| `client` | 🆕 MC 扩展 | 客户端生命周期,见 [client.md](client.md) | -| `input` | 🆕 MC 扩展 | 客户端键盘输入,见 [client.md](client.md) | -| `ui` | 🆕 MC 扩展 | 客户端屏幕 UI,见 [client.md](client.md) | -| `chat` | 🆕 MC 扩展 | 客户端聊天收发,见 [client.md](client.md) | -| `remoteChannel` | 🆕 MC 扩展 | 服务端↔客户端事件通信,见 [client.md](client.md) | -| `registries` | 🆕 MC 扩展 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | -| `console` | ✅ Box3 | 控制台日志输出(`log`/`warn`/`error`/`debug`) | -| `GameVector3` | ✅ Box3 | 三维向量,见 [math.md](math.md) | -| `GameBounds3` | ✅ Box3 | 包围盒,见 [math.md](math.md) | -| `GameRGBColor` | ✅ Box3 | RGB 颜色,见 [math.md](math.md) | -| `GameRGBAColor` | ✅ Box3 | RGBA 颜色,见 [math.md](math.md) | -| `GameQuaternion` | ✅ Box3 | 四元数,见 [math.md](math.md) | +| `world` | | 世界控制,见 [world.md](world.md) | +| `entity` | | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | +| `player` | | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | +| `voxels` | | 方块操作,见 [voxels.md](voxels.md) | +| `storage` | | 数据持久化,见 [storage.md](storage.md) | +| `db` | | SQLite 数据库,见 [database.md](database.md) | +| `http` | 客户端 | HTTP 请求,见 [http.md](http.md) | +| `audio` | 客户端 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | +| `client` | 客户端 | 客户端生命周期,见 [client.md](client.md) | +| `input` | 客户端 | 客户端键盘输入,见 [client.md](client.md) | +| `ui` | 客户端 | 客户端屏幕 UI,见 [client.md](client.md) | +| `chat` | 客户端 | 客户端聊天收发,见 [client.md](client.md) | +| `remoteChannel` | 客户端 | 服务端↔客户端事件通信,见 [client.md](client.md) | +| `registries` | 服务端 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | +| `console` | | 控制台日志输出(`log`/`warn`/`error`/`debug`) | +| `GameVector3` | | 三维向量,见 [math.md](math.md) | +| `GameBounds3` | | 包围盒,见 [math.md](math.md) | +| `GameRGBColor` | | RGB 颜色,见 [math.md](math.md) | +| `GameRGBAColor` | | RGBA 颜色,见 [math.md](math.md) | +| `GameQuaternion` | | 四元数,见 [math.md](math.md) | ## API 标注说明 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md index 3371e61..3bde14d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md @@ -246,26 +246,26 @@ Find APIs by what you want to do, not by which global object they live on. | Object | Type | Description | |--------|------|-------------| -| `world` | ✅ Box3 | World control, see [world_en.md](world_en.md) | -| `entity` | ✅ Box3 | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity_en.md](entity_en.md) | -| `player` | ✅ Box3 | Player wrapper (via `entity.player`), see [player_en.md](player_en.md) | -| `voxels` | ✅ Box3 | Block operations, see [voxels_en.md](voxels_en.md) | -| `storage` | ✅ Box3 | Data persistence, see [storage_en.md](storage_en.md) | -| `db` | ✅ Box3 | SQLite database, see [database_en.md](database_en.md) | -| `http` | 🆕 MC Extension | HTTP requests, see [http_en.md](http_en.md) | -| `audio` | 🆕 MC Extension | Client sound, music, volume control, see [client_en.md](client_en.md) | -| `client` | 🆕 MC Extension | Client lifecycle, see [client_en.md](client_en.md) | -| `input` | 🆕 MC Extension | Client keyboard input, see [client_en.md](client_en.md) | -| `ui` | 🆕 MC Extension | Client screen UI, see [client_en.md](client_en.md) | -| `chat` | 🆕 MC Extension | Client chat send/receive, see [client_en.md](client_en.md) | -| `remoteChannel` | 🆕 MC Extension | Server↔client event channel, see [client_en.md](client_en.md) | -| `registries` | 🆕 MC Extension | Custom blocks, items & sounds (compiled mode), see [registries_en.md](registries_en.md) | -| `console` | ✅ Box3 | Console logging (`log`/`warn`/`error`/`debug`) | -| `GameVector3` | ✅ Box3 | 3D vector, see [math_en.md](math_en.md) | -| `GameBounds3` | ✅ Box3 | Bounding box, see [math_en.md](math_en.md) | -| `GameRGBColor` | ✅ Box3 | RGB color, see [math_en.md](math_en.md) | -| `GameRGBAColor` | ✅ Box3 | RGBA color, see [math_en.md](math_en.md) | -| `GameQuaternion` | ✅ Box3 | Quaternion, see [math_en.md](math_en.md) | +| `world` | | World control, see [world_en.md](world_en.md) | +| `entity` | | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity_en.md](entity_en.md) | +| `player` | | Player wrapper (via `entity.player`), see [player_en.md](player_en.md) | +| `voxels` | | Block operations, see [voxels_en.md](voxels_en.md) | +| `storage` | | Data persistence, see [storage_en.md](storage_en.md) | +| `db` | | SQLite database, see [database_en.md](database_en.md) | +| `http` | Client | HTTP requests, see [http_en.md](http_en.md) | +| `audio` | Client | Client sound, music, volume control, see [client_en.md](client_en.md) | +| `client` | Client | Client lifecycle, see [client_en.md](client_en.md) | +| `input` | Client | Client keyboard input, see [client_en.md](client_en.md) | +| `ui` | Client | Client screen UI, see [client_en.md](client_en.md) | +| `chat` | Client | Client chat send/receive, see [client_en.md](client_en.md) | +| `remoteChannel` | Client | Server↔client event channel, see [client_en.md](client_en.md) | +| `registries` | Server | Custom blocks, items & sounds (compiled mode), see [registries_en.md](registries_en.md) | +| `console` | | Console logging (`log`/`warn`/`error`/`debug`) | +| `GameVector3` | | 3D vector, see [math_en.md](math_en.md) | +| `GameBounds3` | | Bounding box, see [math_en.md](math_en.md) | +| `GameRGBColor` | | RGB color, see [math_en.md](math_en.md) | +| `GameRGBAColor` | | RGBA color, see [math_en.md](math_en.md) | +| `GameQuaternion` | | Quaternion, see [math_en.md](math_en.md) | ## API Legend diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md index 3ca714d..65ca4ec 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md @@ -12,6 +12,7 @@ | `storage` | `GameStorage` | 客户端本地持久化存储 | | `db` | `GameDatabase` | 客户端本地 SQLite 数据库 | | `http` | `GameHttpAPI` | HTTP 请求(同步/异步) | +| `gui` | `GameGUI` | 自定义容器 GUI 界面 | | `remoteChannel` | `RemoteChannel` | 客户端 ↔ 服务端事件通信 | > **前置条件:** 客户端必须安装 Box3JS mod,服务端必须启用该项目的客户端脚本并通过网络自动下发。 @@ -21,7 +22,7 @@ ### audio.playSound(path, volume, pitch) -🆕 MC 扩展 | 播放音效(SoundSource.PLAYERS 类别)。 +播放音效(SoundSource.PLAYERS 类别)。 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -36,7 +37,7 @@ audio.playSound("minecraft:entity.experience_orb.pickup", 0.5, 1.5); ### audio.playMusic(path, volume, pitch) -🆕 MC 扩展 | 播放音乐(SoundSource.MUSIC 类别)。参数同 `playSound`。 +播放音乐(SoundSource.MUSIC 类别)。参数同 `playSound`。 ```js audio.playMusic("minecraft:music.creative", 0.5, 1.0); @@ -44,7 +45,7 @@ audio.playMusic("minecraft:music.creative", 0.5, 1.0); ### audio.stopAll() -🆕 MC 扩展 | 停止所有正在播放的声音和音乐。 +停止所有正在播放的声音和音乐。 ```js audio.stopAll(); @@ -52,7 +53,7 @@ audio.stopAll(); ### audio.getVolume(category) -🆕 MC 扩展 | 获取指定音频类别的音量。 +获取指定音频类别的音量。 | 参数 | 类型 | 说明 | |------|------|------| @@ -64,7 +65,7 @@ var musicVol = audio.getVolume("music"); // 0.0–1.0 ### audio.setVolume(category, value) -🆕 MC 扩展 | 设置指定音频类别的音量。 +设置指定音频类别的音量。 | 参数 | 类型 | 说明 | |------|------|------| @@ -95,7 +96,7 @@ audio.setVolume("player", 0.8); ### client.onTick(callback) -🆕 MC 扩展 | 注册客户端每 tick 回调(每秒 20 次)。无参数,无返回值。 +注册客户端每 tick 回调(每秒 20 次)。无参数,无返回值。 ```js client.onTick(() => { @@ -105,11 +106,59 @@ client.onTick(() => { > **注意:** 服务端也有 `world.onTick()`,但参数为 `TickInfo` 对象。客户端 `client.onTick()` 无参数。 +### client.getFPS() + +获取当前游戏帧率 (FPS)。 + +```js +var fps = client.getFPS(); +console.log(`Current FPS: ${fps}`); +``` + +### client.getPlayer() + +获取本地玩家信息。如果玩家未加载则返回 `null`。 + +```js +var player = client.getPlayer(); +if (player) { + console.log(`Player: ${player.name}, HP: ${player.health}/${player.maxHealth}`); + console.log(`Position: ${player.position.x}, ${player.position.y}, ${player.position.z}`); +} +``` + +### client.getLookingAt() + +获取玩家准星正在看向的目标。未指向任何目标时返回 `null`。 + +```js +var target = client.getLookingAt(); +if (target) { + if (target.type === "entity") { + console.log(`Looking at entity: ${target.entity.name}`); + } else if (target.type === "block") { + console.log(`Looking at block: ${target.blockPos.x}, ${target.blockPos.y}, ${target.blockPos.z}`); + } +} +``` + +### client.getServerInfo() + +获取当前连接的服务器信息。单人游戏返回 `{ ip: "localhost", name: "Singleplayer", isLocal: true }`。 + +```js +var info = client.getServerInfo(); +console.log(`Server: ${info.name} (${info.ip})`); +if (!info.isLocal) { + console.log(`Players: ${info.playerCount}/${info.maxPlayers}`); +} +``` + ## input — 键盘输入 ### input.isKeyDown(key) -🆕 MC 扩展 | 检查指定按键当前是否被按下。 +检查指定按键当前是否被按下。 | 参数 | 类型 | 说明 | |------|------|------| @@ -123,7 +172,7 @@ if (input.isKeyDown("space")) { ### input.onKeyPress(key, callback) -🆕 MC 扩展 | 注册按键按下回调(按下瞬间触发一次)。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 +注册按键按下回调(按下瞬间触发一次)。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 ```js var token = input.onKeyPress("f", () => { @@ -134,6 +183,46 @@ var token = input.onKeyPress("f", () => { token.cancel(); ``` +### input.getMouseX() + +获取当前鼠标 X 坐标(屏幕像素)。 + +```js +var mx = input.getMouseX(); +``` + +### input.getMouseY() + +获取当前鼠标 Y 坐标(屏幕像素)。 + +```js +var my = input.getMouseY(); +``` + +### input.onMouseClick(callback) + +注册鼠标按键回调。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 + +回调参数:`(button: number, action: number, x: number, y: number) => void` + +| 参数 | 说明 | +|------|------| +| `button` | 0=左键, 1=右键, 2=中键 | +| `action` | 0=释放, 1=按下, 2=重复 | +| `x` | 鼠标 X 坐标(屏幕像素) | +| `y` | 鼠标 Y 坐标(屏幕像素) | + +```js +var token = input.onMouseClick((button, action, x, y) => { + if (action === 1) { // 按下 + console.log(`Clicked button ${button} at (${x}, ${y})`); + } +}); + +// 取消监听 +token.cancel(); +``` + ### 支持的按键名称 | 类别 | 按键 | @@ -149,7 +238,7 @@ token.cancel(); ### ui.showOverlay(text) -🆕 MC 扩展 | 在动作栏(快捷栏上方)显示文字。支持颜色代码(`§a`、`§b` 等)。 +在动作栏(快捷栏上方)显示文字。支持颜色代码(`§a`、`§b` 等)。 ```js ui.showOverlay("§a欢迎来到服务器!"); @@ -157,7 +246,7 @@ ui.showOverlay("§a欢迎来到服务器!"); ### ui.showTitle(title, subtitle, fadeIn?, stay?, fadeOut?) -🆕 MC 扩展 | 显示屏幕中央大标题。 +显示屏幕中央大标题。 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| @@ -174,17 +263,63 @@ ui.showTitle("§c游戏结束", "§7再接再厉"); ### ui.showActionBar(text) -🆕 MC 扩展 | 在动作栏显示文字(与 `showOverlay` 相同)。 +在动作栏显示文字(与 `showOverlay` 相同)。 ```js ui.showActionBar("§e按 F 键使用技能"); ``` +### ui.getScreenSize() + +获取当前游戏窗口和 GUI 缩放尺寸。 + +```js +var size = ui.getScreenSize(); +console.log(size.width, size.height); // 窗口像素 +console.log(size.scaledWidth, size.scaledHeight); // GUI 缩放坐标 +``` + +### ui.drawText(id, x, y, text, color?) + +在屏幕上绘制自定义文字(每帧持续绘制,直到调用 `removeDrawText` 移除)。 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | number | (必需) | 文字 ID,用于后续移除或更新 | +| `x` | number | (必需) | X 坐标(GUI 缩放坐标系) | +| `y` | number | (必需) | Y 坐标(GUI 缩放坐标系) | +| `text` | string | (必需) | 要显示的文字 | +| `color` | GameRGBColor | `白色` | 文字颜色 | + +返回文字 ID(与传入的 `id` 相同)。重复调用相同 ID 会覆盖之前的内容。 + +```js +var textId = ui.drawText(1, 10, 10, "Hello, Box3JS!"); +// 更新位置或内容 +ui.drawText(1, 10, 30, "Updated text", new GameRGBColor(1, 0, 0)); // 红色 +``` + +### ui.removeDrawText(id) + +移除指定 ID 的绘制文字。 + +```js +ui.removeDrawText(1); +``` + +### ui.clearDrawTexts() + +清除所有通过 `drawText()` 绘制的文字。 + +```js +ui.clearDrawTexts(); +``` + ## chat — 聊天消息与命令 ### chat.sendMessage(text) -🆕 MC 扩展 | 向服务端发送聊天消息。 +向服务端发送聊天消息。 ```js chat.sendMessage("大家好!"); @@ -192,7 +327,7 @@ chat.sendMessage("大家好!"); ### chat.sendCommand(cmd) -🆕 MC 扩展 | 向服务端发送命令(等同于在聊天框输入 `/` 前缀的命令)。 +向服务端发送命令(等同于在聊天框输入 `/` 前缀的命令)。 ```js chat.sendCommand("spawn"); @@ -201,7 +336,7 @@ chat.sendCommand("home"); ### chat.onMessage(handler) -🆕 MC 扩展 | 注册接收聊天消息的处理器。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 +注册接收聊天消息的处理器。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 回调参数:`(message: string, sender: string, isSystem: boolean) => boolean | void` @@ -220,13 +355,64 @@ var token = chat.onMessage((message, sender, isSystem) => { token.cancel(); ``` +## gui — 自定义 GUI + +### gui.openGUI(config) + +打开一个脚本控制的自定义容器 GUI(类似箱子界面),返回控制器对象。 +客户端会自动向服务端请求创建容器,并返回 `GuiController` 用于操作界面和监听事件。 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `config.title` | string | `"Container"` | 标题 | +| `config.rows` | number | `3` | 行数 (1–6) | +| `config.slots` | object | `{}` | 初始物品,格式 `{ [槽位]: "物品ID" }` | + +```js +var ctrl = gui.openGUI({ + title: "§6商店", + rows: 3, + slots: { 0: "minecraft:diamond", 4: "minecraft:emerald" }, +}); + +// 设置物品 +ctrl.setItem(1, "minecraft:gold_ingot", 5); + +// 获取物品 +var item = ctrl.getItem(0); +console.log(item.id, item.count); // minecraft:diamond, 1 + +// 监听槽位点击 +ctrl.onSlotClick((slot) => { + console.log("Clicked slot:", slot); +}); + +// 监听关闭 +ctrl.onClose(() => { + console.log("GUI closed"); +}); + +// 关闭 GUI +ctrl.close(); +``` + +### GuiController 方法 + +| 方法 | 说明 | +|------|------| +| `setItem(slot, itemId, count?)` | 设置指定槽位的物品 | +| `getItem(slot)` | 获取指定槽位的物品,返回 `{ id, count }` | +| `onSlotClick(callback)` | 注册槽位点击回调,`callback(slot: number)` | +| `onClose(callback)` | 注册关闭回调,`callback()` | +| `close()` | 关闭 GUI | + ## remoteChannel — 客户端 ↔ 服务端通信 客户端通过 `remoteChannel` 与服务端进行双向事件通信。事件数据通过 JSON 序列化传输。 ### remoteChannel.sendServerEvent(event) -🆕 MC 扩展 | 向服务端发送事件。`event` 为任意 JSON 可序列化的值。 +向服务端发送事件。`event` 为任意 JSON 可序列化的值。 ```js remoteChannel.sendServerEvent({ @@ -237,7 +423,7 @@ remoteChannel.sendServerEvent({ ### remoteChannel.onClientEvent(handler) -🆕 MC 扩展 | 注册来自服务端的远程事件处理器。返回 `GameEventHandlerToken`。 +注册来自服务端的远程事件处理器。返回 `GameEventHandlerToken`。 回调参数:`(event: { tick: number, args: T }) => void` @@ -313,4 +499,4 @@ remoteChannel.onClientEvent((event) => { console.log("[client] loaded!"); ``` -全部 🆕 MC 扩展(客户端 API 为 Box3JS 专属,非 Box3 平台原有)。 +客户端 API 为 Box3JS 专属。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md index 51a906f..4451869 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md @@ -12,6 +12,7 @@ Client scripts run locally on the player's Minecraft client and are accessed thr | `storage` | `GameStorage` | Client-side persistent key-value storage | | `db` | `GameDatabase` | Client-side SQLite database | | `http` | `GameHttpAPI` | HTTP requests (sync/async) | +| `gui` | `GameGUI` | Custom container GUI interface | | `remoteChannel` | `RemoteChannel` | Client ↔ Server event communication | > **Prerequisite:** The client must have the Box3JS mod installed. The server must enable the project's client script, which is automatically sent to connecting players. @@ -21,7 +22,7 @@ Client scripts run locally on the player's Minecraft client and are accessed thr ### audio.playSound(path, volume, pitch) -🆕 MC Extension | Plays a sound effect (SoundSource.PLAYERS category). +Plays a sound effect (SoundSource.PLAYERS category). | Parameter | Type | Default | Description | |-----------|------|---------|-------------| @@ -36,7 +37,7 @@ audio.playSound("minecraft:entity.experience_orb.pickup", 0.5, 1.5); ### audio.playMusic(path, volume, pitch) -🆕 MC Extension | Plays music (SoundSource.MUSIC category). Same parameters as `playSound`. +Plays music (SoundSource.MUSIC category). Same parameters as `playSound`. ```js audio.playMusic("minecraft:music.creative", 0.5, 1.0); @@ -44,7 +45,7 @@ audio.playMusic("minecraft:music.creative", 0.5, 1.0); ### audio.stopAll() -🆕 MC Extension | Stops all currently playing sounds and music. +Stops all currently playing sounds and music. ```js audio.stopAll(); @@ -52,7 +53,7 @@ audio.stopAll(); ### audio.getVolume(category) -🆕 MC Extension | Gets the volume of a specific audio category. +Gets the volume of a specific audio category. | Parameter | Type | Description | |-----------|------|-------------| @@ -64,7 +65,7 @@ var musicVol = audio.getVolume("music"); // 0.0–1.0 ### audio.setVolume(category, value) -🆕 MC Extension | Sets the volume of a specific audio category. +Sets the volume of a specific audio category. | Parameter | Type | Description | |-----------|------|-------------| @@ -95,7 +96,7 @@ audio.setVolume("player", 0.8); ### client.onTick(callback) -🆕 MC Extension | Registers a callback invoked every client tick (20 times/sec). No parameters, no return value. +Registers a callback invoked every client tick (20 times/sec). No parameters, no return value. ```js client.onTick(() => { @@ -105,11 +106,59 @@ client.onTick(() => { > **Note:** Server-side `world.onTick()` receives a `TickInfo` object. Client-side `client.onTick()` receives nothing. +### client.getFPS() + +Gets the current frames per second (FPS). + +```js +var fps = client.getFPS(); +console.log(`Current FPS: ${fps}`); +``` + +### client.getPlayer() + +Gets local player information. Returns `null` if the player is not yet loaded. + +```js +var player = client.getPlayer(); +if (player) { + console.log(`Player: ${player.name}, HP: ${player.health}/${player.maxHealth}`); + console.log(`Position: ${player.position.x}, ${player.position.y}, ${player.position.z}`); +} +``` + +### client.getLookingAt() + +Gets what the player's crosshair is currently pointing at. Returns `null` when not looking at anything. + +```js +var target = client.getLookingAt(); +if (target) { + if (target.type === "entity") { + console.log(`Looking at entity: ${target.entity.name}`); + } else if (target.type === "block") { + console.log(`Looking at block: ${target.blockPos.x}, ${target.blockPos.y}, ${target.blockPos.z}`); + } +} +``` + +### client.getServerInfo() + +Gets current server connection information. Returns `{ ip: "localhost", name: "Singleplayer", isLocal: true }` for singleplayer. + +```js +var info = client.getServerInfo(); +console.log(`Server: ${info.name} (${info.ip})`); +if (!info.isLocal) { + console.log(`Players: ${info.playerCount}/${info.maxPlayers}`); +} +``` + ## input — Keyboard Input ### input.isKeyDown(key) -🆕 MC Extension | Checks whether a key is currently held down. +Checks whether a key is currently held down. | Parameter | Type | Description | |-----------|------|-------------| @@ -123,7 +172,7 @@ if (input.isKeyDown("space")) { ### input.onKeyPress(key, callback) -🆕 MC Extension | Registers a callback fired once when the key is pressed. Returns `GameEventHandlerToken`; call `.cancel()` to unregister. +Registers a callback fired once when the key is pressed. Returns `GameEventHandlerToken`; call `.cancel()` to unregister. ```js var token = input.onKeyPress("f", () => { @@ -134,6 +183,46 @@ var token = input.onKeyPress("f", () => { token.cancel(); ``` +### input.getMouseX() + +Gets the current mouse X position in screen pixels. + +```js +var mx = input.getMouseX(); +``` + +### input.getMouseY() + +Gets the current mouse Y position in screen pixels. + +```js +var my = input.getMouseY(); +``` + +### input.onMouseClick(callback) + +Registers a mouse button callback. Returns `GameEventHandlerToken`; call `.cancel()` to unregister. + +Callback: `(button: number, action: number, x: number, y: number) => void` + +| Parameter | Description | +|-----------|-------------| +| `button` | 0=left, 1=right, 2=middle | +| `action` | 0=release, 1=press, 2=repeat | +| `x` | Mouse X in screen pixels | +| `y` | Mouse Y in screen pixels | + +```js +var token = input.onMouseClick((button, action, x, y) => { + if (action === 1) { // pressed + console.log(`Clicked button ${button} at (${x}, ${y})`); + } +}); + +// Unregister +token.cancel(); +``` + ### Supported Key Names | Category | Keys | @@ -149,7 +238,7 @@ token.cancel(); ### ui.showOverlay(text) -🆕 MC Extension | Displays text in the action bar (above the hotbar). Supports color codes (`§a`, `§b`, etc.). +Displays text in the action bar (above the hotbar). Supports color codes (`§a`, `§b`, etc.). ```js ui.showOverlay("§aWelcome to the server!"); @@ -157,7 +246,7 @@ ui.showOverlay("§aWelcome to the server!"); ### ui.showTitle(title, subtitle, fadeIn?, stay?, fadeOut?) -🆕 MC Extension | Displays a large centered screen title. +Displays a large centered screen title. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| @@ -174,17 +263,63 @@ ui.showTitle("§cGame Over", "§7Try again"); ### ui.showActionBar(text) -🆕 MC Extension | Displays text in the action bar (same as `showOverlay`). +Displays text in the action bar (same as `showOverlay`). ```js ui.showActionBar("§ePress F to use ability"); ``` +### ui.getScreenSize() + +Gets the current game window and GUI-scaled dimensions. + +```js +var size = ui.getScreenSize(); +console.log(size.width, size.height); // window pixels +console.log(size.scaledWidth, size.scaledHeight); // GUI-scaled +``` + +### ui.drawText(id, x, y, text, color?) + +Draws custom text on screen (persists every frame until removed via `removeDrawText`). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `id` | number | (required) | Text ID for later removal or update | +| `x` | number | (required) | X position (GUI-scaled coordinates) | +| `y` | number | (required) | Y position (GUI-scaled coordinates) | +| `text` | string | (required) | Text to display | +| `color` | GameRGBColor | white | Text colour | + +Returns the text ID (same as the passed `id`). Reusing the same ID overwrites the previous entry. + +```js +var textId = ui.drawText(1, 10, 10, "Hello, Box3JS!"); +// Update position or content +ui.drawText(1, 10, 30, "Updated text", new GameRGBColor(1, 0, 0)); // red +``` + +### ui.removeDrawText(id) + +Removes the drawn text with the given ID. + +```js +ui.removeDrawText(1); +``` + +### ui.clearDrawTexts() + +Clears all texts drawn via `drawText()`. + +```js +ui.clearDrawTexts(); +``` + ## chat — Chat Messages & Commands ### chat.sendMessage(text) -🆕 MC Extension | Sends a chat message to the server. +Sends a chat message to the server. ```js chat.sendMessage("Hello everyone!"); @@ -192,7 +327,7 @@ chat.sendMessage("Hello everyone!"); ### chat.sendCommand(cmd) -🆕 MC Extension | Sends a command to the server (equivalent to typing a `/` command in chat). +Sends a command to the server (equivalent to typing a `/` command in chat). ```js chat.sendCommand("spawn"); @@ -201,7 +336,7 @@ chat.sendCommand("home"); ### chat.onMessage(handler) -🆕 MC Extension | Registers a handler for incoming chat messages. Returns `GameEventHandlerToken`; call `.cancel()` to unregister. +Registers a handler for incoming chat messages. Returns `GameEventHandlerToken`; call `.cancel()` to unregister. Callback: `(message: string, sender: string, isSystem: boolean) => boolean | void` @@ -220,13 +355,64 @@ var token = chat.onMessage((message, sender, isSystem) => { token.cancel(); ``` +## gui — Custom GUI + +### gui.openGUI(config) + +Opens a script-controlled custom container GUI (chest-like screen), returning a controller object. +The client automatically requests the server to create the container, and returns a `GuiController` for manipulating the GUI and listening to events. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `config.title` | string | `"Container"` | Title | +| `config.rows` | number | `3` | Number of rows (1–6) | +| `config.slots` | object | `{}` | Initial items, format `{ [slot]: "itemId" }` | + +```js +var ctrl = gui.openGUI({ + title: "§6Shop", + rows: 3, + slots: { 0: "minecraft:diamond", 4: "minecraft:emerald" }, +}); + +// Set item +ctrl.setItem(1, "minecraft:gold_ingot", 5); + +// Get item +var item = ctrl.getItem(0); +console.log(item.id, item.count); // minecraft:diamond, 1 + +// Slot click listener +ctrl.onSlotClick((slot) => { + console.log("Clicked slot:", slot); +}); + +// Close listener +ctrl.onClose(() => { + console.log("GUI closed"); +}); + +// Close the GUI +ctrl.close(); +``` + +### GuiController Methods + +| Method | Description | +|--------|-------------| +| `setItem(slot, itemId, count?)` | Sets the item in the given slot | +| `getItem(slot)` | Gets the item in the given slot, returns `{ id, count }` | +| `onSlotClick(callback)` | Registers a slot click callback, `callback(slot: number)` | +| `onClose(callback)` | Registers a close callback, `callback()` | +| `close()` | Closes the GUI | + ## remoteChannel — Client ↔ Server Communication The client uses `remoteChannel` for bidirectional event communication with the server. Event data is JSON-serialized. ### remoteChannel.sendServerEvent(event) -🆕 MC Extension | Sends an event to the server. `event` is any JSON-serializable value. +Sends an event to the server. `event` is any JSON-serializable value. ```js remoteChannel.sendServerEvent({ @@ -237,7 +423,7 @@ remoteChannel.sendServerEvent({ ### remoteChannel.onClientEvent(handler) -🆕 MC Extension | Registers a handler for remote events sent from the server. Returns `GameEventHandlerToken`. +Registers a handler for remote events sent from the server. Returns `GameEventHandlerToken`. Callback: `(event: { tick: number, args: T }) => void` @@ -313,4 +499,4 @@ remoteChannel.onClientEvent((event) => { console.log("[client] loaded!"); ``` -All 🆕 MC Extension (client APIs are Box3JS-specific, not from the Box3 platform). +Client APIs are Box3JS-specific. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md index 230807c..564e4f1 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -8,15 +8,15 @@ ### entity.id -✅ Box3 API | 只读。实体的 UUID 字符串(如 `"550e8400-e29b-41d4-a716-446655440000"`)。 +只读。实体的 UUID 字符串(如 `"550e8400-e29b-41d4-a716-446655440000"`)。 ### entity.isPlayer() -✅ Box3 API | 返回 `true` 表示该实体是玩家。当返回 `true` 后,`entity.player` 必定非 null。 +返回 `true` 表示该实体是玩家。当返回 `true` 后,`entity.player` 必定非 null。 ### entity.entityType -✅ Box3 API | 只读。返回实体的命名空间 ID 字符串(如 `"minecraft:zombie"`)。 +只读。返回实体的命名空间 ID 字符串(如 `"minecraft:zombie"`)。 ```js var all = world.querySelectorAll("*"); @@ -30,7 +30,7 @@ for (var i = 0; i < all.length; i++) { ### entity.position -✅ Box3 API | 只读 `GameVector3`。这是一个 **LiveVec3**:读取时自动同步实体当前坐标,调用 `.set(x,y,z)` 可**直接传送实体**。 +只读 `GameVector3`。这是一个 **LiveVec3**:读取时自动同步实体当前坐标,调用 `.set(x,y,z)` 可**直接传送实体**。 ```js var pos = entity.position; @@ -42,7 +42,7 @@ entity.position.set(0, 100, 0); ### entity.velocity -✅ Box3 API | 只读 `GameVector3`。**LiveVec3**:读取时自动同步当前速度,`.set(x,y,z)` 直接设置速度向量。 +只读 `GameVector3`。**LiveVec3**:读取时自动同步当前速度,`.set(x,y,z)` 直接设置速度向量。 ```js entity.velocity.set(0, 1, 0); // 向上弹射 @@ -51,7 +51,7 @@ entity.velocity.set(2, 0, 2); // 水平方向速度 ### entity.bounds -✅ Box3 API | 只读 `GameVector3`。实体的包围盒**半尺寸** (half-extents): +只读 `GameVector3`。实体的包围盒**半尺寸** (half-extents): - `x` = 宽度 / 2 - `y` = 高度 / 2 - `z` = 宽度 / 2 @@ -78,11 +78,11 @@ var eye = entity.eyePosition; ### entity.hp -✅ Box3 API | 获取/设置当前生命值。非 LivingEntity 返回/存储自定义属性。 +获取/设置当前生命值。非 LivingEntity 返回/存储自定义属性。 ### entity.maxHp -✅ Box3 API | 获取/设置最大生命值。设置后若当前生命超过新上限会自动截断。 +获取/设置最大生命值。设置后若当前生命超过新上限会自动截断。 ```js var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); @@ -92,11 +92,11 @@ zombie.hp = 100; ### entity.hurt(amount) -✅ Box3 API | 对实体造成 `amount` 点伤害(通用伤害类型,触发伤害事件)。 +对实体造成 `amount` 点伤害(通用伤害类型,触发伤害事件)。 ### entity.heal(amount) -✅ Box3 API | 治疗实体 `amount` 点生命值(不超过 maxHp)。 +治疗实体 `amount` 点生命值(不超过 maxHp)。 ```js zombie.hurt(10); // 造成 10 点伤害 @@ -114,11 +114,11 @@ console.log(entity.invulnerable); ### entity.destroyed -✅ Box3 API | 只读。实体是否已被移除/销毁。 +只读。实体是否已被移除/销毁。 ## 物理属性 -✅ Box3 API | 以下属性控制实体的物理行为。 +以下属性控制实体的物理行为。 ### entity.collides @@ -172,7 +172,7 @@ var ball = world.createEntity({ ### entity.meshInvisible -✅ Box3 API | 控制实体是否不可见(隐身)。 +控制实体是否不可见(隐身)。 ```js entity.meshInvisible = true; // 隐身 @@ -230,7 +230,7 @@ entity.setNameTag("§e守卫"); // 方法方式设置 ## 标签系统 -全部 ✅ Box3 API。标签是附加在实体上的字符串标记(实质是 Minecraft 的 scoreboard tags),用于分类和查询。 +标签是附加在实体上的字符串标记(实质是 Minecraft 的 scoreboard tags),用于分类和查询。 ### entity.addTag(tag) @@ -421,11 +421,11 @@ entity.setAttribute("minecraft:generic.armor", 10); ### entity.destroy() -✅ Box3 API | 销毁实体。如果通过 `setOnDestroy()` 设置了回调,会触发它。 +销毁实体。如果通过 `setOnDestroy()` 设置了回调,会触发它。 ### entity.setOnDestroy(handler) -✅ Box3 API | 设置销毁回调。`handler` 接收一个参数 `(entity)`。 +设置销毁回调。`handler` 接收一个参数 `(entity)`。 ```js entity.setOnDestroy(function(e) { @@ -451,7 +451,7 @@ boss.setOnDestroy(function(e) { ## 自定义属性 -✅ Box3 API | 可以直接在 entity 上存储任意 JS 数据,存活期等于实体生命周期。 +可以直接在 entity 上存储任意 JS 数据,存活期等于实体生命周期。 自定义属性存储在 entity 的 UUID 下,通过 `ConcurrentHashMap` 持久化直到实体被移除。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md b/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md index 3def9f8..f218985 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md @@ -8,15 +8,15 @@ Use `entity.player` to get the corresponding `player` object (non-null only when ### entity.id -✅ Box3 API | Readonly. The entity's UUID string (e.g. `"550e8400-e29b-41d4-a716-446655440000"`). +Readonly. The entity's UUID string (e.g. `"550e8400-e29b-41d4-a716-446655440000"`). ### entity.isPlayer() -✅ Box3 API | Returns `true` if the entity is a player. When true, `entity.player` is guaranteed non-null. +Returns `true` if the entity is a player. When true, `entity.player` is guaranteed non-null. ### entity.entityType -✅ Box3 API | Readonly. Returns the entity's namespace ID string (e.g. `"minecraft:zombie"`). +Readonly. Returns the entity's namespace ID string (e.g. `"minecraft:zombie"`). ```js var all = world.querySelectorAll("*"); @@ -30,7 +30,7 @@ for (var i = 0; i < all.length; i++) { ### entity.position -✅ Box3 API | Readonly `GameVector3`. This is a **LiveVec3**: reading syncs to the entity's current coordinates; calling `.set(x,y,z)` **directly teleports** the entity. +Readonly `GameVector3`. This is a **LiveVec3**: reading syncs to the entity's current coordinates; calling `.set(x,y,z)` **directly teleports** the entity. ```js var pos = entity.position; @@ -42,7 +42,7 @@ entity.position.set(0, 100, 0); ### entity.velocity -✅ Box3 API | Readonly `GameVector3`. **LiveVec3**: reading syncs to current velocity; `.set(x,y,z)` directly sets the velocity vector. +Readonly `GameVector3`. **LiveVec3**: reading syncs to current velocity; `.set(x,y,z)` directly sets the velocity vector. ```js entity.velocity.set(0, 1, 0); // Launch upward @@ -51,7 +51,7 @@ entity.velocity.set(2, 0, 2); // Horizontal velocity ### entity.bounds -✅ Box3 API | Readonly `GameVector3`. The entity's bounding box **half-extents**: +Readonly `GameVector3`. The entity's bounding box **half-extents**: - `x` = width / 2 - `y` = height / 2 - `z` = width / 2 @@ -78,11 +78,11 @@ var eye = entity.eyePosition; ### entity.hp -✅ Box3 API | Gets/sets current health. For non-LivingEntity, returns/stores a custom property. +Gets/sets current health. For non-LivingEntity, returns/stores a custom property. ### entity.maxHp -✅ Box3 API | Gets/sets maximum health. If current health exceeds the new maximum, it is clamped. +Gets/sets maximum health. If current health exceeds the new maximum, it is clamped. ```js var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); @@ -92,11 +92,11 @@ zombie.hp = 100; ### entity.hurt(amount) -✅ Box3 API | Deals `amount` generic damage to the entity (triggers damage events). +Deals `amount` generic damage to the entity (triggers damage events). ### entity.heal(amount) -✅ Box3 API | Heals the entity by `amount` (capped at maxHp). +Heals the entity by `amount` (capped at maxHp). ```js zombie.hurt(10); // Deal 10 damage @@ -114,11 +114,11 @@ console.log(entity.invulnerable); ### entity.destroyed -✅ Box3 API | Readonly. Whether the entity has been removed/destroyed. +Readonly. Whether the entity has been removed/destroyed. ## Physics -✅ Box3 API | The following properties control entity physics behavior. +The following properties control entity physics behavior. ### entity.collides @@ -172,7 +172,7 @@ var ball = world.createEntity({ ### entity.meshInvisible -✅ Box3 API | Controls entity invisibility. +Controls entity invisibility. ```js entity.meshInvisible = true; // Invisible @@ -230,7 +230,7 @@ entity.setNameTag("§eGuard"); // Method access ## Tag System -All ✅ Box3 API. Tags are string markers attached to entities (backed by Minecraft scoreboard tags), used for classification and queries. + Tags are string markers attached to entities (backed by Minecraft scoreboard tags), used for classification and queries. ### entity.addTag(tag) @@ -421,11 +421,11 @@ entity.setAttribute("minecraft:generic.armor", 10); ### entity.destroy() -✅ Box3 API | Destroys the entity. If a callback was registered via `setOnDestroy()`, it will be invoked. +Destroys the entity. If a callback was registered via `setOnDestroy()`, it will be invoked. ### entity.setOnDestroy(handler) -✅ Box3 API | Registers a callback called when the entity is destroyed. `handler` receives one argument `(entity)`. +Registers a callback called when the entity is destroyed. `handler` receives one argument `(entity)`. ```js entity.setOnDestroy(function(e) { @@ -451,7 +451,7 @@ boss.setOnDestroy(function(e) { ## Custom Properties -✅ Box3 API | You can store arbitrary JS data directly on the entity. The data lives as long as the entity exists. +You can store arbitrary JS data directly on the entity. The data lives as long as the entity exists. Custom properties are stored under the entity's UUID in a `ConcurrentHashMap` and persist until the entity is removed. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md index aa200ef..5d730f9 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -1,6 +1,6 @@ # 数学类型 -全部 ✅ Box3 API。以下数据类型在 JS 中全局可用。 +以下数据类型在 JS 中全局可用。 ## GameVector3 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md index aada06a..efdff2c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md @@ -1,6 +1,6 @@ # Math Types -All ✅ Box3 API. The following data types are globally available in JS. + The following data types are globally available in JS. ## GameVector3 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index af2e98b..649f38e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -13,15 +13,15 @@ world.onPlayerJoin(function(entity, tick) { ### player.name -✅ Box3 API | 只读。玩家名称。 +只读。玩家名称。 ### player.userId -✅ Box3 API | 只读。玩家 UUID 字符串(与 `entity.id` 相同)。 +只读。玩家 UUID 字符串(与 `entity.id` 相同)。 ### player.opLevel -✅ Box3 API | 获取/设置玩家管理员权限等级 (0–4)。 +获取/设置玩家管理员权限等级 (0–4)。 | 等级 | 说明 | |------|------| @@ -44,11 +44,11 @@ player.opLevel = 3; // 属性方式设置为 3 级 ### player.invisible -✅ Box3 API | 获取/设置玩家是否隐形。 +获取/设置玩家是否隐形。 ### player.scale -✅ Box3 API | 只读。玩家模型缩放比例(MC 原生 scale,非 Box3 scale)。 +只读。玩家模型缩放比例(MC 原生 scale,非 Box3 scale)。 ```js player.invisible = true; // 隐形 @@ -57,7 +57,7 @@ console.log("玩家缩放: " + player.scale); ## 移动 -全部 ✅ Box3 API。 + ### player.walkSpeed @@ -131,23 +131,23 @@ world.onTick(function() { ### player.canFly -✅ Box3 API | 获取/设置飞行权限(`mayfly`)。设为 `true` 后玩家按跳跃键起飞。 +获取/设置飞行权限(`mayfly`)。设为 `true` 后玩家按跳跃键起飞。 ### player.flying -✅ Box3 API | 获取/设置是否正在飞行(`flying`)。需要先设置 `canFly = true`。 +获取/设置是否正在飞行(`flying`)。需要先设置 `canFly = true`。 ### player.flySpeed -✅ Box3 API | 飞行速度。 +飞行速度。 ### player.disableFly -✅ Box3 API | 设为 `true` 时立即停止飞行并禁用飞行权限。 +设为 `true` 时立即停止飞行并禁用飞行权限。 ### player.spectator -✅ Box3 API | 只读。玩家是否处于旁观模式。 +只读。玩家是否处于旁观模式。 ```js // 允许飞行 @@ -204,7 +204,7 @@ if (player.dead) { ### player.gameMode -✅ Box3 API | 获取/设置游戏模式。get 返回名称字符串,set 接受字符串或数字。 +获取/设置游戏模式。get 返回名称字符串,set 接受字符串或数字。 ```js player.gameMode = "creative"; // 创造模式 @@ -216,7 +216,7 @@ player.gameMode = "spectator"; // 旁观模式 ## 相机 -全部 ✅ Box3 API。 + ### player.cameraMode @@ -259,11 +259,11 @@ var target = player.cameraTarget; ### player.teleport(pos) -✅ Box3 API | 传送玩家到指定 `GameVector3` 坐标。 +传送玩家到指定 `GameVector3` 坐标。 ### player.spawnPoint -✅ Box3 API | 获取/设置玩家的重生点坐标 (`GameVector3`)。读取时若玩家未设置重生点,返回世界出生点。 +获取/设置玩家的重生点坐标 (`GameVector3`)。读取时若玩家未设置重生点,返回世界出生点。 ```js // 属性方式设置 @@ -273,15 +273,15 @@ console.log(player.spawnPoint); ### player.setRespawnPoint(pos) -✅ Box3 API | 设置玩家重生点(方法方式,与 `spawnPoint` 属性等价)。 +设置玩家重生点(方法方式,与 `spawnPoint` 属性等价)。 ### player.setSpawnPoint(pos) -✅ Box3 API | 同 `setRespawnPoint`,Box3 标准命名。 +同 `setRespawnPoint`,Box3 标准命名。 ### player.respawn() -✅ Box3 API | 强制玩家重生(仅死亡状态有效)。 +强制玩家重生(仅死亡状态有效)。 ### player.dimension @@ -300,11 +300,11 @@ player.teleport(new GameVector3(0, 70, 0)); ### player.kick() -✅ Box3 API | 踢出玩家,默认提示 "Kicked"。 +踢出玩家,默认提示 "Kicked"。 ### player.kick(reason) -✅ Box3 API | 踢出玩家,自定义踢出原因。 +踢出玩家,自定义踢出原因。 ```js player.kick("你已被移出游戏"); @@ -314,7 +314,7 @@ player.kick("你已被移出游戏"); ### player.directMessage(msg) -✅ Box3 API | 向玩家发送聊天栏消息(仅该玩家可见的系统消息)。 +向玩家发送聊天栏消息(仅该玩家可见的系统消息)。 ### player.directMessage(msg, color) @@ -327,11 +327,11 @@ player.directMessage("警告!", new GameRGBColor(1, 0.5, 0)); // 橙色 ### player.actionBar(msg) -✅ Box3 API | 向玩家发送快捷栏上方消息(Action Bar)。 +向玩家发送快捷栏上方消息(Action Bar)。 ### player.title(title, subtitle) -✅ Box3 API | 向玩家发送屏幕标题。使用默认动画参数:淡入 10 tick、停留 70 tick、淡出 20 tick。 +向玩家发送屏幕标题。使用默认动画参数:淡入 10 tick、停留 70 tick、淡出 20 tick。 ### player.title(title, subtitle, fadeIn, stay, fadeOut) @@ -339,7 +339,7 @@ player.directMessage("警告!", new GameRGBColor(1, 0.5, 0)); // 橙色 ### player.dialog(config) -✅ Box3 API | 弹出对话框。传入 `{content, options}` 配置,返回 `{index, value}`。目前 MC 中发送系统消息作为简化实现。 +弹出对话框。传入 `{content, options}` 配置,返回 `{index, value}`。目前 MC 中发送系统消息作为简化实现。 ```js var result = player.dialog({ @@ -351,11 +351,11 @@ player.directMessage("你选择了: " + result.value); ### player.link(href) -✅ Box3 API | 向玩家发送可点击的 URL 链接(蓝色下划线)。 +向玩家发送可点击的 URL 链接(蓝色下划线)。 ### player.onChat(handler) -✅ Box3 API | 为单个玩家注册聊天回调(比全局 `world.onChat` 更精细的控制,常用于对话树)。 +为单个玩家注册聊天回调(比全局 `world.onChat` 更精细的控制,常用于对话树)。 ```js player.directMessage("你好!"); @@ -460,6 +460,62 @@ console.log(held.id, held.count); // "minecraft:diamond_sword" 1 player.clearInventory(); ``` +## 自定义容器 GUI + +⬆ MC 扩展 | 为玩家打开脚本控制的容器 GUI(类似箱子界面),可自定义格子内容、点击行为和关闭回调。 + +### player.openGUI(config?) + +打开一个容器 GUI 并返回 `GUIController` 控制器对象。 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `title` | `string` | `"Container"` | 容器标题 | +| `rows` | `number` | `3` | 行数 (1–6),每行 9 格 | +| `slots` | `{ [slot: number]: string }` | `{}` | 预填充物品,key 为格子索引,value 为物品 ID | + +**返回值 `GUIController` 方法:** + +| 方法 | 说明 | +|------|------| +| `setItem(slot, itemId, count?)` | 在指定格子放置物品 | +| `getItem(slot)` | 获取格子物品,返回 `{ id, count }` | +| `onSlotClick(callback)` | 注册点击回调,`return false` 可取消点击 | +| `onClose(callback)` | 注册关闭回调(ESC 或 `close()` 触发) | +| `close()` | 关闭容器 | + +```js +world.onChat(function(entity, msg, tick) { + if (msg === "!shop") { + var gui = entity.player.openGUI({ + title: "§6§l商店", + rows: 3, + slots: { + 0: "minecraft:diamond", + 4: "minecraft:emerald", + 8: "minecraft:gold_ingot", + }, + }); + + gui.setItem(1, "minecraft:netherite_ingot", 5); + + gui.onSlotClick(function(slot, player) { + console.log("点击格子: " + slot); + if (slot === 0) return false; // 禁止拿走钻石 + }); + + gui.onClose(function(player) { + player.directMessage("商店已关闭"); + }); + } + + if (msg === "!closegui") { + // 也可通过 controller 编程关闭 + // (需要将 controller 保存在外部作用域) + } +}); +``` + ## 药水效果 ### player.addEffect(effectId, duration, amplifier) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md index ed7d6a2..98b4ec4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md @@ -13,15 +13,15 @@ world.onPlayerJoin(function(entity, tick) { ### player.name -✅ Box3 API | Readonly. Player display name. +Readonly. Player display name. ### player.userId -✅ Box3 API | Readonly. Player UUID string (same as `entity.id`). +Readonly. Player UUID string (same as `entity.id`). ### player.opLevel -✅ Box3 API | Gets/sets the player's operator permission level (0–4). +Gets/sets the player's operator permission level (0–4). | Level | Description | |-------|-------------| @@ -44,11 +44,11 @@ There is also `player.getOpLevel()` method returning the permission level number ### player.invisible -✅ Box3 API | Gets/sets whether the player is invisible. +Gets/sets whether the player is invisible. ### player.scale -✅ Box3 API | Readonly. Player model scale (Minecraft native scale, not Box3 scale). +Readonly. Player model scale (Minecraft native scale, not Box3 scale). ```js player.invisible = true; // Invisible @@ -57,7 +57,7 @@ console.log("Player scale: " + player.scale); ## Movement -All ✅ Box3 API. + ### player.walkSpeed @@ -131,23 +131,23 @@ world.onTick(function() { ### player.canFly -✅ Box3 API | Gets/sets flight permission (`mayfly`). When `true`, player can take off by pressing jump. +Gets/sets flight permission (`mayfly`). When `true`, player can take off by pressing jump. ### player.flying -✅ Box3 API | Gets/sets whether the player is currently flying (`flying`). Requires `canFly = true` first. +Gets/sets whether the player is currently flying (`flying`). Requires `canFly = true` first. ### player.flySpeed -✅ Box3 API | Flying speed. +Flying speed. ### player.disableFly -✅ Box3 API | When set to `true`, immediately stops flight and disables flight permission. +When set to `true`, immediately stops flight and disables flight permission. ### player.spectator -✅ Box3 API | Readonly. Whether the player is in spectator mode. +Readonly. Whether the player is in spectator mode. ```js // Enable flight @@ -204,7 +204,7 @@ if (player.dead) { ### player.gameMode -✅ Box3 API | Gets/sets game mode. Get returns a name string; set accepts a string or number. +Gets/sets game mode. Get returns a name string; set accepts a string or number. ```js player.gameMode = "creative"; // Creative @@ -216,7 +216,7 @@ player.gameMode = "spectator"; // Spectator ## Camera -All ✅ Box3 API. + ### player.cameraMode @@ -259,11 +259,11 @@ var target = player.cameraTarget; ### player.teleport(pos) -✅ Box3 API | Teleports the player to the given `GameVector3` coordinates. +Teleports the player to the given `GameVector3` coordinates. ### player.spawnPoint -✅ Box3 API | Gets/sets the player's respawn point (`GameVector3`). When reading, returns the world spawn if the player hasn't set a personal respawn point. +Gets/sets the player's respawn point (`GameVector3`). When reading, returns the world spawn if the player hasn't set a personal respawn point. ```js // Property-style set @@ -273,15 +273,15 @@ console.log(player.spawnPoint); ### player.setRespawnPoint(pos) -✅ Box3 API | Sets the player's respawn point (method-style, equivalent to `spawnPoint` property). +Sets the player's respawn point (method-style, equivalent to `spawnPoint` property). ### player.setSpawnPoint(pos) -✅ Box3 API | Same as `setRespawnPoint`, Box3 standard naming. +Same as `setRespawnPoint`, Box3 standard naming. ### player.respawn() -✅ Box3 API | Forces the player to respawn (only works when dead). +Forces the player to respawn (only works when dead). ### player.dimension @@ -300,11 +300,11 @@ player.teleport(new GameVector3(0, 70, 0)); ### player.kick() -✅ Box3 API | Kicks the player with the default reason "Kicked". +Kicks the player with the default reason "Kicked". ### player.kick(reason) -✅ Box3 API | Kicks the player with a custom reason. +Kicks the player with a custom reason. ```js player.kick("You have been removed from the game"); @@ -314,7 +314,7 @@ player.kick("You have been removed from the game"); ### player.directMessage(msg) -✅ Box3 API | Sends a chat message visible only to this player (system message). +Sends a chat message visible only to this player (system message). ### player.directMessage(msg, color) @@ -327,11 +327,11 @@ player.directMessage("Warning!", new GameRGBColor(1, 0.5, 0)); // Orange ### player.actionBar(msg) -✅ Box3 API | Sends a message displayed on the action bar (above the hotbar). +Sends a message displayed on the action bar (above the hotbar). ### player.title(title, subtitle) -✅ Box3 API | Displays a screen title with default animation: fade-in 10 ticks, stay 70 ticks, fade-out 20 ticks. +Displays a screen title with default animation: fade-in 10 ticks, stay 70 ticks, fade-out 20 ticks. ### player.title(title, subtitle, fadeIn, stay, fadeOut) @@ -339,7 +339,7 @@ player.directMessage("Warning!", new GameRGBColor(1, 0.5, 0)); // Orange ### player.dialog(config) -✅ Box3 API | Shows a dialog panel. Pass `{content, options}`, returns `{index, value}`. Currently simplified in MC — sends system messages. +Shows a dialog panel. Pass `{content, options}`, returns `{index, value}`. Currently simplified in MC — sends system messages. ```js var result = player.dialog({ @@ -351,11 +351,11 @@ player.directMessage("You chose: " + result.value); ### player.link(href) -✅ Box3 API | Sends a clickable URL link to the player (blue underlined text). +Sends a clickable URL link to the player (blue underlined text). ### player.onChat(handler) -✅ Box3 API | Registers a per-player chat handler (more granular than global `world.onChat`, useful for dialog trees). +Registers a per-player chat handler (more granular than global `world.onChat`, useful for dialog trees). ```js player.directMessage("Hello!"); @@ -460,6 +460,57 @@ Clears the entire inventory (including armor slots and offhand). player.clearInventory(); ``` +## Custom Container GUI + +⬆ MC Extension | Opens a script-controlled container GUI (chest-like screen) for the player, with custom slot contents, click behavior, and close callbacks. + +### player.openGUI(config?) + +Opens a container GUI and returns a `GUIController` handle. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `title` | `string` | `"Container"` | Container title | +| `rows` | `number` | `3` | Number of rows (1–6), 9 slots each | +| `slots` | `{ [slot: number]: string }` | `{}` | Pre-fill slots, key = slot index, value = item ID | + +**Returned `GUIController` methods:** + +| Method | Description | +|--------|-------------| +| `setItem(slot, itemId, count?)` | Place an item in the given slot | +| `getItem(slot)` | Get item info as `{ id, count }` | +| `onSlotClick(callback)` | Register click callback; `return false` to cancel | +| `onClose(callback)` | Register close callback (ESC or `close()` fires it) | +| `close()` | Close the container | + +```js +world.onChat(function(entity, msg, tick) { + if (msg === "!shop") { + var gui = entity.player.openGUI({ + title: "§6§lShop", + rows: 3, + slots: { + 0: "minecraft:diamond", + 4: "minecraft:emerald", + 8: "minecraft:gold_ingot", + }, + }); + + gui.setItem(1, "minecraft:netherite_ingot", 5); + + gui.onSlotClick(function(slot, player) { + console.log("Clicked slot: " + slot); + if (slot === 0) return false; // prevent taking the diamond + }); + + gui.onClose(function(player) { + player.directMessage("Shop closed"); + }); + } +}); +``` + ## Status Effects ### player.addEffect(effectId, duration, amplifier) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index 929984c..fc1e6d2 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -6,11 +6,11 @@ ### storage.getDataStorage(name) -✅ Box3 API | 获取或创建一个命名存储。同名存储返回同一实例。 +获取或创建一个命名存储。同名存储返回同一实例。 ### storage.getGroupStorage(name) -✅ Box3 API | 获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 +获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 ```js var store = storage.getDataStorage("leaderboard"); @@ -21,11 +21,11 @@ var config = storage.getDataStorage("settings"); ### store.set(key, value) -✅ Box3 API | 存储键值对。`value` 可以是字符串、数字、对象(自动 JSON 序列化)。 +存储键值对。`value` 可以是字符串、数字、对象(自动 JSON 序列化)。 ### store.get(key) -✅ Box3 API | 获取值。返回存储时的原始类型。 +获取值。返回存储时的原始类型。 ```js store.set("highScore", 100); @@ -41,7 +41,7 @@ var cfg = store.get("config"); // {difficulty: "hard", ...} (object) ### store.keys() -✅ Box3 API | 返回所有 key 的数组。 +返回所有 key 的数组。 ```js var keys = store.keys(); @@ -54,7 +54,7 @@ for (var i = 0; i < keys.length; i++) { ### store.update(key, handler) -✅ Box3 API | 回调式更新值。`handler` 接收当前值,返回新值。类似于 `store.set(key, handler(store.get(key)))`,但保证原子性。 +回调式更新值。`handler` 接收当前值,返回新值。类似于 `store.set(key, handler(store.get(key)))`,但保证原子性。 ```js store.set("counter", 0); @@ -65,11 +65,11 @@ store.update("counter", function (current) { ### store.remove(key) -✅ Box3 API | 删除指定 key,并返回被删除的旧值(不存在时返回 `null`)。 +删除指定 key,并返回被删除的旧值(不存在时返回 `null`)。 ### store.destroy() -✅ Box3 API | 删除整个存储文件(同时清除内存缓存)。 +删除整个存储文件(同时清除内存缓存)。 ```js store.remove("tempKey"); @@ -80,7 +80,7 @@ store.destroy(); // 删除该存储的所有数据 ### store.increment(key, delta) -✅ Box3 API | 递增数值。`delta` 默认为 1,返回递增后的新值。 +递增数值。`delta` 默认为 1,返回递增后的新值。 ```js store.set("kills", 0); @@ -93,7 +93,7 @@ store.increment("kills", -2); // kills = 4 ### store.list(options) -✅ Box3 API | 游标分页查询。`options` 对象支持的字段: +游标分页查询。`options` 对象支持的字段: | 字段 | 类型 | 说明 | | ------------------ | ------- | ----------------------------------- | @@ -163,4 +163,4 @@ while (true) { } ``` -全部 ✅ Box3 API。 + diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md index d0b293b..5cdc596 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md @@ -6,11 +6,11 @@ ### storage.getDataStorage(name) -✅ Box3 API | Gets or creates a named storage. Same name returns the same instance. +Gets or creates a named storage. Same name returns the same instance. ### storage.getGroupStorage(name) -✅ Box3 API | Gets a **cross-project shared** storage. All projects access the same data via the same `name` (uses `__shared__/` namespace internally). Useful for global leaderboards, shared config, etc. +Gets a **cross-project shared** storage. All projects access the same data via the same `name` (uses `__shared__/` namespace internally). Useful for global leaderboards, shared config, etc. ```js var store = storage.getDataStorage("leaderboard"); @@ -21,11 +21,11 @@ var config = storage.getDataStorage("settings"); ### store.set(key, value) -✅ Box3 API | Store a key-value pair. `value` can be a string, number, or object (auto JSON-serialized). +Store a key-value pair. `value` can be a string, number, or object (auto JSON-serialized). ### store.get(key) -✅ Box3 API | Get a value. Returns the original type. +Get a value. Returns the original type. ```js store.set("highScore", 100); @@ -41,7 +41,7 @@ var cfg = store.get("config"); // {difficulty: "hard", ...} (object) ### store.keys() -✅ Box3 API | Returns an array of all keys. +Returns an array of all keys. ```js var keys = store.keys(); @@ -54,7 +54,7 @@ for (var i = 0; i < keys.length; i++) { ### store.update(key, handler) -✅ Box3 API | Callback-based value update. `handler` receives the current value and returns the new value. Equivalent to `store.set(key, handler(store.get(key)))` but guarantees atomicity. +Callback-based value update. `handler` receives the current value and returns the new value. Equivalent to `store.set(key, handler(store.get(key)))` but guarantees atomicity. ```js store.set("counter", 0); @@ -65,11 +65,11 @@ store.update("counter", function (current) { ### store.remove(key) -✅ Box3 API | Deletes the specified key and returns the previous value (or `null` if missing). +Deletes the specified key and returns the previous value (or `null` if missing). ### store.destroy() -✅ Box3 API | Deletes the entire storage file (also clears the in-memory cache). +Deletes the entire storage file (also clears the in-memory cache). ```js store.remove("tempKey"); @@ -80,7 +80,7 @@ store.destroy(); // delete all data in this storage ### store.increment(key, delta) -✅ Box3 API | Increment a numeric value. `delta` defaults to 1 and returns the new value. +Increment a numeric value. `delta` defaults to 1 and returns the new value. ```js store.set("kills", 0); @@ -93,7 +93,7 @@ store.increment("kills", -2); // kills = 4 ### store.list(options) -✅ Box3 API | Cursor-based paginated query. Supported `options` fields: +Cursor-based paginated query. Supported `options` fields: | Field | Type | Description | | ------------------ | ------- | -------------------------------------------------- | @@ -163,4 +163,4 @@ while (true) { } ``` -All ✅ Box3 API. + diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels.md b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md index 2aa59c6..6c4a5f2 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/voxels.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md @@ -6,21 +6,21 @@ ### voxels.shape -✅ Box3 API | 只读 `GameVector3`。世界尺寸(仅在部分 Box3 环境中有效)。 +只读 `GameVector3`。世界尺寸(仅在部分 Box3 环境中有效)。 ### voxels.VoxelTypes -✅ Box3 API | 只读字符串数组。所有已注册方块名称列表。 +只读字符串数组。所有已注册方块名称列表。 ## 名称 ↔ ID ### voxels.id(name) -✅ Box3 API | 方块名称 → 内部 ID。`name` 为带命名空间的字符串(如 `"minecraft:stone"`)。 +方块名称 → 内部 ID。`name` 为带命名空间的字符串(如 `"minecraft:stone"`)。 ### voxels.name(id) -✅ Box3 API | 内部 ID → 方块名称。 +内部 ID → 方块名称。 ```js var stoneId = voxels.id("minecraft:stone"); // 获取 ID @@ -31,7 +31,7 @@ var name = voxels.name(stoneId); // "minecraft:stone" ### voxels.setVoxel(x, y, z, voxel) -✅ Box3 API | 在指定坐标放置方块。`voxel` 参数接受: +在指定坐标放置方块。`voxel` 参数接受: - 字符串:命名空间 ID,如 `"minecraft:glass"` - 数字:内部方块 ID(含 rotation 编码) @@ -44,7 +44,7 @@ var name = voxels.name(stoneId); // "minecraft:stone" ### voxels.setVoxel(x, y, z, voxel, rotation) -✅ Box3 API | 放置方块并指定旋转方向。`rotation` 为 0–3,控制朝向(类似 `BlockState` 的旋转)。 +放置方块并指定旋转方向。`rotation` 为 0–3,控制朝向(类似 `BlockState` 的旋转)。 ### voxels.setVoxel(pos, voxel, rotation) @@ -62,7 +62,7 @@ voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:oak_stairs", 2); ### voxels.setVoxelId(x, y, z, voxelId) -✅ Box3 API | 放置方块,`voxelId` 为已编码 rotation 的内部 ID。 +放置方块,`voxelId` 为已编码 rotation 的内部 ID。 ### voxels.setVoxelId(pos, voxelId) @@ -97,7 +97,7 @@ voxels.fillVoxel( ### voxels.getVoxel(x, y, z) -✅ Box3 API | 返回方块的基础 ID(不含 rotation 信息)。 +返回方块的基础 ID(不含 rotation 信息)。 ### voxels.getVoxel(pos) @@ -105,7 +105,7 @@ voxels.fillVoxel( ### voxels.getVoxelId(x, y, z) -✅ Box3 API | 返回完整 ID(含 rotation 编码位)。 +返回完整 ID(含 rotation 编码位)。 ### voxels.getVoxelId(pos) @@ -113,7 +113,7 @@ voxels.fillVoxel( ### voxels.getVoxelName(x, y, z) -✅ Box3 API | 返回方块的命名空间 ID 字符串。 +返回方块的命名空间 ID 字符串。 ### voxels.getVoxelName(pos) @@ -121,7 +121,7 @@ voxels.fillVoxel( ### voxels.getVoxelRotation(x, y, z) -✅ Box3 API | 返回方块的 rotation 值(0–3)。 +返回方块的 rotation 值(0–3)。 ### voxels.getVoxelRotation(pos) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md b/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md index c645949..ffe5726 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md @@ -6,21 +6,21 @@ ### voxels.shape -✅ Box3 API | Read-only `GameVector3`. World dimensions (valid in some Box3 environments). +Read-only `GameVector3`. World dimensions (valid in some Box3 environments). ### voxels.VoxelTypes -✅ Box3 API | Read-only string array. All registered block names. +Read-only string array. All registered block names. ## Name ↔ ID ### voxels.id(name) -✅ Box3 API | Block name → internal ID. `name` is a namespaced string (e.g. `"minecraft:stone"`). +Block name → internal ID. `name` is a namespaced string (e.g. `"minecraft:stone"`). ### voxels.name(id) -✅ Box3 API | Internal ID → block name. +Internal ID → block name. ```js var stoneId = voxels.id("minecraft:stone"); // get ID @@ -31,7 +31,7 @@ var name = voxels.name(stoneId); // "minecraft:stone" ### voxels.setVoxel(x, y, z, voxel) -✅ Box3 API | Place a block at the given coordinates. `voxel` accepts: +Place a block at the given coordinates. `voxel` accepts: - String: namespaced ID, e.g. `"minecraft:glass"` - Number: internal block ID (rotation encoded) @@ -44,7 +44,7 @@ Returns the internal ID of the newly placed block. ### voxels.setVoxel(x, y, z, voxel, rotation) -✅ Box3 API | Place a block with rotation. `rotation` is 0–3, controlling orientation (like `BlockState` rotation). +Place a block with rotation. `rotation` is 0–3, controlling orientation (like `BlockState` rotation). ### voxels.setVoxel(pos, voxel, rotation) @@ -62,7 +62,7 @@ voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:oak_stairs", 2); ### voxels.setVoxelId(x, y, z, voxelId) -✅ Box3 API | Place a block, `voxelId` is the internal ID with encoded rotation. +Place a block, `voxelId` is the internal ID with encoded rotation. ### voxels.setVoxelId(pos, voxelId) @@ -97,7 +97,7 @@ voxels.fillVoxel( ### voxels.getVoxel(x, y, z) -✅ Box3 API | Returns the block's base ID (without rotation info). +Returns the block's base ID (without rotation info). ### voxels.getVoxel(pos) @@ -105,7 +105,7 @@ voxels.fillVoxel( ### voxels.getVoxelId(x, y, z) -✅ Box3 API | Returns the full ID (with rotation bits encoded). +Returns the full ID (with rotation bits encoded). ### voxels.getVoxelId(pos) @@ -113,7 +113,7 @@ voxels.fillVoxel( ### voxels.getVoxelName(x, y, z) -✅ Box3 API | Returns the block's namespaced ID string. +Returns the block's namespaced ID string. ### voxels.getVoxelName(pos) @@ -121,7 +121,7 @@ voxels.fillVoxel( ### voxels.getVoxelRotation(x, y, z) -✅ Box3 API | Returns the block's rotation value (0–3). +Returns the block's rotation value (0–3). ### voxels.getVoxelRotation(pos) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index ca3cdf1..cc81b54 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -6,7 +6,7 @@ ### world.projectName -✅ Box3 API | 只读属性。服务端 MOTD 字符串。为兼容旧代码也可作为方法调用 `world.projectName()`。 +只读属性。服务端 MOTD 字符串。为兼容旧代码也可作为方法调用 `world.projectName()`。 ```js console.log(world.projectName); // "A Minecraft Server" @@ -14,7 +14,7 @@ console.log(world.projectName); // "A Minecraft Server" ### world.serverId -✅ Box3 API | 可读写属性。服务器标识符,映射到服务端 MOTD。 +可读写属性。服务器标识符,映射到服务端 MOTD。 ```js world.serverId = "My Cool Server"; @@ -23,7 +23,7 @@ console.log(world.serverId); ### world.currentTick -✅ Box3 API | 只读属性。服务器自启动以来的总 tick 数。为兼容旧代码也可作为方法调用 `world.currentTick()`。 +只读属性。服务器自启动以来的总 tick 数。为兼容旧代码也可作为方法调用 `world.currentTick()`。 ```js var uptime = world.currentTick; @@ -34,7 +34,7 @@ world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ### world.rainDensity -✅ Box3 API | 获取/设置降雨强度,范围 0.0–1.0。 +获取/设置降雨强度,范围 0.0–1.0。 ```js world.rainDensity = 1.0; // 满强度下雨 @@ -61,7 +61,7 @@ world.clearWeather(); ### world.time -✅ Box3 API | 获取/设置世界时间(tick)。Minecraft 一天 = 24000 tick。 +获取/设置世界时间(tick)。Minecraft 一天 = 24000 tick。 ```js world.time = 6000; // 正午 @@ -79,7 +79,7 @@ world.setTime(6000); // 等效于 world.time = 6000 ### world.timeScale -✅ Box3 API | 获取/设置时间流速。`0` = 暂停,`1` = 正常。底层修改 `doDaylightCycle` 游戏规则。 +获取/设置时间流速。`0` = 暂停,`1` = 正常。底层修改 `doDaylightCycle` 游戏规则。 ```js world.timeScale = 0; // 冻结时间 @@ -90,7 +90,7 @@ world.timeScale = 1; // 恢复正常 ### world.difficulty -✅ Box3 API | 获取/设置游戏难度。get 返回名称字符串,set 接受名称字符串或数字 0–3。 +获取/设置游戏难度。get 返回名称字符串,set 接受名称字符串或数字 0–3。 ```js world.difficulty = "hard"; @@ -104,11 +104,11 @@ console.log(world.difficulty); // "hard" ### world.spawnPoint -✅ Box3 API | 只读,返回世界出生点 `GameVector3`。 +只读,返回世界出生点 `GameVector3`。 ### world.setWorldSpawn(pos) -✅ Box3 API | 设置世界出生点。 +设置世界出生点。 ```js world.setWorldSpawn(new GameVector3(0, 70, 0)); @@ -146,7 +146,7 @@ console.log(world.getGameRule("doMobSpawning")); // true/false ### world.spawnEntity(type, pos) -✅ Box3 API | 在指定位置生成实体。`type` 为命名空间 ID,返回 `Box3JSEntity`。 +在指定位置生成实体。`type` 为命名空间 ID,返回 `Box3JSEntity`。 ```js var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); @@ -159,7 +159,7 @@ zombie.setAI(true); ### world.createEntity(config) -✅ Box3 API | 使用完整配置对象生成实体。返回 `Box3JSEntity`。 +使用完整配置对象生成实体。返回 `Box3JSEntity`。 支持的配置字段:`type`、`position`、`velocity`、`fixed`、`gravity`、`friction`、`mass`、`restitution`、`collides`、`meshInvisible`、`hp`、`maxHp`、`tags`(数组)。 @@ -179,7 +179,7 @@ var entity = world.createEntity({ ## 音效属性 -✅ Box3 API | 存储音效路径字符串,设为非空后触发时机如下: +存储音效路径字符串,设为非空后触发时机如下: | 属性 | 触发时机 | | ------------------ | ----------------------------------------------- | @@ -203,7 +203,7 @@ world.breakVoxelSound = "minecraft:block.stone.break"; ### world.sound(config) -✅ Box3 API | 播放音效。`config` 可以是路径字符串或 `{path, position, volume, pitch}` 对象。 +播放音效。`config` 可以是路径字符串或 `{path, position, volume, pitch}` 对象。 ```js // 字符串简写 — 在原点以默认音量/音调播放 @@ -222,7 +222,7 @@ world.sound({ ### world.searchBox(bounds) -✅ Box3 API | 查询 GameBounds3 区域内的所有实体。 +查询 GameBounds3 区域内的所有实体。 ```js var bounds = new GameBounds3( @@ -254,19 +254,19 @@ var token = world.onTick(function (info) { | 事件 | 类型 | 回调签名 | 触发时机 | | ---------------------------- | ------- | ------------------------------------------------------ | ------------------------------------- | -| `world.onTick(fn)` | ✅ Box3 | `(info)` → `{tick, prevTick, elapsedTimeMS, skip}` | 每 tick | -| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity, tick)` | 玩家登录 | -| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity, tick)` | 玩家退出 | -| `world.onChat(fn)` | ✅ Box3 | `(entity, message, tick) => boolean \| void` | 玩家发送聊天消息;返回 `false` 可取消 | -| `world.onVoxelDestroy(fn)` | ✅ Box3 | `(entity, x, y, z, voxel, tick)` | 玩家破坏方块 | +| `world.onTick(fn)` | | `(info)` → `{tick, prevTick, elapsedTimeMS, skip}` | 每 tick | +| `world.onPlayerJoin(fn)` | | `(entity, tick)` | 玩家登录 | +| `world.onPlayerLeave(fn)` | | `(entity, tick)` | 玩家退出 | +| `world.onChat(fn)` | | `(entity, message, tick) => boolean \| void` | 玩家发送聊天消息;返回 `false` 可取消 | +| `world.onVoxelDestroy(fn)` | | `(entity, x, y, z, voxel, tick)` | 玩家破坏方块 | | `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | 玩家放置方块 | | `world.onBlockActivate(fn)` | ⬆ MC | `(entity, x, y, z, voxel, tick)` | 玩家右键方块 | -| `world.onInteract(fn)` | ✅ Box3 | `(entity, target, tick)` | 玩家右键实体 | -| `world.onVoxelContact(fn)` | ✅ Box3 | `(entity, voxelId, x, y, z, contactType, force, tick)` | 实体接触方块 | -| `world.onEntityContact(fn)` | ✅ Box3 | `(entity, other, tick)` | 两个实体接触 | -| `world.onEntitySeparate(fn)` | ✅ Box3 | `(entity, other, tick)` | 两个实体分离 | -| `world.onFluidEnter(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | 实体进入液体 | -| `world.onFluidLeave(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | 实体离开液体 | +| `world.onInteract(fn)` | | `(entity, target, tick)` | 玩家右键实体 | +| `world.onVoxelContact(fn)` | | `(entity, voxelId, x, y, z, contactType, force, tick)` | 实体接触方块 | +| `world.onEntityContact(fn)` | | `(entity, other, tick)` | 两个实体接触 | +| `world.onEntitySeparate(fn)` | | `(entity, other, tick)` | 两个实体分离 | +| `world.onFluidEnter(fn)` | | `(entity, fluid, x, y, z, tick)` | 实体进入液体 | +| `world.onFluidLeave(fn)` | | `(entity, fluid, x, y, z, tick)` | 实体离开液体 | | `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | 实体死亡;`killer` 可能为 null | | `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | 实体受伤(Pre 阶段) | | `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity, tick)` | 玩家重生 | @@ -330,11 +330,11 @@ world.onEntityDeath((entity, killer) => { ### world.querySelectorAll(selector) -✅ Box3 API | 查询所有匹配实体。返回 `Box3JSEntity[]`。 +查询所有匹配实体。返回 `Box3JSEntity[]`。 ### world.querySelector(selector) -✅ Box3 API | 查询单个匹配实体。返回 `Box3JSEntity` 或 null。 +查询单个匹配实体。返回 `Box3JSEntity` 或 null。 **选择器语法:** @@ -359,7 +359,7 @@ if (specific) { ### world.say(message) -✅ Box3 API | 向全服广播消息。 +向全服广播消息。 ```js world.say("§6[公告] §f比赛即将开始!"); diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md index ec9f1d7..cc010ac 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md @@ -6,7 +6,7 @@ ### world.projectName -✅ Box3 API | Read-only property. The server MOTD string. Also callable as `world.projectName()` for backward compatibility. +Read-only property. The server MOTD string. Also callable as `world.projectName()` for backward compatibility. ```js console.log(world.projectName); // "A Minecraft Server" @@ -14,7 +14,7 @@ console.log(world.projectName); // "A Minecraft Server" ### world.serverId -✅ Box3 API | Read/write property. Server identifier, maps to the server MOTD. +Read/write property. Server identifier, maps to the server MOTD. ```js world.serverId = "My Cool Server"; @@ -23,7 +23,7 @@ console.log(world.serverId); ### world.currentTick -✅ Box3 API | Read-only property. Total ticks since server startup. Also callable as `world.currentTick()` for backward compatibility. +Read-only property. Total ticks since server startup. Also callable as `world.currentTick()` for backward compatibility. ```js var uptime = world.currentTick; @@ -36,7 +36,7 @@ world.say( ### world.rainDensity -✅ Box3 API | Get/set rain intensity, range 0.0–1.0. +Get/set rain intensity, range 0.0–1.0. ```js world.rainDensity = 1.0; // full rain @@ -63,7 +63,7 @@ world.clearWeather(); ### world.time -✅ Box3 API | Get/set world time (ticks). One Minecraft day = 24000 ticks. +Get/set world time (ticks). One Minecraft day = 24000 ticks. ```js world.time = 6000; // noon @@ -81,7 +81,7 @@ Common time values: `0` sunrise, `6000` noon, `12000` sunset, `18000` midnight. ### world.timeScale -✅ Box3 API | Get/set time flow rate. `0` = paused, `1` = normal. Internally modifies the `doDaylightCycle` game rule. +Get/set time flow rate. `0` = paused, `1` = normal. Internally modifies the `doDaylightCycle` game rule. ```js world.timeScale = 0; // freeze time @@ -92,7 +92,7 @@ world.timeScale = 1; // resume normal ### world.difficulty -✅ Box3 API | Get/set game difficulty. Get returns the name string; set accepts a name string or number 0–3. +Get/set game difficulty. Get returns the name string; set accepts a name string or number 0–3. ```js world.difficulty = "hard"; @@ -106,11 +106,11 @@ console.log(world.difficulty); // "hard" ### world.spawnPoint -✅ Box3 API | Read-only, returns the world spawn point as `GameVector3`. +Read-only, returns the world spawn point as `GameVector3`. ### world.setWorldSpawn(pos) -✅ Box3 API | Set the world spawn point. +Set the world spawn point. ```js world.setWorldSpawn(new GameVector3(0, 70, 0)); @@ -148,7 +148,7 @@ console.log(world.getGameRule("doMobSpawning")); // true/false ### world.spawnEntity(type, pos) -✅ Box3 API | Spawn an entity at the given position. `type` is a namespaced ID, returns `Box3JSEntity`. +Spawn an entity at the given position. `type` is a namespaced ID, returns `Box3JSEntity`. ```js var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); @@ -161,7 +161,7 @@ zombie.setAI(true); ### world.createEntity(config) -✅ Box3 API | Spawn an entity with a full configuration object. Returns `Box3JSEntity`. +Spawn an entity with a full configuration object. Returns `Box3JSEntity`. Supported config fields: `type`, `position`, `velocity`, `fixed`, `gravity`, `friction`, `mass`, `restitution`, `collides`, `meshInvisible`, `hp`, `maxHp`, `tags` (array). @@ -181,7 +181,7 @@ var entity = world.createEntity({ ## Sound Properties -✅ Box3 API | Sound path strings that auto-play when set to a non-empty value: +Sound path strings that auto-play when set to a non-empty value: | Property | Trigger | | ------------------ | ---------------------------------------------------------- | @@ -205,7 +205,7 @@ world.breakVoxelSound = "minecraft:block.stone.break"; ### world.sound(config) -✅ Box3 API | Play a sound. `config` can be a path string or `{path, position, volume, pitch}` object. +Play a sound. `config` can be a path string or `{path, position, volume, pitch}` object. ```js // String shorthand — plays at origin with default volume/pitch @@ -224,7 +224,7 @@ world.sound({ ### world.searchBox(bounds) -✅ Box3 API | Query all entities within a GameBounds3 region. +Query all entities within a GameBounds3 region. ```js var bounds = new GameBounds3( @@ -256,19 +256,19 @@ var token = world.onTick(function (info) { | Event | Type | Callback Signature | Trigger | | ---------------------------- | ------- | ------------------------------------------------------ | --------------------------------------------------- | -| `world.onTick(fn)` | ✅ Box3 | `(info)` → `{tick, prevTick, elapsedTimeMS, skip}` | Every tick | -| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity, tick)` | Player logs in | -| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity, tick)` | Player leaves | -| `world.onChat(fn)` | ✅ Box3 | `(entity, message, tick) => boolean \| void` | Player sends chat message; return `false` to cancel | -| `world.onVoxelDestroy(fn)` | ✅ Box3 | `(entity, x, y, z, voxel, tick)` | Player breaks a block | +| `world.onTick(fn)` | | `(info)` → `{tick, prevTick, elapsedTimeMS, skip}` | Every tick | +| `world.onPlayerJoin(fn)` | | `(entity, tick)` | Player logs in | +| `world.onPlayerLeave(fn)` | | `(entity, tick)` | Player leaves | +| `world.onChat(fn)` | | `(entity, message, tick) => boolean \| void` | Player sends chat message; return `false` to cancel | +| `world.onVoxelDestroy(fn)` | | `(entity, x, y, z, voxel, tick)` | Player breaks a block | | `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | Player places a block | | `world.onBlockActivate(fn)` | ⬆ MC | `(entity, x, y, z, voxel, tick)` | Player right-clicks a block | -| `world.onInteract(fn)` | ✅ Box3 | `(entity, target, tick)` | Player right-clicks an entity | -| `world.onVoxelContact(fn)` | ✅ Box3 | `(entity, voxelId, x, y, z, contactType, force, tick)` | Entity contacts a block | -| `world.onEntityContact(fn)` | ✅ Box3 | `(entity, other, tick)` | Two entities contact | -| `world.onEntitySeparate(fn)` | ✅ Box3 | `(entity, other, tick)` | Two entities separate | -| `world.onFluidEnter(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | Entity enters a fluid | -| `world.onFluidLeave(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | Entity leaves a fluid | +| `world.onInteract(fn)` | | `(entity, target, tick)` | Player right-clicks an entity | +| `world.onVoxelContact(fn)` | | `(entity, voxelId, x, y, z, contactType, force, tick)` | Entity contacts a block | +| `world.onEntityContact(fn)` | | `(entity, other, tick)` | Two entities contact | +| `world.onEntitySeparate(fn)` | | `(entity, other, tick)` | Two entities separate | +| `world.onFluidEnter(fn)` | | `(entity, fluid, x, y, z, tick)` | Entity enters a fluid | +| `world.onFluidLeave(fn)` | | `(entity, fluid, x, y, z, tick)` | Entity leaves a fluid | | `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | Entity dies; `killer` may be null | | `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | Entity takes damage (Pre phase) | | `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity, tick)` | Player respawns | @@ -332,11 +332,11 @@ world.onEntityDeath((entity, killer) => { ### world.querySelectorAll(selector) -✅ Box3 API | Query all matching entities. Returns `Box3JSEntity[]`. +Query all matching entities. Returns `Box3JSEntity[]`. ### world.querySelector(selector) -✅ Box3 API | Query a single matching entity. Returns `Box3JSEntity` or null. +Query a single matching entity. Returns `Box3JSEntity` or null. **Selector syntax:** @@ -361,7 +361,7 @@ if (specific) { ### world.say(message) -✅ Box3 API | Broadcast a message to the entire server. +Broadcast a message to the entire server. ```js world.say("§6[Announcement] §fThe match is about to begin!"); diff --git a/Box3JS-NeoForge-1.21.1/net/neoforged/neoforge/client/event/RegisterMenuScreensEvent.java b/Box3JS-NeoForge-1.21.1/net/neoforged/neoforge/client/event/RegisterMenuScreensEvent.java new file mode 100644 index 0000000..074001e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/net/neoforged/neoforge/client/event/RegisterMenuScreensEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.event; + +import java.util.Map; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.neoforged.bus.api.Event; +import net.neoforged.fml.event.IModBusEvent; +import org.jetbrains.annotations.ApiStatus; + +public class RegisterMenuScreensEvent extends Event implements IModBusEvent { + private final Map, MenuScreens.ScreenConstructor> registeredScreens; + + @ApiStatus.Internal + public RegisterMenuScreensEvent(Map, MenuScreens.ScreenConstructor> registeredScreens) { + this.registeredScreens = registeredScreens; + } + + public > void register( + MenuType menuType, MenuScreens.ScreenConstructor screenConstructor) { + if (registeredScreens.containsKey(menuType)) { + throw new IllegalStateException("Duplicate attempt to register screen: " + BuiltInRegistries.MENU.getKey(menuType)); + } + registeredScreens.put(menuType, screenConstructor); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java index 6525914..e450ba4 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java @@ -1,16 +1,26 @@ package com.box3lab.box3js; import com.box3lab.box3js.client.Box3JSClientEngine; +import com.box3lab.box3js.client.Box3JSGuiProxy; +import com.box3lab.box3js.client.screen.Box3JSScriptContainerScreen; import com.box3lab.box3js.registries.Box3JSRecipeManager; import com.box3lab.box3js.script.Box3ScriptCommand; import com.box3lab.box3js.script.Box3ScriptEngine; +import com.box3lab.box3js.script.Box3JSGuiServerHandler; +import com.box3lab.box3js.script.Box3JSScriptContainerMenu; import com.mojang.logging.LogUtils; +import net.minecraft.core.registries.Registries; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.registries.DeferredRegister; +import java.util.function.Supplier; import java.nio.file.Path; import java.util.Set; @@ -35,7 +45,20 @@ public class Box3JS { /** Tracks which connected players have Box3JS installed on their client. */ public static final Set clientsWithBox3JS = ConcurrentHashMap.newKeySet(); + // ── Registries ── + + private static final DeferredRegister> MENU_TYPES = + DeferredRegister.create(Registries.MENU, MODID); + + /** Single MenuType for all script container sizes (1-6 rows). */ + public static final Supplier> SCRIPT_CONTAINER_MENU = + MENU_TYPES.register("script_container", + () -> new MenuType<>(new Box3JSScriptContainerMenu.Factory(), FeatureFlags.DEFAULT_FLAGS)); + public Box3JS(IEventBus modEventBus, ModContainer modContainer) { + // Register registries + MENU_TYPES.register(modEventBus); + // Register custom payloads modEventBus.addListener(RegisterPayloadHandlersEvent.class, event -> { var registrar = event.registrar("1"); @@ -68,8 +91,47 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { } } ); + + // Client → Server: GUI operations (open, setItem, registerCallbacks, close) + registrar.optional().playToServer( + Box3JSNetwork.GUIServerboundPayload.TYPE, + Box3JSNetwork.GUIServerboundPayload.STREAM_CODEC, + (payload, context) -> { + if (context.player() instanceof ServerPlayer sp) { + clientsWithBox3JS.add(sp.getUUID()); + switch (payload.actionType()) { + case 0 -> Box3JSGuiServerHandler.handleOpen( + sp, payload.title(), payload.rows(), payload.slotsJson()); + case 1 -> Box3JSGuiServerHandler.handleSetItem( + sp, payload.slot(), payload.itemId(), payload.count()); + case 2 -> Box3JSGuiServerHandler.handleRegisterCallbacks( + sp, payload.hasSlotClick(), payload.hasClose()); + case 3 -> Box3JSGuiServerHandler.handleClose(sp); + } + } + } + ); + + // Server → Client: GUI events (slot click, close) for client-side JS callbacks + registrar.optional().playToClient( + Box3JSNetwork.GUIClientboundPayload.TYPE, + Box3JSNetwork.GUIClientboundPayload.STREAM_CODEC, + (payload, context) -> { + Box3JSGuiProxy proxy = Box3JSClientEngine.get().getActiveGuiProxy(); + if (proxy != null) { + switch (payload.eventType()) { + case 0 -> proxy.fireSlotClick(payload.slot()); + case 1 -> proxy.fireClose(); + } + } + } + ); }); + // Register client-side screen for the script container + modEventBus.addListener(RegisterMenuScreensEvent.class, event -> + event.register(SCRIPT_CONTAINER_MENU.get(), Box3JSScriptContainerScreen::new)); + // Script commands NeoForge.EVENT_BUS.addListener(Box3ScriptCommand::register); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java index 7e89d5e..e4f3976 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java @@ -93,6 +93,74 @@ public Type type() { } } + // ── GUI payloads (client ↔ server) ── + + /** Client → Server: open/manipulate/close a script container GUI. */ + public record GUIServerboundPayload( + int actionType, // 0=OPEN, 1=SET_ITEM, 2=REGISTER_CALLBACKS, 3=CLOSE + String title, // for OPEN + int rows, // for OPEN + String slotsJson, // for OPEN (JSON object string of slot→itemId) + int slot, // for SET_ITEM + String itemId, // for SET_ITEM + int count, // for SET_ITEM + boolean hasSlotClick, // for REGISTER_CALLBACKS + boolean hasClose // for REGISTER_CALLBACKS + ) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(ResourceLocation.fromNamespaceAndPath(Box3JS.MODID, "gui_serverbound")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.of( + (buf, p) -> { + buf.writeVarInt(p.actionType); + buf.writeUtf(p.title); + buf.writeVarInt(p.rows); + buf.writeUtf(p.slotsJson); + buf.writeVarInt(p.slot); + buf.writeUtf(p.itemId); + buf.writeVarInt(p.count); + buf.writeBoolean(p.hasSlotClick); + buf.writeBoolean(p.hasClose); + }, + buf -> new GUIServerboundPayload( + buf.readVarInt(), + buf.readUtf(), + buf.readVarInt(), + buf.readUtf(), + buf.readVarInt(), + buf.readUtf(), + buf.readVarInt(), + buf.readBoolean(), + buf.readBoolean() + ) + ); + + @Override + public Type type() { return TYPE; } + } + + /** Server → Client: GUI events (slot click, close) for client-side JS callbacks. */ + public record GUIClientboundPayload( + int eventType, // 0=SLOT_CLICK, 1=CLOSE + int slot // for SLOT_CLICK (ignored for CLOSE) + ) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(ResourceLocation.fromNamespaceAndPath(Box3JS.MODID, "gui_clientbound")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, GUIClientboundPayload::eventType, + ByteBufCodecs.VAR_INT, GUIClientboundPayload::slot, + GUIClientboundPayload::new + ); + + @Override + public Type type() { return TYPE; } + } + // ── Server-side: send client scripts to a joining player ── private static final ResourceLocation CLIENT_SCRIPT_ID = diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java index 51fd2ff..4f57e6d 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java @@ -30,6 +30,15 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.client.multiplayer.ServerData; +import net.neoforged.neoforge.client.event.RenderGuiEvent; /** * Singleton client-side Rhino engine. @@ -61,10 +70,16 @@ public class Box3JSClientEngine { private volatile boolean tickRegistered; private volatile boolean keyRegistered; private volatile boolean chatRegistered; + private volatile boolean renderRegistered; + private volatile boolean mouseRegistered; + private final Map drawTexts = new ConcurrentHashMap<>(); + private final AtomicInteger drawTextIdCounter = new AtomicInteger(0); + private final List mouseClickHandlers = new CopyOnWriteArrayList<>(); private String currentProject = ""; private Box3JSClientStorage storage; private Box3JSClientDatabase database; private Box3JSClientHttp http; + private Box3JSGuiProxy activeGuiProxy; private volatile boolean dbWarningShown; private static final Map KEY_MAP = new HashMap<>(); @@ -104,6 +119,8 @@ public class Box3JSClientEngine { private Box3JSClientEngine() {} + public Box3JSGuiProxy getActiveGuiProxy() { return activeGuiProxy; } + // ── Initialisation ── private void init() { @@ -160,6 +177,101 @@ public Object call(Context cx, Scriptable scope, } }); + // client.getFPS() + ScriptableObject.putProperty(clientObj, "getFPS", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + return Minecraft.getInstance().getFps(); + } + }); + + // client.getPlayer() + ScriptableObject.putProperty(clientObj, "getPlayer", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + var player = Minecraft.getInstance().player; + if (player == null) return null; + Scriptable obj = cx.newObject(scope); + ScriptableObject.putProperty(obj, "name", player.getName().getString()); + ScriptableObject.putProperty(obj, "uuid", player.getUUID().toString()); + ScriptableObject.putProperty(obj, "health", player.getHealth()); + ScriptableObject.putProperty(obj, "maxHealth", player.getMaxHealth()); + ScriptableObject.putProperty(obj, "food", player.getFoodData().getFoodLevel()); + ScriptableObject.putProperty(obj, "saturation", player.getFoodData().getSaturationLevel()); + ScriptableObject.putProperty(obj, "xp", player.experienceLevel); + ScriptableObject.putProperty(obj, "dimension", player.level().dimension().location().toString()); + var pos = player.position(); + Scriptable objPos = cx.newObject(scope); + ScriptableObject.putProperty(objPos, "x", pos.x); + ScriptableObject.putProperty(objPos, "y", pos.y); + ScriptableObject.putProperty(objPos, "z", pos.z); + ScriptableObject.putProperty(obj, "position", objPos); + return obj; + } + }); + + // client.getLookingAt() + ScriptableObject.putProperty(clientObj, "getLookingAt", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + var hit = Minecraft.getInstance().hitResult; + if (hit == null || hit.getType() == HitResult.Type.MISS) return null; + Scriptable obj = cx.newObject(scope); + ScriptableObject.putProperty(obj, "type", hit.getType().name().toLowerCase()); + var loc = hit.getLocation(); + Scriptable objPos = cx.newObject(scope); + ScriptableObject.putProperty(objPos, "x", loc.x); + ScriptableObject.putProperty(objPos, "y", loc.y); + ScriptableObject.putProperty(objPos, "z", loc.z); + ScriptableObject.putProperty(obj, "position", objPos); + if (hit instanceof EntityHitResult ehr) { + Entity target = ehr.getEntity(); + Scriptable objEnt = cx.newObject(scope); + ScriptableObject.putProperty(objEnt, "name", target.getName().getString()); + ScriptableObject.putProperty(objEnt, "uuid", target.getUUID().toString()); + ScriptableObject.putProperty(objEnt, "type", target.getType().getDescriptionId()); + ScriptableObject.putProperty(obj, "entity", objEnt); + } + if (hit instanceof BlockHitResult bhr) { + var blockPos = bhr.getBlockPos(); + Scriptable objBlockPos = cx.newObject(scope); + ScriptableObject.putProperty(objBlockPos, "x", blockPos.getX()); + ScriptableObject.putProperty(objBlockPos, "y", blockPos.getY()); + ScriptableObject.putProperty(objBlockPos, "z", blockPos.getZ()); + ScriptableObject.putProperty(obj, "blockPos", objBlockPos); + ScriptableObject.putProperty(obj, "direction", bhr.getDirection().getName()); + } + return obj; + } + }); + + // client.getServerInfo() + ScriptableObject.putProperty(clientObj, "getServerInfo", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + var serverData = Minecraft.getInstance().getCurrentServer(); + if (serverData == null) { + // Singleplayer — return local info + Scriptable obj = cx.newObject(scope); + ScriptableObject.putProperty(obj, "ip", "localhost"); + ScriptableObject.putProperty(obj, "name", "Singleplayer"); + ScriptableObject.putProperty(obj, "isLocal", true); + return obj; + } + Scriptable obj = cx.newObject(scope); + ScriptableObject.putProperty(obj, "ip", serverData.ip); + ScriptableObject.putProperty(obj, "name", serverData.name); + ScriptableObject.putProperty(obj, "isLocal", false); + ScriptableObject.putProperty(obj, "playerCount", serverData.players != null ? serverData.players.online() : -1); + ScriptableObject.putProperty(obj, "maxPlayers", serverData.players != null ? serverData.players.max() : -1); + return obj; + } + }); + ScriptableObject.putProperty(scope, "client", clientObj); // -- audio global (sound playback) ------------------------------ @@ -287,6 +399,37 @@ public Object call(Context cx, Scriptable scope, } }); + // input.getMouseX() + ScriptableObject.putProperty(inputObj, "getMouseX", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + return Minecraft.getInstance().mouseHandler.xpos(); + } + }); + + // input.getMouseY() + ScriptableObject.putProperty(inputObj, "getMouseY", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + return Minecraft.getInstance().mouseHandler.ypos(); + } + }); + + // input.onMouseClick(callback) — returns GameEventHandlerToken + ScriptableObject.putProperty(inputObj, "onMouseClick", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1 || !(args[0] instanceof Function fn)) + return Undefined.instance; + mouseClickHandlers.add(fn); + registerMouseListener(); + return new GameEventHandlerToken(() -> mouseClickHandlers.remove(fn)); + } + }); + ScriptableObject.putProperty(scope, "input", inputObj); // -- ui global (screen overlays) -------------------------------- @@ -356,6 +499,66 @@ public Object call(Context cx, Scriptable scope, } }); + // ui.getScreenSize() + ScriptableObject.putProperty(uiObj, "getScreenSize", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + var window = Minecraft.getInstance().getWindow(); + Scriptable obj = cx.newObject(scope); + ScriptableObject.putProperty(obj, "width", window.getScreenWidth()); + ScriptableObject.putProperty(obj, "height", window.getScreenHeight()); + ScriptableObject.putProperty(obj, "scaledWidth", window.getGuiScaledWidth()); + ScriptableObject.putProperty(obj, "scaledHeight", window.getGuiScaledHeight()); + return obj; + } + }); + + // ui.drawText(id, x, y, text, color?) + ScriptableObject.putProperty(uiObj, "drawText", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 4) return -1; + int id = args[0] instanceof Number n ? n.intValue() : drawTextIdCounter.incrementAndGet(); + int x = ((Number) args[1]).intValue(); + int y = ((Number) args[2]).intValue(); + String text = args[3].toString(); + int color = 0xFFFFFFFF; + if (args.length > 4 && args[4] instanceof NativeObject c) { + int r = ((Number) c.get("r", c)).intValue(); + int g = ((Number) c.get("g", c)).intValue(); + int b = ((Number) c.get("b", c)).intValue(); + color = 0xFF000000 | (r << 16) | (g << 8) | b; + } + drawTexts.put(id, new DrawTextEntry(x, y, text, color)); + registerRenderListener(); + return id; + } + }); + + // ui.removeDrawText(id) + ScriptableObject.putProperty(uiObj, "removeDrawText", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1) return Undefined.instance; + int id = ((Number) args[0]).intValue(); + drawTexts.remove(id); + return Undefined.instance; + } + }); + + // ui.clearDrawTexts() + ScriptableObject.putProperty(uiObj, "clearDrawTexts", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + drawTexts.clear(); + return Undefined.instance; + } + }); + ScriptableObject.putProperty(scope, "ui", uiObj); // -- chat global (messaging) ------------------------------------ @@ -506,6 +709,54 @@ public Object call(Context cx, Scriptable scope, }); ScriptableObject.putProperty(scope, "http", httpObj); + // -- gui global ------------------------------------------------- + ScriptableObject guiObj = (ScriptableObject) cx.newObject(scope); + + ScriptableObject.putProperty(guiObj, "openGUI", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1 || !(args[0] instanceof NativeObject config)) + return Undefined.instance; + + String title = "Container"; + if (config.containsKey("title")) title = config.get("title").toString(); + int rows = 3; + if (config.containsKey("rows")) + rows = Math.max(1, Math.min(6, ((Number) config.get("rows")).intValue())); + + // Build slotsJson manually + String slotsJson = ""; + if (config.containsKey("slots") && config.get("slots") instanceof NativeObject slots + && slots.keySet().size() > 0) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Object key : slots.keySet()) { + Object val = slots.get(key); + if (val == null || val == UniqueTag.NOT_FOUND) continue; + if (!first) sb.append(","); + first = false; + sb.append("\"").append(key).append("\":\""); + sb.append(val.toString().replace("\\", "\\\\").replace("\"", "\\\"")); + sb.append("\""); + } + sb.append("}"); + slotsJson = sb.toString(); + } + + Box3JSGuiProxy proxy = new Box3JSGuiProxy(); + activeGuiProxy = proxy; + + PacketDistributor.sendToServer( + new Box3JSNetwork.GUIServerboundPayload(0, title, rows, slotsJson, + 0, "", 0, false, false)); + + return Context.javaToJS(proxy, scope); + } + }); + + ScriptableObject.putProperty(scope, "gui", guiObj); + // -- regex helpers (shared pure JS, from Box3ScriptUtils) --------- cx.evaluateString(scope, com.box3lab.box3js.script.Box3ScriptUtils.REGEX_HELPERS_JS, "regex-helpers", 1, null); @@ -685,6 +936,55 @@ private static String stringify(Context cx, Scriptable scope, Object value) { return com.box3lab.box3js.script.Box3ScriptUtils.stringify(cx, scope, value); } + // ── Render overlay for drawText ── + + private void registerRenderListener() { + if (renderRegistered) return; + Minecraft.getInstance().execute(() -> { + if (renderRegistered) return; + NeoForge.EVENT_BUS.addListener(RenderGuiEvent.Post.class, event -> { + if (drawTexts.isEmpty()) return; + GuiGraphics gfx = event.getGuiGraphics(); + var font = Minecraft.getInstance().font; + for (DrawTextEntry entry : drawTexts.values()) { + gfx.drawString(font, entry.text(), entry.x(), entry.y(), entry.color()); + } + }); + renderRegistered = true; + }); + } + + // ── Mouse click listener ── + + private void registerMouseListener() { + if (mouseRegistered) return; + Minecraft.getInstance().execute(() -> { + if (mouseRegistered) return; + NeoForge.EVENT_BUS.addListener(InputEvent.MouseButton.class, event -> { + if (mouseClickHandlers.isEmpty()) return; + int button = event.getButton(); + int action = event.getAction(); + double x = Minecraft.getInstance().mouseHandler.xpos(); + double y = Minecraft.getInstance().mouseHandler.ypos(); + for (Function fn : mouseClickHandlers) { + Context cx = Context.enter(); + try { + fn.call(cx, scope, scope, new Object[]{button, action, x, y}); + } catch (Exception e) { + LOGGER.error("Mouse click handler error", e); + } finally { + Context.exit(); + } + } + }); + mouseRegistered = true; + }); + } + + // ── Draw text entry ── + + private record DrawTextEntry(int x, int y, String text, int color) {} + // ── Console backend ── public static class Box3JSConsole { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java new file mode 100644 index 0000000..daf0510 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java @@ -0,0 +1,123 @@ +package com.box3lab.box3js.client; + +import com.box3lab.box3js.Box3JSNetwork; +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.network.PacketDistributor; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.NativeObject; +import org.slf4j.Logger; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Client-side proxy returned to JS from gui.openGUI(). + * Stores callbacks locally and sends C→S packets for mutations. + */ +public class Box3JSGuiProxy { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final List slotClickCallbacks = new CopyOnWriteArrayList<>(); + private final List closeCallbacks = new CopyOnWriteArrayList<>(); + private boolean callbacksRegistered; + + // ---- Slot manipulation (C→S packets) ---- + + public void setItem(int slot, String itemId, int count) { + PacketDistributor.sendToServer( + new Box3JSNetwork.GUIServerboundPayload(1, "", 0, "", slot, itemId, Math.max(1, Math.min(count, 64)), false, false)); + } + + public NativeObject getItem(int slot) { + NativeObject result = new NativeObject(); + var mc = Minecraft.getInstance(); + if (mc.player == null || mc.player.containerMenu == null) { + result.put("id", result, "minecraft:air"); + result.put("count", result, 0); + return result; + } + if (slot < 0 || slot >= mc.player.containerMenu.slots.size()) { + result.put("id", result, "minecraft:air"); + result.put("count", result, 0); + return result; + } + ItemStack stack = mc.player.containerMenu.getSlot(slot).getItem(); + if (stack.isEmpty()) { + result.put("id", result, "minecraft:air"); + result.put("count", result, 0); + } else { + var key = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem()); + result.put("id", result, key.toString()); + result.put("count", result, stack.getCount()); + } + return result; + } + + // ---- Callbacks ---- + + public void onSlotClick(Function callback) { + slotClickCallbacks.add(callback); + ensureCallbacksRegistered(); + } + + public void onClose(Function callback) { + closeCallbacks.add(callback); + ensureCallbacksRegistered(); + } + + private void ensureCallbacksRegistered() { + if (callbacksRegistered) return; + callbacksRegistered = true; + PacketDistributor.sendToServer( + new Box3JSNetwork.GUIServerboundPayload(2, "", 0, "", 0, "", 0, + !slotClickCallbacks.isEmpty(), !closeCallbacks.isEmpty())); + } + + // ---- Close ---- + + public void close() { + Minecraft.getInstance().execute(() -> { + if (Minecraft.getInstance().player != null) { + Minecraft.getInstance().player.closeContainer(); + } + }); + PacketDistributor.sendToServer( + new Box3JSNetwork.GUIServerboundPayload(3, "", 0, "", 0, "", 0, false, false)); + } + + // ---- Internal: called from GUIClientboundPayload handler (on netty thread → render thread) ---- + + public void fireSlotClick(int slot) { + Minecraft.getInstance().execute(() -> { + for (Function cb : slotClickCallbacks) { + Context cx = Context.enter(); + try { + cb.call(cx, cb.getParentScope(), cb, new Object[] { slot }); + } catch (Exception e) { + LOGGER.error("onSlotClick callback error", e); + } finally { + Context.exit(); + } + } + }); + } + + public void fireClose() { + Minecraft.getInstance().execute(() -> { + for (Function cb : closeCallbacks) { + Context cx = Context.enter(); + try { + cb.call(cx, cb.getParentScope(), cb, new Object[] {}); + } catch (Exception e) { + LOGGER.error("onClose callback error", e); + } finally { + Context.exit(); + } + } + }); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java new file mode 100644 index 0000000..22bfd61 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java @@ -0,0 +1,63 @@ +package com.box3lab.box3js.client.screen; + +import com.box3lab.box3js.script.Box3JSScriptContainerMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +public class Box3JSScriptContainerScreen extends AbstractContainerScreen { + + private static final ResourceLocation CONTAINER_BACKGROUND = + ResourceLocation.fromNamespaceAndPath("minecraft", "textures/gui/container/generic_54.png"); + + private final int rows; + + public Box3JSScriptContainerScreen(Box3JSScriptContainerMenu menu, Inventory playerInventory, + Component title) { + super(menu, playerInventory, title); + this.rows = menu.getRows(); + this.imageHeight = 114 + this.rows * 18; + this.inventoryLabelY = this.imageHeight - 94; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + this.renderTooltip(guiGraphics, mouseX, mouseY); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) / 2; + + // Draw chest background — only render the top part (dynamic rows) + int containerRows = this.rows; + int texHeight = 222; // Full 6-row texture height + int renderHeight = 18 + containerRows * 18; // Top border + rows + gap above inventory + + // Top of the container (including top border) + guiGraphics.blit(CONTAINER_BACKGROUND, x, y, 0, 0, this.imageWidth, 17); + // Middle rows + for (int row = 0; row < containerRows; row++) { + guiGraphics.blit(CONTAINER_BACKGROUND, x, y + 17 + row * 18, 0, 17, this.imageWidth, 18); + } + // Gap + player inventory area + int bottomStart = 17 + containerRows * 18; + int bottomTexY = 215 - 96; // Transition area above player inventory in texture + // Actually render the bottom inventory section from the 6-row texture + int invSectionStart = 17 + containerRows * 18; + // Copy the player inventory from the fixed texture + for (int row = 0; row < 4; row++) { + int srcY = 17 + 5 * 18 + row * 18; // Start from row 5 in the 6-row texture (skip rows) + // Actually use the bottom inventory strip from the texture + guiGraphics.blit(CONTAINER_BACKGROUND, x, y + invSectionStart + row * 18, + 0, 125 + row * 18, this.imageWidth, 18); + } + // Render the remaining lines below + guiGraphics.blit(CONTAINER_BACKGROUND, x, y + invSectionStart + 4 * 18, + 0, 215 - 7, this.imageWidth, 7); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java new file mode 100644 index 0000000..f2e6b0d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java @@ -0,0 +1,88 @@ +package com.box3lab.box3js.script; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import org.mozilla.javascript.NativeObject; + +import java.util.function.Consumer; + +/** + * Server-side container manager for script GUIs. The menu delegates click/close + * events here; the server handler wires these to S→C notification packets. + */ +public class Box3JSGuiController { + + private Box3JSScriptContainerMenu menu; + private final ServerPlayer owner; + private Consumer slotClickNotifier; + private Runnable closeNotifier; + + public Box3JSGuiController(Box3JSScriptContainerMenu menu, ServerPlayer owner) { + this.menu = menu; + this.owner = owner; + } + + // ---- Slot manipulation ---- + + void setMenu(Box3JSScriptContainerMenu menu) { + this.menu = menu; + } + + public void setItem(int slot, String itemId, int count) { + if (menu == null) return; + var item = Box3ScriptUtils.lookupItem(itemId); + if (item == null) return; + if (slot < 0 || slot >= menu.getContainer().getContainerSize()) return; + menu.getContainer().setItem(slot, new ItemStack(item, Math.max(1, Math.min(count, 64)))); + } + + public NativeObject getItem(int slot) { + NativeObject result = new NativeObject(); + if (menu == null || slot < 0 || slot >= menu.getContainer().getContainerSize()) { + result.put("id", result, "minecraft:air"); + result.put("count", result, 0); + return result; + } + ItemStack stack = menu.getContainer().getItem(slot); + if (stack.isEmpty()) { + result.put("id", result, "minecraft:air"); + result.put("count", result, 0); + } else { + var key = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem()); + result.put("id", result, key.toString()); + result.put("count", result, stack.getCount()); + } + return result; + } + + // ---- Event notifiers (set by server handler) ---- + + public void onSlotClickEvent(Consumer notifier) { + this.slotClickNotifier = notifier; + } + + public void onCloseEvent(Runnable notifier) { + this.closeNotifier = notifier; + } + + // ---- Close ---- + + public void close() { + owner.closeContainer(); + } + + // ---- Internal: called from Box3JSScriptContainerMenu ---- + + boolean fireSlotClick(int slot) { + if (slotClickNotifier != null) { + slotClickNotifier.accept(slot); + } + return true; // always allow click; callback is client-side notification + } + + void fireClose() { + if (closeNotifier != null) { + closeNotifier.run(); + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java new file mode 100644 index 0000000..a2aba6e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java @@ -0,0 +1,127 @@ +package com.box3lab.box3js.script; + +import com.box3lab.box3js.Box3JS; +import com.box3lab.box3js.Box3JSNetwork; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.neoforged.neoforge.network.PacketDistributor; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Handles client→server GUI packets on the server thread. + * Creates containers, manages active GUIs per player, and forwards events back to the client. + */ +public final class Box3JSGuiServerHandler { + + private Box3JSGuiServerHandler() {} + + private static final Map activeGuis = new ConcurrentHashMap<>(); + + private static class ActiveGui { + final Box3JSScriptContainerMenu menu; + final Box3JSGuiController controller; + final ServerPlayer player; + boolean hasSlotClick; + boolean hasClose; + + ActiveGui(Box3JSScriptContainerMenu menu, Box3JSGuiController controller, ServerPlayer player) { + this.menu = menu; + this.controller = controller; + this.player = player; + } + } + + public static void handleOpen(ServerPlayer player, String title, int rows, String slotsJson) { + // Close any existing GUI for this player + handleClose(player); + + Box3JSGuiController controller = new Box3JSGuiController(null, player); + + MenuProvider provider = new SimpleMenuProvider((containerId, inv, p) -> { + Box3JSScriptContainerMenu menu = new Box3JSScriptContainerMenu( + Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, inv, rows, controller); + controller.setMenu(menu); + + // Populate initial slots from JSON string like {"0":"minecraft:diamond","1":"minecraft:stone"} + if (slotsJson != null && !slotsJson.isEmpty()) { + String inner = slotsJson.trim(); + if (inner.startsWith("{") && inner.endsWith("}")) { + inner = inner.substring(1, inner.length() - 1); + for (String pair : inner.split(",")) { + pair = pair.trim(); + if (pair.isEmpty()) continue; + int colon = pair.indexOf(':'); + if (colon < 0) continue; + try { + String key = pair.substring(0, colon).trim(); + if (key.startsWith("\"") && key.endsWith("\"")) + key = key.substring(1, key.length() - 1); + int slot = Integer.parseInt(key); + String val = pair.substring(colon + 1).trim(); + if (val.startsWith("\"") && val.endsWith("\"")) + val = val.substring(1, val.length() - 1); + var item = Box3ScriptUtils.lookupItem(val); + if (item != null && slot >= 0 && slot < rows * 9) { + menu.getContainer().setItem(slot, new net.minecraft.world.item.ItemStack(item, 1)); + } + } catch (NumberFormatException | IndexOutOfBoundsException ignored) {} + } + } + } + + return menu; + }, Component.literal(title)); + + player.openMenu(provider, buf -> buf.writeVarInt(rows)); + + // After openMenu, the player's containerMenu is the new menu + if (player.containerMenu instanceof Box3JSScriptContainerMenu menu) { + ActiveGui gui = new ActiveGui(menu, controller, player); + + // Set up close notifier for cleanup + event forwarding + controller.onCloseEvent(() -> { + if (gui.hasClose) { + PacketDistributor.sendToPlayer(player, + new Box3JSNetwork.GUIClientboundPayload(1, 0)); + } + activeGuis.remove(player.getUUID()); + }); + + // Set up slot click notifier for event forwarding + controller.onSlotClickEvent(slot -> { + if (gui.hasSlotClick) { + PacketDistributor.sendToPlayer(player, + new Box3JSNetwork.GUIClientboundPayload(0, slot)); + } + }); + + activeGuis.put(player.getUUID(), gui); + } + } + + public static void handleSetItem(ServerPlayer player, int slot, String itemId, int count) { + ActiveGui gui = activeGuis.get(player.getUUID()); + if (gui != null) { + gui.controller.setItem(slot, itemId, count); + } + } + + public static void handleRegisterCallbacks(ServerPlayer player, boolean hasSlotClick, boolean hasClose) { + ActiveGui gui = activeGuis.get(player.getUUID()); + if (gui != null) { + gui.hasSlotClick = gui.hasSlotClick || hasSlotClick; + gui.hasClose = gui.hasClose || hasClose; + } + } + + public static void handleClose(ServerPlayer player) { + ActiveGui gui = activeGuis.remove(player.getUUID()); + if (gui != null) { + player.closeContainer(); + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java index 1945323..0686430 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java @@ -71,11 +71,6 @@ public GameVector3 getBounds() { public String getUserId() { return player.getUUID().toString(); } public ServerPlayer getPlayer() { return player; } - /** Whether this player has Box3JS installed on their client. */ - public boolean hasBox3JSClientMod() { - return com.box3lab.box3js.Box3JS.clientsWithBox3JS.contains(player.getUUID()); - } - public int getOpLevel() { return server.getProfilePermissions(player.getGameProfile()); } public void setOpLevel(int level) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java new file mode 100644 index 0000000..a58c7a9 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java @@ -0,0 +1,143 @@ +package com.box3lab.box3js.script; + +import com.box3lab.box3js.Box3JS; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.flag.FeatureFlags; +import net.neoforged.neoforge.network.IContainerFactory; + +public class Box3JSScriptContainerMenu extends AbstractContainerMenu { + + public static final int SLOTS_PER_ROW = 9; + + private final SimpleContainer container; + private final int rows; + private Box3JSGuiController controller; + + public Box3JSScriptContainerMenu(MenuType type, int containerId, Inventory playerInventory, int rows, + Box3JSGuiController controller) { + super(type, containerId); + this.rows = rows; + this.container = new SimpleContainer(rows * SLOTS_PER_ROW); + this.controller = controller; + + // Container slots + for (int i = 0; i < rows * SLOTS_PER_ROW; i++) { + int col = i % SLOTS_PER_ROW; + int row = i / SLOTS_PER_ROW; + this.addSlot(new Slot(container, i, 8 + col * 18, 18 + row * 18)); + } + + // Player inventory + addPlayerInventory(playerInventory, 8, 84 + (rows - 3) * 18); + } + + public Box3JSGuiController getController() { + return controller; + } + + public void setController(Box3JSGuiController controller) { + this.controller = controller; + } + + public SimpleContainer getContainer() { + return container; + } + + public int getRows() { + return rows; + } + + @Override + public void clicked(int slot, int button, ClickType clickType, Player player) { + if (controller != null) { + int scriptSlots = rows * SLOTS_PER_ROW; + if (slot >= 0 && slot < scriptSlots) { + controller.fireSlotClick(slot); + } + } + super.clicked(slot, button, clickType, player); + } + + @Override + public void removed(Player player) { + super.removed(player); + if (controller != null) { + controller.fireClose(); + } + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + int scriptSlots = rows * SLOTS_PER_ROW; + Slot slot = this.slots.get(index); + if (!slot.hasItem()) return ItemStack.EMPTY; + + ItemStack stackInSlot = slot.getItem(); + ItemStack copy = stackInSlot.copy(); + + if (index < scriptSlots) { + // From script container → player inventory + if (!this.moveItemStackTo(stackInSlot, scriptSlots, this.slots.size(), true)) { + return ItemStack.EMPTY; + } + } else { + // From player inventory → script container + if (!this.moveItemStackTo(stackInSlot, 0, scriptSlots, false)) { + return ItemStack.EMPTY; + } + } + + if (stackInSlot.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + return copy; + } + + // ---- Factory for MenuType registration ---- + + public static class Factory implements MenuType.MenuSupplier, + IContainerFactory { + @Override + public Box3JSScriptContainerMenu create(int containerId, Inventory playerInventory) { + // Default 3 rows — used as fallback if IContainerFactory path fails + return new Box3JSScriptContainerMenu( + Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, playerInventory, 3, null); + } + + @Override + public Box3JSScriptContainerMenu create(int containerId, Inventory playerInventory, + RegistryFriendlyByteBuf extraData) { + int rows = extraData.readVarInt(); + return new Box3JSScriptContainerMenu( + Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, playerInventory, rows, null); + } + } + + // ---- Player inventory helper ---- + + private void addPlayerInventory(Inventory playerInventory, int x, int y) { + // Hotbar (bottom row) + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col, x + col * 18, y + 58)); + } + // Main inventory (3 rows above hotbar) + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col + row * 9 + 9, x + col * 18, y + row * 18)); + } + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java index 84adc23..a96e8f1 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -336,7 +336,7 @@ private Runnable wrapContext(String project, Runnable cb) { }; } - private void runInContext(String project, Runnable action) { + public void runInContext(String project, Runnable action) { String prev = currentProject; setCurrentProject(project); try { diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts index 4090883..fa048a5 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts @@ -3,6 +3,7 @@ /// /// /// +/// // ── §1 @zh RemoteChannel 客户端方法(接口合并) @en RemoteChannel client‑side methods (interface merging) ── @@ -36,6 +37,60 @@ interface RemoteChannel { interface GameClient { /** @zh 注册客户端每 tick 回调(每秒 20 次)。 @en Registers a callback invoked every client tick (20/sec). */ onTick(callback: () => void): void; + + /** + * @zh 获取当前帧率 (FPS)。 + * @en Gets the current frames per second (FPS). + * @returns @zh FPS 数值 @en FPS value + */ + getFPS(): number; + + /** + * @zh 获取本地玩家信息。 + * @en Gets local player information. + * @returns @zh `{ name, uuid, health, maxHealth, food, saturation, xp, dimension, position }` 或 null @en `{ name, uuid, health, maxHealth, food, saturation, xp, dimension, position }` or null + */ + getPlayer(): { + name: string; + uuid: string; + health: number; + maxHealth: number; + food: number; + saturation: number; + xp: number; + dimension: string; + position: { x: number; y: number; z: number }; + } | null; + + /** + * @zh 获取玩家准星正在看向的目标。 + * @en Gets what the player's crosshair is currently pointing at. + * @returns @zh `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` 或 null @en `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` or null + */ + getLookingAt(): { + type: string; + position: { x: number; y: number; z: number }; + entity?: { + name: string; + uuid: string; + type: string; + }; + blockPos?: { x: number; y: number; z: number }; + direction?: string; + } | null; + + /** + * @zh 获取当前连接的服务器信息。 + * @en Gets current server connection information. + * @returns @zh `{ ip, name, isLocal, playerCount?, maxPlayers? }` — 单人游戏返回 localhost @en `{ ip, name, isLocal, playerCount?, maxPlayers? }` — returns localhost for singleplayer + */ + getServerInfo(): { + ip: string; + name: string; + isLocal: boolean; + playerCount: number; + maxPlayers: number; + }; } // ── §7 @zh 全局声明(客户端) @en Global Declarations (client) ── @@ -45,5 +100,6 @@ declare const client: GameClient; declare const input: GameInput; declare const ui: GameUI; declare const chat: GameChat; +declare const gui: GameGUI; // storage, console, remoteChannel, db, http — declared in shared.d.ts diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts new file mode 100644 index 0000000..4982fc5 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts @@ -0,0 +1,74 @@ +/// + +// ── § @zh 自定义 GUI @en Custom GUI ── + +/** @zh 通过 `gui` 访问:自定义容器界面 @en Accessed via `gui`: custom container GUI */ +interface GameGUI { + /** + * @zh 打开一个脚本控制的自定义容器界面。 + * @en Opens a script-controlled custom container GUI. + * + * @example + * ```ts + * const ctrl = gui.openGUI({ title: "Shop", rows: 3, slots: { 0: "minecraft:diamond" } }); + * console.log(ctrl.getItem(0)); // { id: "minecraft:diamond", count: 1 } + * ctrl.onSlotClick((slot) => { console.log("Clicked slot:", slot); }); + * ctrl.onClose(() => { console.log("GUI closed"); }); + * ctrl.close(); + * ``` + * + * @param config - @zh 配置对象 @en configuration object + * @returns @zh GuiController — 可操作容器和注册事件回调 @en GuiController — for manipulating the container and registering event callbacks + */ + openGUI(config: GuiOpenConfig): GuiController; +} + +/** @zh GUI 配置 @en GUI configuration */ +interface GuiOpenConfig { + /** @zh 标题(默认 "Container") @en Title (default "Container") */ + title?: string; + /** @zh 行数 1–6(默认 3) @en Number of rows 1–6 (default 3) */ + rows?: number; + /** @zh 初始物品 `{ [slotIndex: number]: string }` @en Initial items `{ [slotIndex: number]: string }` */ + slots?: { [slot: number]: string }; +} + +/** @zh GUI 控制器 @en GUI controller */ +interface GuiController { + /** + * @zh 设置指定槽位的物品。 + * @en Sets the item in the given slot. + * @param slot - @zh 槽位索引 (0–rows×9-1) @en Slot index (0–rows×9-1) + * @param itemId - @zh 物品 ID @en Item ID + * @param count - @zh 数量(可选,默认 1) @en Count (optional, default 1) + */ + setItem(slot: number, itemId: string, count?: number): void; + + /** + * @zh 获取指定槽位的物品。 + * @en Gets the item in the given slot. + * @param slot - @zh 槽位索引 @en Slot index + * @returns @zh `{ id: string, count: number }` — 空槽位返回 minecraft:air, count 0 @en `{ id: string, count: number }` — empty slot returns minecraft:air, count 0 + */ + getItem(slot: number): { id: string; count: number }; + + /** + * @zh 注册槽位点击回调(仅通知,无法取消点击)。 + * @en Registers a slot click callback (notification only, cannot cancel the click). + * @param callback - @zh 回调函数,接收被点击的槽位索引 @en Callback receiving the clicked slot index + */ + onSlotClick(callback: (slot: number) => void): void; + + /** + * @zh 注册关闭回调。 + * @en Registers a close callback. + * @param callback - @zh GUI 关闭时调用的回调函数 @en Callback called when the GUI is closed + */ + onClose(callback: () => void): void; + + /** + * @zh 关闭 GUI。 + * @en Closes the GUI. + */ + close(): void; +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts index c00901d..213117b 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts @@ -38,4 +38,33 @@ interface GameInput { * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to unsubscribe */ onKeyPress(key: KeyName, callback: () => void): GameEventHandlerToken; + + /** + * @zh 获取当前鼠标 X 坐标(屏幕像素)。 + * @en Gets the current mouse X position in screen pixels. + * @returns @zh 鼠标 X 坐标 @en mouse X coordinate + */ + getMouseX(): number; + + /** + * @zh 获取当前鼠标 Y 坐标(屏幕像素)。 + * @en Gets the current mouse Y position in screen pixels. + * @returns @zh 鼠标 Y 坐标 @en mouse Y coordinate + */ + getMouseY(): number; + + /** + * @zh 注册鼠标点击回调。 + * @en Registers a mouse button click callback. + * @param callback - @zh 回调函数 (button, action, x, y)。button: 0=左键 1=右键 2=中键; action: 0=释放 1=按下 2=重复; x/y: 鼠标坐标 @en callback (button, action, x, y). button: 0=left 1=right 2=middle; action: 0=release 1=press 2=repeat; x/y: mouse position + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to unsubscribe + */ + onMouseClick( + callback: ( + button: number, + action: number, + x: number, + y: number, + ) => void, + ): GameEventHandlerToken; } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/ui.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/ui.d.ts index 818dda4..8d54cc9 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/ui.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/ui.d.ts @@ -34,4 +34,47 @@ interface GameUI { * @param text - @zh 要显示的文字 @en text to display */ showActionBar(text: string): void; + + /** + * @zh 获取当前屏幕/窗口尺寸。 + * @en Gets the current screen / window dimensions. + * @returns @zh `{ width, height, scaledWidth, scaledHeight }` — width/height 为窗口像素, scaledWidth/scaledHeight 为 GUI 缩放后尺寸 @en `{ width, height, scaledWidth, scaledHeight }` — width/height are window pixels, scaledWidth/scaledHeight are GUI-scaled dimensions + */ + getScreenSize(): { + width: number; + height: number; + scaledWidth: number; + scaledHeight: number; + }; + + /** + * @zh 在屏幕上绘制自定义文字(每帧绘制,直到被移除)。 + * @en Draws custom text on screen (drawn every frame until removed). + * @param id - @zh 文字 ID(用于后续移除),不传则自动生成 @en text ID for later removal; auto-generated if omitted + * @param x - @zh X 坐标(GUI 缩放坐标) @en X position (GUI-scaled coordinates) + * @param y - @zh Y 坐标(GUI 缩放坐标) @en Y position (GUI-scaled coordinates) + * @param text - @zh 要显示的文字 @en text to display + * @param color - @zh 文字颜色 (GameRGBColor / GameRGBAColor, 默认白色) @en text colour (GameRGBColor / GameRGBAColor, default white) + * @returns @zh 文字 ID @en the text ID + */ + drawText( + id: number, + x: number, + y: number, + text: string, + color?: GameRGBColor | GameRGBAColor, + ): number; + + /** + * @zh 移除指定 ID 的绘制文字。 + * @en Removes the drawn text with the given ID. + * @param id - @zh 文字 ID @en text ID + */ + removeDrawText(id: number): void; + + /** + * @zh 清除所有通过 `drawText()` 绘制的文字。 + * @en Clears all texts drawn via `drawText()`. + */ + clearDrawTexts(): void; } From 8de46cdf1b43bda31f2ba2932d90dd340100d581 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 13 May 2026 17:18:34 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(api):=20=E6=9B=B4=E6=96=B0=20API=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8C=E6=98=8E=E7=A1=AE=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E7=8E=AF=E5=A2=83=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 `world.projectName` 从属性改为方法 `world.projectName()`,返回当前脚本项目名称;使用 `world.serverId` 获取服务器 MOTD - 将 `world.currentTick` 从属性改为方法 `world.currentTick()`,返回服务器启动以来的总 tick 数 - 在 README 中新增服务端概述文档章节 - 在客户端 API 文档中增加 `gui` 全局对象 - 明确所有 API 对象的运行时环境(服务端/客户端/共享) - 按运行时环境拆分文档结构 - 添加文档风格规范与指南 - 将类型声明按服务端/客户端整理到对应目录 - 明确说明客户端脚本无法直接修改服务端世界 - 更新数据库、HTTP 及存储 API 文档,补充运行时信息 - 修正示例路径,使用 `src/server/app.ts` 替代 `src/app.ts` - 更新完整的 API 对比表 --- AGENTS.md | 136 +++++++++++ .../docs/BOX3_API_COMPARISON.md | 4 +- Box3JS-NeoForge-1.21.1/docs/README.md | 3 +- Box3JS-NeoForge-1.21.1/docs/README_en.md | 3 +- Box3JS-NeoForge-1.21.1/docs/api/README.md | 81 ++++-- Box3JS-NeoForge-1.21.1/docs/api/README_en.md | 73 ++++-- Box3JS-NeoForge-1.21.1/docs/api/client.md | 20 +- Box3JS-NeoForge-1.21.1/docs/api/client_en.md | 20 +- Box3JS-NeoForge-1.21.1/docs/api/database.md | 4 +- .../docs/api/database_en.md | 4 +- Box3JS-NeoForge-1.21.1/docs/api/http.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/http_en.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/server.md | 230 ++++++++++++++++++ Box3JS-NeoForge-1.21.1/docs/api/server_en.md | 230 ++++++++++++++++++ Box3JS-NeoForge-1.21.1/docs/api/storage.md | 12 +- Box3JS-NeoForge-1.21.1/docs/api/storage_en.md | 12 +- Box3JS-NeoForge-1.21.1/docs/api/world.md | 12 +- Box3JS-NeoForge-1.21.1/docs/api/world_en.md | 12 +- .../docs/guide/getting-started.md | 9 +- .../docs/guide/getting-started_en.md | 8 +- .../docs/tutorial/01-basics.md | 4 +- .../docs/tutorial/01-basics_en.md | 4 +- .../docs/tutorial/05-examples.md | 2 +- .../docs/tutorial/05-examples_en.md | 2 +- .../box3lab/box3js/client/Box3JSGuiProxy.java | 30 ++- .../box3lab/box3js/script/Box3JSVoxels.java | 8 +- .../box3lab/box3js/script/Box3JSWorld.java | 4 +- .../box3js/script/Box3ScriptEngine.java | 13 +- .../box3js/script/Box3ScriptTemplate.java | 5 +- .../box3js/script/Box3ScriptWatcher.java | 37 ++- .../assets/box3js/template/build.mjs | 77 ++++-- .../assets/box3js/template/package.json | 2 + .../assets/box3js/template/src/client/app.ts | 33 +-- .../assets/box3js/template/src/server/app.ts | 25 +- .../assets/box3js/template/tsconfig.base.json | 3 +- .../box3js/template/tsconfig.client.json | 2 +- .../box3js/template/tsconfig.server.json | 2 +- .../box3js/template/types/client/gui.d.ts | 8 +- .../box3js/template/types/client/index.d.ts | 8 + .../box3js/template/types/server/index.d.ts | 7 + .../box3js/template/types/server/server.d.ts | 7 +- .../box3js/template/types/server/world.d.ts | 6 +- 42 files changed, 977 insertions(+), 189 deletions(-) create mode 100644 AGENTS.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/server.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/api/server_en.md create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/index.d.ts create mode 100644 Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/index.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..27fd94d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,136 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +## Project Overview + +Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a server-side TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, and world interactions. + +The repository is a **multi-project monorepo** with 7 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. + +## Subprojects + +| Directory | Loader | MC Version | Java | Notes | +| ------------------ | -------- | ---------- | ---- | ---------------------------------------- | +| `Fabric-1.20.1/` | Fabric | 1.20.1 | 17 | `fabric-loom-remap` | +| `Fabric-1.21.1/` | Fabric | 1.21.1 | 21 | `fabric-loom-remap` | +| `Fabric-1.21.11/` | Fabric | 1.21.11 | 21 | `fabric-loom-remap` | +| `Fabric-26.1/` | Fabric | 26.1 | 25 | `fabric-loom` | +| `Forge-1.20.1/` | Forge | 1.20.1 | 17 | `net.minecraftforge.gradle` v6.x | +| `NeoForge-1.21.1/` | NeoForge | 1.21.1 | 21 | **Box3JS lives here** — NeoForge ModDevGradle | +| `NeoForge-26.1/` | NeoForge | 26.1 | 25 | NeoForge ModDevGradle | + +Only NeoForge-1.21.1 has the Box3JS scripting engine. The other 6 subprojects are purely the Box3Blocks decorative block mod. + +## Build Commands + +```bash +# Build a single subproject +cd NeoForge-1.21.1 && ./gradlew build + +# Clean build artifacts +cd NeoForge-1.21.1 && ./gradlew clean + +# Build Box3JS script (in run/config/box3/script//) +cd run/config/box3/script/colorzone +npm install && npm run build # esbuild → Babel → Rhino target +``` + +**Important:** Forge-1.20.1 requires Java 17. All other subprojects use Java 21+. NeoForge-26.1 uses Java 25. + +There are no existing tests (`src/test` directories are empty). + +## Shared Resources Architecture + +Shared resources are centralized to avoid ~20,000 duplicate asset files: + +- **`shared-resources/`** — used by ALL subprojects: block textures, models, blockstates, item models, worldgen data, `block-id.json`, `block-spec.json` +- **`shared-resources-fabric/`** — used by all 4 Fabric subprojects: `models/item/` JSONs + lang files +- **`shared-resources-forge/`** — used by Forge + both NeoForge subprojects: `models/item/` JSONs + lang files + +## Block Mod Architecture + +All subprojects (including NeoForge-1.21.1) share the block mod's runtime generation architecture: + +- `BlockIndexData` / `BlockIndexUtil` reads `block-id.json` and `block-spec.json` at registration time +- `VoxelBlockFactories` creates `Block` instances dynamically (no per-block Java classes) +- Only 6 special blocks have dedicated Java classes: `VoxelBlock`, `GlassVoxelBlock`, `BarrierVoxelBlock`, `BouncePadBlock`, `ConveyorBlock`, `SpiderWebBlock` + +## Box3JS Scripting Engine (NeoForge-1.21.1 only) + +Box3JS uses Mozilla Rhino to run server-side JavaScript/TypeScript. Scripts live in `run/config/box3/script//`. Each project has its own isolated scope, callbacks, and tracked state. + +### Java Package: `com.box3lab.box3js` + +| File | Role | +|------|------| +| `Box3JS.java` | `@Mod` entry point, subscribes to NeoForge events, fires callbacks into JS | +| `script/Box3ScriptEngine.java` | Singleton Rhino engine: load/reload/stop scripts, fire events, manage scopes | +| `script/Box3ScriptCommand.java` | `/box3script` command handler | +| `script/Box3ScriptConfig.java` | Config: enabled projects, sandbox state, file watcher | +| `script/Box3ScriptSandbox.java` | Tracks block/entity/player/world mutations for rollback | +| `script/Box3ScriptTemplate.java` | Template for `/box3script create` | +| `script/Box3ScriptWatcher.java` | File watching + auto-reload on `.js` change | +| `script/Box3JSWorld.java` | `world.*` API: events, entity queries, scoreboard, BossBar, teams, border, particles, fireworks, recipes, structures, custom items | +| `script/Box3JSEntity.java` | `entity.*` API: position, velocity, HP, tags, AI, equipment, effects | +| `script/Box3JSPlayer.java` | `player.*` API: inventory, flight, game mode, teleport, XP, food, advancements, tab list | +| `script/Box3JSVoxels.java` | `voxels.*` API: get/set voxel, fill region, spawner control | +| `script/Box3JSQuery.java` | `world.querySelectorAll()` / `entitiesInRadius()` etc. | +| `script/Box3JSEventBus.java` | Per-project callback storage with isolation | +| `script/Box3JSCallbacks.java` | Callback interface definitions | +| `script/Box3JSScoreboard.java` | Scoreboard CRUD | +| `script/Box3JSBossbar.java` | BossBar CRUD | +| `script/Box3JSTeam.java` | Team CRUD | +| `script/Box3JSStorage.java` | Per-project JSON file persistence | +| `script/Box3ScriptUtils.java` | Shared helpers: sound playing, raycast, entity lookAt | +| `script/GameVector3.java` | 3D vector exposed to JS (`new GameVector3(x, y, z)`) | +| `script/GameBounds3.java` | AABB bounds | +| `script/GameRGBColor.java` / `GameRGBAColor.java` | Color types | +| `script/GameQuaternion.java` | Quaternion math | +| `script/GameEventHandlerToken.java` | Returned by `world.onXxx()` — has `cancel()` and `active()` | +| `registries/Box3JSCustomItems.java` | Custom items via Minecraft data components on `minecraft:paper` carrier | +| `registries/Box3JSRecipeManager.java` | Recipe blacklist via `RecipeManager.replaceRecipes()` | + +### DTS Type Constraints + +`world.currentTick` and `world.projectName` are **methods** in `globals.d.ts`, not properties: +```ts +world.currentTick() // ✅ returns number +world.projectName() // ✅ returns string +``` + +### Script Build Pipeline + +`build.mjs` in each script project does: `esbuild bundle` → `Babel` (target Rhino 1.9.1) → regex sanitize for Rhino. Entry is always `src/app.ts`, output is `dist/app.js`. Supports `--watch` for hot reload. + +### Custom Items System + +Uses `minecraft:paper` as carrier with `DataComponents` (CUSTOM_NAME, LORE, CUSTOM_MODEL_DATA, MAX_STACK_SIZE, ENCHANTMENT_GLINT_OVERRIDE, RARITY, FOOD). Client-side textures via resource pack `paper.json` with `custom_model_data` overrides. **No DeferredRegister** — no registry sync needed. + +Config: `resourcepacks/box3js-items/items.json` + textures + model JSONs. Loaded via `world.loadCustomItems("box3js-items")`. + +Consumable/Cooldown/Enchantable/JukeboxPlayable components are NOT available in NeoForge 21.1.220 (need MC 1.21.2+). + +### Recipe Manager + +`Box3JSRecipeManager` uses `RecipeManager.replaceRecipes()` (public API, no reflection): +- `removeRecipe(id)` — filters via replaceRecipes +- `clearRecipes()` — restores full original list +- `listRecipes(filter)` — searches by keyword + +### Documentation + +- `docs/api/` — Full API reference for world, entity, player, voxels, storage, math, commands (Chinese + English) +- `docs/tutorial/` — 5-part tutorial series (01-basics → 05-examples) with complete PvP arena and parkour game examples + +## Version Differences + +- `VoxelExport` only in Fabric-1.21.11, Fabric-26.1, and Forge/NeoForge variants +- `VoxelFluidRenderHandler` only in Fabric-1.21.11 +- NeoForge-26.1 moved client code to `src/client/java` +- Fabric-26.1 uses `fabric-loom` (not `fabric-loom-remap`) and Java 25 + +## Tools + +- **`tools/generate_blocks_fabric.py`** / **`tools/generate_blocks_forge.py`** — generates block registration code +- **`tools/box3-texture-cut/`** — TypeScript tool for cutting sprite sheets into textures diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md index 8e989ce..14bfc72 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md @@ -27,9 +27,9 @@ | Box3 API | Box3JS 实现 | 状态 | 差异说明 | |----------|-------------|------|---------| -| `world.projectName` (只读属性) | `world.projectName` (属性) | ✅ | 一致。返回服务端 MOTD 字符串 | +| `world.projectName()` (只读方法) | `world.projectName` (属性) | ✅ | 返回当前脚本项目名;服务器 MOTD 使用 `world.serverId` | | `world.serverId` (属性) | `world.serverId` (读写属性) | ✅ | 一致。映射到服务端 MOTD(get/set) | -| `world.currentTick` (只读属性) | `world.currentTick` (属性) | ✅ | 一致。返回服务器总 tick 数 | +| `world.currentTick()` (只读方法) | `world.currentTick` (属性) | ✅ | 返回服务器总 tick 数 | | `world.url` (只读属性) | — | ❌ | 未实现。MC 无地图 URL 概念 | ### 1.2 天气 diff --git a/Box3JS-NeoForge-1.21.1/docs/README.md b/Box3JS-NeoForge-1.21.1/docs/README.md index 65a0082..efe5e30 100644 --- a/Box3JS-NeoForge-1.21.1/docs/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/README.md @@ -36,6 +36,7 @@ Box3JS 是 Minecraft NeoForge 1.21.1 的 JavaScript/TypeScript 脚本引擎模 | 分类 | 文档 | 全局对象 | |------|------|---------| +| **服务端总览** | [server](api/server.md) | 服务端运行边界、事件、玩家/实体、方块、数据、跨端通信 | | **世界** | [world](api/world.md) | `world` — 事件、粒子、烟花、音效、计分板 | | **实体** | [entity](api/entity.md) | `entity` — 属性、AI、装备、效果 | | **玩家** | [player](api/player.md) | `player` — 背包、消息、飞行、传送 | @@ -43,7 +44,7 @@ Box3JS 是 Minecraft NeoForge 1.21.1 的 JavaScript/TypeScript 脚本引擎模 | **存储** | [storage](api/storage.md) | `storage` — JSON 持久化 | | **数据库** | [database](api/database.md) | `db` — SQLite 数据库 | | **网络** | [http](api/http.md) | `http` — HTTP 请求 | -| **客户端** | [client](api/client.md) | `audio` `client` `input` `ui` `chat` `remoteChannel` | +| **客户端** | [client](api/client.md) | `audio` `client` `input` `ui` `chat` `gui` `remoteChannel` | | **注册表** | [registries](api/registries.md) | `registries` — 自定义方块/物品/音效 | | **数学** | [math](api/math.md) | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | | **命令** | [commands](api/commands.md) | `/box3script` CLI 命令 | diff --git a/Box3JS-NeoForge-1.21.1/docs/README_en.md b/Box3JS-NeoForge-1.21.1/docs/README_en.md index 921875a..b16526e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/README_en.md @@ -36,6 +36,7 @@ Complete API docs organized by functional category. One document per global obje | Category | Doc | Globals | |----------|-----|---------| +| **Server Overview** | [server](api/server_en.md) | Server runtime boundary, events, players/entities, blocks, data, cross-side communication | | **World** | [world](api/world_en.md) | `world` — events, particles, fireworks, sound, scoreboards | | **Entity** | [entity](api/entity_en.md) | `entity` — properties, AI, equipment, effects | | **Player** | [player](api/player_en.md) | `player` — inventory, messages, flight, teleport | @@ -43,7 +44,7 @@ Complete API docs organized by functional category. One document per global obje | **Storage** | [storage](api/storage_en.md) | `storage` — JSON persistence | | **Database** | [database](api/database_en.md) | `db` — SQLite database | | **HTTP** | [http](api/http_en.md) | `http` — HTTP requests | -| **Client** | [client](api/client_en.md) | `audio` `client` `input` `ui` `chat` `remoteChannel` | +| **Client** | [client](api/client_en.md) | `audio` `client` `input` `ui` `chat` `gui` `remoteChannel` | | **Registries** | [registries](api/registries_en.md) | `registries` — custom blocks/items/sounds | | **Math** | [math](api/math_en.md) | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | | **Commands** | [commands](api/commands_en.md) | `/box3script` CLI commands | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 5f6bde3..54f0f0e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -1,6 +1,6 @@ # Box3JS API 参考 -Box3JS 是一个 Minecraft 模组,允许用 JavaScript/TypeScript 编写服务端脚本。所有脚本运行在 `config/box3/script/<项目名>` 下。 +Box3JS 是一个 Minecraft 模组,允许用 JavaScript/TypeScript 编写服务端脚本,并可选下发客户端脚本来实现本地 UI、输入和音效。每个脚本项目位于 `config/box3/script/<项目名>` 下。 ## 5 分钟快速开始 @@ -16,7 +16,7 @@ npm install && npm run build /box3script start mygame ``` -打开 `src/app.ts`,写入: +打开 `src/server/app.ts`,写入: ```js world.onChat((entity, message) => { @@ -30,19 +30,19 @@ world.onChat((entity, message) => { console.log("脚本已加载"); ``` -每次修改后重新 `npm run build`,然后用 `/box3script reload mygame` 热重载。 +每次修改后重新 `npm run build`,然后用 `/box3script reload mygame` 热重载。客户端逻辑放在 `src/client/app.ts`,构建后生成 `dist/client.js`,玩家安装 Box3JS 客户端 Mod 后会在加入服务器时自动接收。 > **新手上路**: [快速开始指南](../guide/getting-started.md) | **原理深入**: [运行原理](../guide/architecture.md) | **JS vs Java**: [技术选型对比](../guide/js-vs-java.md) ## API 领域分类 -Box3JS API 按运行环境分为三大类: +Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服务端与客户端类型声明已拆分,`tsconfig.server.json` 不会包含客户端全局对象,`tsconfig.client.json` 也不会包含服务端 `world` / `voxels`。 | 领域 | 运行环境 | 全局对象 | 说明 | |------|---------|---------|------| | **世界与实体** (服务端) | 服务端 | `world` `voxels` | 世界控制、方块操作、事件回调 | -| **玩家与数据** (服务端) | 服务端 | `player` `entity` `storage` `db` `http` | 通过 `entity.player` 访问 | -| **客户端交互** (客户端) | 客户端 | `audio` `client` `input` `ui` `chat` | 需 Box3JS 客户端 Mod | +| **玩家与数据** (服务端) | 服务端 | `entity` `player` `storage` `db` `http` | `entity`/`player` 来自回调或查询 | +| **客户端交互** (客户端) | 客户端 | `audio` `client` `input` `ui` `chat` `gui` | 需 Box3JS 客户端 Mod | | **跨端通信** | 双端 | `remoteChannel` | 服务端↔客户端事件通信 | | **注册与编译** | 编译时 | `registries` | 仅在 `/box3script compile` JAR 模式可用 | | **数学与工具** | 双端 | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | 通过 `new` 构造 | @@ -52,6 +52,14 @@ Box3JS API 按运行环境分为三大类: > **客户端 API** 仅在安装了 Box3JS 客户端 Mod 时可用,用于 UI、输入、音效。 > **注册 API** 仅在编译 JAR 模式下可用(`/box3script start` 解释模式中 `registries` 为 `undefined`)。 +## 按运行环境阅读 + +| 入口 | 适合场景 | 包含文档 | +|------|----------|----------| +| [服务端 API 总览](server.md) | 写玩法逻辑、事件、方块、实体、玩家、数据、服务端到客户端事件 | `world`、`entity`、`player`、`voxels`、`storage`、`db`、`http`、`registries` | +| [客户端 API 总览](client.md) | 写本地 UI、输入、音效、聊天辅助、本地数据、客户端到服务端事件 | `client`、`audio`、`input`、`ui`、`chat`、`gui`、`storage`、`db`、`http` | +| [共享工具](math.md) | 写双端都可用的数学、颜色、空间计算代码 | `GameVector3`、`GameBounds3`、`GameRGBColor`、`GameRGBAColor`、`GameQuaternion` | + ## 功能速查 — 我想... 按你想做的事情查找对应 API,而非按全局对象记。 @@ -246,26 +254,27 @@ Box3JS API 按运行环境分为三大类: | 对象 | 类型 | 说明 | |------|------|------| -| `world` | | 世界控制,见 [world.md](world.md) | -| `entity` | | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | -| `player` | | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | -| `voxels` | | 方块操作,见 [voxels.md](voxels.md) | -| `storage` | | 数据持久化,见 [storage.md](storage.md) | -| `db` | | SQLite 数据库,见 [database.md](database.md) | -| `http` | 客户端 | HTTP 请求,见 [http.md](http.md) | +| `world` | 服务端 | 世界控制,见 [world.md](world.md) | +| `voxels` | 服务端 | 方块操作,见 [voxels.md](voxels.md) | +| `entity` | 服务端值 | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | +| `player` | 服务端值 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | +| `storage` | 双端 | 数据持久化,见 [storage.md](storage.md) | +| `db` | 双端 | SQLite 数据库,见 [database.md](database.md) | +| `http` | 双端 | HTTP 请求,见 [http.md](http.md) | | `audio` | 客户端 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | | `client` | 客户端 | 客户端生命周期,见 [client.md](client.md) | | `input` | 客户端 | 客户端键盘输入,见 [client.md](client.md) | | `ui` | 客户端 | 客户端屏幕 UI,见 [client.md](client.md) | | `chat` | 客户端 | 客户端聊天收发,见 [client.md](client.md) | -| `remoteChannel` | 客户端 | 服务端↔客户端事件通信,见 [client.md](client.md) | +| `gui` | 客户端 | 自定义容器 GUI,见 [client.md](client.md) | +| `remoteChannel` | 双端 | 服务端↔客户端事件通信,见 [server.md](server.md) / [client.md](client.md) | | `registries` | 服务端 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | -| `console` | | 控制台日志输出(`log`/`warn`/`error`/`debug`) | -| `GameVector3` | | 三维向量,见 [math.md](math.md) | -| `GameBounds3` | | 包围盒,见 [math.md](math.md) | -| `GameRGBColor` | | RGB 颜色,见 [math.md](math.md) | -| `GameRGBAColor` | | RGBA 颜色,见 [math.md](math.md) | -| `GameQuaternion` | | 四元数,见 [math.md](math.md) | +| `console` | 双端 | 控制台日志输出(`log`/`warn`/`error`/`debug`) | +| `GameVector3` | 双端 | 三维向量,见 [math.md](math.md) | +| `GameBounds3` | 双端 | 包围盒,见 [math.md](math.md) | +| `GameRGBColor` | 双端 | RGB 颜色,见 [math.md](math.md) | +| `GameRGBAColor` | 双端 | RGBA 颜色,见 [math.md](math.md) | +| `GameQuaternion` | 双端 | 四元数,见 [math.md](math.md) | ## API 标注说明 @@ -274,10 +283,23 @@ Box3JS API 按运行环境分为三大类: | ✅ **Box3 API** | 源自 Box3 平台,命名和语义与 Box3 保持一致 | | ⬆ **MC 扩展** | 非 Box3 原有,利用 Minecraft 特性新增的 API | +## 文档风格约定 + +每个 API 文档统一使用以下结构,后续新增 API 时也按这个格式维护: + +1. 顶部说明运行环境:服务端、客户端或双端共享。 +2. 先列全局对象和核心概念,再列方法详情。 +3. 每个方法使用 `对象.方法(参数)` 标题。 +4. 参数使用表格说明名称、类型、默认值和语义。 +5. 示例优先使用 TypeScript/JavaScript 片段,并标明服务端或客户端上下文。 +6. 跨端能力必须说明数据方向:服务端 → 客户端,或客户端 → 服务端。 +7. 与 DTS 不一致时,以 `types/server/index.d.ts` 和 `types/client/index.d.ts` 为准,并同步修正文档。 + ## 详细文档索引 | 文档 | 内容 | |------|------| +| [server.md](server.md) | 服务端 API 总览:运行边界、全局对象、事件、玩家/实体、方块、数据、跨端通信 | | [world.md](world.md) | 世界状态、事件回调、记分板、BossBar、队伍、边界、粒子、烟花、闪电、音效 | | [entity.md](entity.md) | 实体属性、AI、装备、药水效果、寻路、标签、碰撞 | | [player.md](player.md) | 背包、消息、飞行、游戏模式、传送、命令、经验值 | @@ -285,7 +307,7 @@ Box3JS API 按运行环境分为三大类: | [storage.md](storage.md) | 数据持久化存储 | | [database.md](database.md) | SQLite 数据库 | | [http.md](http.md) | HTTP 网络请求 | -| [client.md](client.md) | 客户端脚本:生命周期、键盘输入、屏幕 UI、聊天、remoteChannel、客户端本地存储 | +| [client.md](client.md) | 客户端 API:生命周期、键盘输入、屏幕 UI、聊天、GUI、remoteChannel、客户端本地存储 | | [registries.md](registries.md) | 自定义方块/物品/音效(blocks.json、items.json、sounds.json、creativeTabs.json) | | [math.md](math.md) | GameVector3、GameBounds3、GameRGBColor、GameRGBAColor、GameQuaternion | | [commands.md](commands.md) | `/box3script` 命令参考 | @@ -303,8 +325,21 @@ config/box3/script/mygame/ ├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ ├── types/ │ ├── shared.d.ts ← 服务端&客户端共享类型 -│ ├── server.d.ts ← 服务端专属类型 -│ └── client.d.ts ← 客户端专属类型 +│ ├── server/ +│ │ ├── index.d.ts ← 服务端类型入口 +│ │ ├── server.d.ts +│ │ ├── entity.d.ts +│ │ ├── player.d.ts +│ │ ├── world.d.ts +│ │ └── voxels.d.ts +│ └── client/ +│ ├── index.d.ts ← 客户端类型入口 +│ ├── client.d.ts +│ ├── audio.d.ts +│ ├── input.d.ts +│ ├── ui.d.ts +│ ├── chat.d.ts +│ └── gui.d.ts ├── src/ │ ├── server/ │ │ ├── app.ts ← 服务端入口 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md index 3bde14d..fb8de02 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md @@ -1,6 +1,6 @@ # Box3JS API Reference -Box3JS is a Minecraft mod that lets you write server-side scripts in JavaScript/TypeScript. All scripts run under `config/box3/script/`. +Box3JS is a Minecraft mod that lets you write server-side scripts in JavaScript/TypeScript, with optional client scripts for local UI, input, and audio. Each script project lives under `config/box3/script/`. ## 5-Minute Quick Start @@ -16,7 +16,7 @@ npm install && npm run build /box3script start mygame ``` -Open `src/app.ts` and write: +Open `src/server/app.ts` and write: ```js world.onChat((entity, message) => { @@ -30,19 +30,19 @@ world.onChat((entity, message) => { console.log("Script loaded"); ``` -After each edit, re-run `npm run build`, then use `/box3script reload mygame` to hot-reload. +After each edit, re-run `npm run build`, then use `/box3script reload mygame` to hot-reload. Client logic goes in `src/client/app.ts`; after build it becomes `dist/client.js` and is sent automatically to players who have the Box3JS client mod installed. > **New here?** [Quick Start Guide](../guide/getting-started_en.md) | **How it works:** [Architecture](../guide/architecture_en.md) | **JS vs Java:** [Comparison](../guide/js-vs-java_en.md) ## API Domain Map -Box3JS APIs are divided by runtime environment: +Box3JS APIs are split into server-side, client-side, and shared runtimes. The type declarations are separated: `tsconfig.server.json` does not include client globals, and `tsconfig.client.json` does not include server globals such as `world` / `voxels`. | Domain | Runtime | Globals | Description | |--------|---------|---------|-------------| | **World & Entities** (server) | Server | `world` `voxels` | World control, blocks, event callbacks | -| **Players & Data** (server) | Server | `player` `entity` `storage` `db` `http` | Accessed via `entity.player` | -| **Client Interaction** (client) | Client | `audio` `client` `input` `ui` `chat` | Requires Box3JS client mod | +| **Players & Data** (server) | Server | `entity` `player` `storage` `db` `http` | `entity`/`player` come from callbacks or queries | +| **Client Interaction** (client) | Client | `audio` `client` `input` `ui` `chat` `gui` | Requires Box3JS client mod | | **Cross-Side** | Both | `remoteChannel` | Server↔Client event communication | | **Registries** | Compile-time | `registries` | Only in `/box3script compile` JAR mode | | **Math & Utilities** | Both | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | Constructed with `new` | @@ -52,6 +52,14 @@ Box3JS APIs are divided by runtime environment: > **Client APIs** are only available with the Box3JS client mod installed, for UI, input, and audio. > **Registry APIs** are only available in compiled JAR mode (`registries` is `undefined` in interpreted mode). +## Read By Runtime + +| Entry | Use it for | Includes | +|-------|------------|----------| +| [Server API Overview](server_en.md) | Gameplay logic, events, blocks, entities, players, data, server-to-client events | `world`, `entity`, `player`, `voxels`, `storage`, `db`, `http`, `registries` | +| [Client API Overview](client_en.md) | Local UI, input, audio, chat helpers, local data, client-to-server events | `client`, `audio`, `input`, `ui`, `chat`, `gui`, `storage`, `db`, `http` | +| [Shared Utilities](math_en.md) | Math, color, and spatial code usable on both sides | `GameVector3`, `GameBounds3`, `GameRGBColor`, `GameRGBAColor`, `GameQuaternion` | + ## Find by Task — I want to... Find APIs by what you want to do, not by which global object they live on. @@ -246,26 +254,27 @@ Find APIs by what you want to do, not by which global object they live on. | Object | Type | Description | |--------|------|-------------| -| `world` | | World control, see [world_en.md](world_en.md) | -| `entity` | | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity_en.md](entity_en.md) | -| `player` | | Player wrapper (via `entity.player`), see [player_en.md](player_en.md) | -| `voxels` | | Block operations, see [voxels_en.md](voxels_en.md) | -| `storage` | | Data persistence, see [storage_en.md](storage_en.md) | -| `db` | | SQLite database, see [database_en.md](database_en.md) | -| `http` | Client | HTTP requests, see [http_en.md](http_en.md) | +| `world` | Server | World control, see [world_en.md](world_en.md) | +| `voxels` | Server | Block operations, see [voxels_en.md](voxels_en.md) | +| `entity` | Server value | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity_en.md](entity_en.md) | +| `player` | Server value | Player wrapper (via `entity.player`), see [player_en.md](player_en.md) | +| `storage` | Both | Data persistence, see [storage_en.md](storage_en.md) | +| `db` | Both | SQLite database, see [database_en.md](database_en.md) | +| `http` | Both | HTTP requests, see [http_en.md](http_en.md) | | `audio` | Client | Client sound, music, volume control, see [client_en.md](client_en.md) | | `client` | Client | Client lifecycle, see [client_en.md](client_en.md) | | `input` | Client | Client keyboard input, see [client_en.md](client_en.md) | | `ui` | Client | Client screen UI, see [client_en.md](client_en.md) | | `chat` | Client | Client chat send/receive, see [client_en.md](client_en.md) | -| `remoteChannel` | Client | Server↔client event channel, see [client_en.md](client_en.md) | +| `gui` | Client | Custom container GUI, see [client_en.md](client_en.md) | +| `remoteChannel` | Both | Server↔client event channel, see [server_en.md](server_en.md) / [client_en.md](client_en.md) | | `registries` | Server | Custom blocks, items & sounds (compiled mode), see [registries_en.md](registries_en.md) | -| `console` | | Console logging (`log`/`warn`/`error`/`debug`) | -| `GameVector3` | | 3D vector, see [math_en.md](math_en.md) | -| `GameBounds3` | | Bounding box, see [math_en.md](math_en.md) | -| `GameRGBColor` | | RGB color, see [math_en.md](math_en.md) | -| `GameRGBAColor` | | RGBA color, see [math_en.md](math_en.md) | -| `GameQuaternion` | | Quaternion, see [math_en.md](math_en.md) | +| `console` | Both | Console logging (`log`/`warn`/`error`/`debug`) | +| `GameVector3` | Both | 3D vector, see [math_en.md](math_en.md) | +| `GameBounds3` | Both | Bounding box, see [math_en.md](math_en.md) | +| `GameRGBColor` | Both | RGB color, see [math_en.md](math_en.md) | +| `GameRGBAColor` | Both | RGBA color, see [math_en.md](math_en.md) | +| `GameQuaternion` | Both | Quaternion, see [math_en.md](math_en.md) | ## API Legend @@ -274,10 +283,23 @@ Find APIs by what you want to do, not by which global object they live on. | ✅ **Box3 API** | Originates from the Box3 platform; naming and semantics match Box3 | | ⬆ **MC Extension** | Not in original Box3; added using Minecraft-specific features | +## Documentation Style + +Each API document should follow this structure. Use the same style when adding future APIs: + +1. State the runtime at the top: server, client, or shared. +2. List globals and core concepts before method details. +3. Use `object.method(parameters)` for method headings. +4. Document parameters in tables with name, type, default, and meaning. +5. Prefer TypeScript/JavaScript examples and identify server or client context when needed. +6. For cross-side APIs, always state the direction: server → client, or client → server. +7. If docs and types disagree, treat `types/server/index.d.ts` and `types/client/index.d.ts` as the source of truth, then update the docs. + ## Detailed Document Index | Document | Content | |----------|---------| +| [server_en.md](server_en.md) | Server API overview: runtime boundary, globals, events, players/entities, blocks, data, cross-side communication | | [world_en.md](world_en.md) | World state, events, scoreboard, bossbar, teams, border, particles, fireworks, lightning, sounds | | [entity_en.md](entity_en.md) | Entity properties, AI, equipment, potion effects, pathfinding, tags, collisions | | [player_en.md](player_en.md) | Inventory, messaging, flight, game mode, teleport, commands, XP | @@ -285,7 +307,7 @@ Find APIs by what you want to do, not by which global object they live on. | [storage_en.md](storage_en.md) | Persistent data storage | | [database_en.md](database_en.md) | SQLite database API | | [http_en.md](http_en.md) | HTTP request API | -| [client_en.md](client_en.md) | Client scripts: lifecycle, keyboard, screen UI, chat, remoteChannel, client-side storage | +| [client_en.md](client_en.md) | Client API: lifecycle, keyboard, screen UI, chat, GUI, remoteChannel, client-side storage | | [registries_en.md](registries_en.md) | Custom blocks, items & sounds (blocks.json, items.json, sounds.json, creativeTabs.json) | | [math_en.md](math_en.md) | GameVector3, GameBounds3, GameRGBColor, GameRGBAColor, GameQuaternion | | [commands_en.md](commands_en.md) | `/box3script` command reference | @@ -304,17 +326,20 @@ config/box3/script/mygame/ ├── types/ │ ├── shared.d.ts ← Shared types (server & client) │ ├── server/ -│ │ ├── server.d.ts ← Server entry point +│ │ ├── index.d.ts ← Server type entry point +│ │ ├── server.d.ts │ │ ├── entity.d.ts │ │ ├── player.d.ts │ │ ├── world.d.ts │ │ └── voxels.d.ts │ └── client/ -│ ├── client.d.ts ← Client entry point +│ ├── index.d.ts ← Client type entry point +│ ├── client.d.ts │ ├── audio.d.ts │ ├── input.d.ts │ ├── ui.d.ts -│ └── chat.d.ts +│ ├── chat.d.ts +│ └── gui.d.ts ├── src/ │ ├── server/ │ │ ├── app.ts ← Server entry point diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md index 65ca4ec..c0ce264 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md @@ -1,6 +1,8 @@ # client — 客户端 API -客户端脚本运行在玩家本地 Minecraft 客户端上,通过以下五个全局对象访问: +客户端脚本运行在玩家本地 Minecraft 客户端上,入口文件是 `src/client/app.ts`,构建产物是 `dist/client.js`。客户端 API 只负责本地 UI、输入、音频、聊天辅助、本地存储、本地 HTTP/SQLite 以及接收/发送跨端事件。 + +客户端脚本通过以下全局对象访问 API: | 对象 | 类型 | 用途 | |------|------|------| @@ -16,7 +18,13 @@ | `remoteChannel` | `RemoteChannel` | 客户端 ↔ 服务端事件通信 | > **前置条件:** 客户端必须安装 Box3JS mod,服务端必须启用该项目的客户端脚本并通过网络自动下发。 -> 客户端脚本放在 `src/client/` 目录下,服务端脚本放在 `src/server/` 目录下。 +> 客户端脚本放在 `src/client/` 目录下,服务端脚本放在 `src/server/` 目录下。客户端类型入口是 `types/client/index.d.ts`,不会包含服务端 `world` / `voxels` API。 + +客户端脚本不能直接修改服务端世界。需要改变方块、玩家、实体或计分板时,应发送事件给服务端: + +```ts +remoteChannel.sendServerEvent({ type: "requestTeleport" }); +``` ## audio — 音频播放 @@ -383,12 +391,12 @@ var item = ctrl.getItem(0); console.log(item.id, item.count); // minecraft:diamond, 1 // 监听槽位点击 -ctrl.onSlotClick((slot) => { +var clickToken = ctrl.onSlotClick((slot) => { console.log("Clicked slot:", slot); }); // 监听关闭 -ctrl.onClose(() => { +var closeToken = ctrl.onClose(() => { console.log("GUI closed"); }); @@ -402,8 +410,8 @@ ctrl.close(); |------|------| | `setItem(slot, itemId, count?)` | 设置指定槽位的物品 | | `getItem(slot)` | 获取指定槽位的物品,返回 `{ id, count }` | -| `onSlotClick(callback)` | 注册槽位点击回调,`callback(slot: number)` | -| `onClose(callback)` | 注册关闭回调,`callback()` | +| `onSlotClick(callback)` | 注册槽位点击回调,返回 `GameEventHandlerToken`,`callback(slot: number)` | +| `onClose(callback)` | 注册关闭回调,返回 `GameEventHandlerToken`,`callback()` | | `close()` | 关闭 GUI | ## remoteChannel — 客户端 ↔ 服务端通信 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md index 4451869..10b91d3 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md @@ -1,6 +1,8 @@ # client — Client-side API -Client scripts run locally on the player's Minecraft client and are accessed through five globals: +Client scripts run locally on the player's Minecraft client. The entry file is `src/client/app.ts`, and the compiled output is `dist/client.js`. Client APIs handle local UI, input, audio, chat helpers, local storage, local HTTP/SQLite, and cross-side events. + +Client scripts access APIs through these globals: | Object | Type | Purpose | |--------|------|---------| @@ -16,7 +18,13 @@ Client scripts run locally on the player's Minecraft client and are accessed thr | `remoteChannel` | `RemoteChannel` | Client ↔ Server event communication | > **Prerequisite:** The client must have the Box3JS mod installed. The server must enable the project's client script, which is automatically sent to connecting players. -> Client scripts go in `src/client/`, server scripts in `src/server/`. +> Client scripts go in `src/client/`, server scripts in `src/server/`. The client type entry is `types/client/index.d.ts`; it does not include server APIs such as `world` / `voxels`. + +Client scripts cannot directly modify the server world. To change blocks, players, entities, or scoreboards, send an event to the server: + +```ts +remoteChannel.sendServerEvent({ type: "requestTeleport" }); +``` ## audio — Sound Playback @@ -383,12 +391,12 @@ var item = ctrl.getItem(0); console.log(item.id, item.count); // minecraft:diamond, 1 // Slot click listener -ctrl.onSlotClick((slot) => { +var clickToken = ctrl.onSlotClick((slot) => { console.log("Clicked slot:", slot); }); // Close listener -ctrl.onClose(() => { +var closeToken = ctrl.onClose(() => { console.log("GUI closed"); }); @@ -402,8 +410,8 @@ ctrl.close(); |--------|-------------| | `setItem(slot, itemId, count?)` | Sets the item in the given slot | | `getItem(slot)` | Gets the item in the given slot, returns `{ id, count }` | -| `onSlotClick(callback)` | Registers a slot click callback, `callback(slot: number)` | -| `onClose(callback)` | Registers a close callback, `callback()` | +| `onSlotClick(callback)` | Registers a slot click callback and returns `GameEventHandlerToken`, `callback(slot: number)` | +| `onClose(callback)` | Registers a close callback and returns `GameEventHandlerToken`, `callback()` | | `close()` | Closes the GUI | ## remoteChannel — Client ↔ Server Communication diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database.md b/Box3JS-NeoForge-1.21.1/docs/api/database.md index 9ee6a04..2ff883e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/database.md @@ -1,6 +1,8 @@ # Database API -Box3JS 通过全局 `db` 对象提供 SQLite 数据库能力。每个脚本项目自动拥有独立的数据库文件(`config/box3/data/.db`),无需手动管理连接。 +Box3JS 通过全局 `db` 对象提供 SQLite 数据库能力,无需手动管理连接。 + +> **运行环境:** 服务端和客户端都可用。服务端数据库位于 `config/box3/data/.db`;客户端数据库位于本地游戏目录的 `box3/client-db/.db`。两端数据库互不共享,需要同步数据时请使用 `remoteChannel`。 ## 依赖与降级行为 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md b/Box3JS-NeoForge-1.21.1/docs/api/database_en.md index a3e8c33..bde65ce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/database_en.md @@ -1,6 +1,8 @@ # Database API -Box3JS exposes SQLite capabilities through the global `db` object. Each script project gets its own database file at `config/box3/data/.db`, and connections are managed automatically. +Box3JS exposes SQLite capabilities through the global `db` object. Connections are managed automatically. + +> **Runtime:** Available on both server and client. Server databases live at `config/box3/data/.db`; client databases live under the local game directory at `box3/client-db/.db`. The two sides do not share database files; use `remoteChannel` when data must be synchronized. ## Dependency & Graceful Fallback diff --git a/Box3JS-NeoForge-1.21.1/docs/api/http.md b/Box3JS-NeoForge-1.21.1/docs/api/http.md index 2518ba9..91c8eed 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/http.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/http.md @@ -2,7 +2,7 @@ Box3JS 通过全局 `http` 对象提供 HTTP 请求能力,支持全部 HTTP 方法、超时、自定义请求头、自动解析、二进制上传,以及同步/异步两种调用方式。 -> **同步请求**会阻塞服务器 tick,避免在高频回调中执行长时间请求。**异步请求**(`async: true`)不阻塞 tick,通过回调接收结果。 +> **运行环境:** 服务端和客户端都可用。服务端同步请求会阻塞服务器 tick,避免在高频回调中执行长时间请求。客户端同步请求会阻塞客户端渲染/逻辑线程。**异步请求**(`async: true`)通过回调接收结果。 ## `http.fetch(url, options?)` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/http_en.md b/Box3JS-NeoForge-1.21.1/docs/api/http_en.md index 6c230ef..2b419c4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/http_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/http_en.md @@ -2,7 +2,7 @@ Box3JS provides HTTP request capabilities via the global `http` object, supporting all HTTP methods, timeout, custom headers, auto-parsing, binary uploads, and both synchronous and asynchronous calling modes. -> **Synchronous requests** block the server tick — avoid long-running requests in high-frequency callbacks. **Async requests** (`async: true`) are non-blocking and deliver results via callbacks. +> **Runtime:** Available on both server and client. Server-side synchronous requests block the server tick, so avoid long-running requests in high-frequency callbacks. Client-side synchronous requests block the client render/logic thread. **Async requests** (`async: true`) deliver results via callbacks. ## `http.fetch(url, options?)` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server.md b/Box3JS-NeoForge-1.21.1/docs/api/server.md new file mode 100644 index 0000000..658dc09 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/server.md @@ -0,0 +1,230 @@ +# server — 服务端 API 总览 + +服务端脚本运行在 Minecraft 服务器线程上,入口文件是 `src/server/app.ts`,构建产物是 `dist/server.js`。服务端 API 负责世界状态、实体和玩家、方块读写、事件回调、持久化数据、网络请求以及服务端到客户端的事件下发。 + +> 客户端 UI、键盘输入、本地音效和本地 GUI 不在服务端 API 中。相关能力见 [client.md](client.md)。 + +## 服务端全局对象 + +| 对象 | 类型 | 作用域 | 说明 | +|------|------|--------|------| +| `world` | `GameWorld` | 服务端 | 世界状态、事件系统、实体生成、计分板、BossBar、队伍、边界、粒子、音效 | +| `voxels` | `GameVoxels` | 服务端 | 方块读写、区域填充、方块 ID/名称转换、刷怪笼控制 | +| `storage` | `GameStorage` | 服务端/客户端 | JSON 持久化存储。服务端按项目写入 `config/box3/storage//` | +| `db` | `GameDatabase` | 服务端/客户端 | SQLite 查询。服务端数据库在 `config/box3/data/.db` | +| `http` | `GameHttpAPI` | 服务端/客户端 | HTTP 请求,支持同步和回调式异步 | +| `remoteChannel` | `RemoteChannel` | 双端 | 服务端向客户端发事件,或接收客户端发来的事件 | +| `registries` | `GameRegistries \| undefined` | 服务端编译模式 | `/box3script compile` 产物中的自定义方块、物品、音效查询 | +| `console` | `GameConsole` | 双端 | `log` / `debug` / `warn` / `error` 日志 | + +实体和玩家不是独立全局对象。它们通常来自事件回调、查询或生成结果: + +```ts +world.onPlayerJoin((entity) => { + const player = entity.player; + player.directMessage("Welcome, " + player.name); +}); +``` + +## 入口和类型 + +服务端代码放在 `src/server/`,只会读取服务端类型入口: + +```text +src/server/app.ts +types/server/index.d.ts +``` + +`tsconfig.server.json` 不包含 `types/client/index.d.ts`,所以服务端代码中直接使用 `client`、`audio`、`input`、`ui`、`chat`、`gui` 会被 TypeScript 报错。需要跨端能力时,通过 `remoteChannel` 发送事件给客户端脚本。 + +```ts +remoteChannel.sendClientEvent(entity, { + type: "showToast", + text: "任务完成", +}); +``` + +## 世界与事件 + +`world` 是服务端脚本的主入口。常用能力: + +| 能力 | API | +|------|-----| +| 当前项目名 | `world.projectName()` | +| 当前 tick | `world.currentTick()` | +| 服务端标识/MOTD | `world.serverId` | +| 广播消息 | `world.say(text)` | +| 每 tick 回调 | `world.onTick(handler)` | +| 玩家加入/离开 | `world.onPlayerJoin(handler)` / `world.onPlayerLeave(handler)` | +| 聊天处理 | `world.onChat(handler)` | +| 实体交互 | `world.onInteract(handler)` | +| 方块交互 | `world.onBlockActivate(handler)` | +| 方块破坏/放置 | `world.onVoxelDestroy(handler)` / `world.onBlockPlace(handler)` | +| 定时器 | `world.setTimeout(fn, ticks)` / `world.setInterval(fn, ticks)` | + +```ts +world.onChat((entity, message) => { + if (message === "!tick") { + entity.player.directMessage("tick = " + world.currentTick()); + return false; + } + return true; +}); +``` + +事件注册会返回 `GameEventHandlerToken`。长期运行的脚本应保存 token,在不需要时取消: + +```ts +const token = world.onTick(() => { + // ... +}); + +token.cancel(); +``` + +## 玩家与实体 + +`GameEntity` 表示玩家或生物,`GamePlayerEntity` 是玩家实体收窄后的类型。先判断 `entity.isPlayer()`,再访问 `entity.player` 更安全。 + +```ts +const target = world.querySelector("#some-uuid"); +if (target && target.isPlayer()) { + target.player.actionBar("你被选中了"); +} +``` + +常用实体能力: + +| 能力 | API | +|------|-----| +| 位置/速度 | `entity.position` / `entity.velocity` | +| 生命值 | `entity.hp` / `entity.maxHp` | +| 标签 | `entity.addTag()` / `entity.removeTag()` / `entity.hasTag()` | +| 装备 | `entity.setEquipment(slot, itemId)` | +| 药水效果 | `entity.addEffect()` / `entity.clearEffects()` | +| AI 与导航 | `entity.setAI()` / `entity.navigateTo()` | + +常用玩家能力: + +| 能力 | API | +|------|-----| +| 私聊/ActionBar/标题 | `player.directMessage()` / `player.actionBar()` / `player.title()` | +| 传送 | `player.teleport(pos)` | +| 背包物品 | `player.giveItem()` / `player.clearInventory()` / `player.getHeldItem()` | +| 游戏模式 | `player.gameMode` | +| 飞行 | `player.canFly` / `player.flying` | +| 经验/饥饿值 | `player.xp` / `player.food` / `player.saturation` | +| 命令 | `player.runCommand(cmd)` | + +## 方块与体素 + +`voxels` 负责方块读写。字符串优先使用命名空间 ID,例如 `"minecraft:stone"`;简写 `"stone"` 也会尝试解析。 + +```ts +const pos = new GameVector3(0, 80, 0); +voxels.setVoxel(pos, "minecraft:diamond_block"); + +const name = voxels.getVoxelName(pos); +world.say("block = " + name); +``` + +区域操作可能一次修改大量方块,应控制范围并优先在服务端空闲时执行: + +```ts +voxels.fillVoxel(0, 70, 0, 10, 75, 10, "minecraft:glass"); +``` + +## 服务端数据 + +`storage` 适合 JSON 型小数据、配置、排行榜缓存;`db` 适合复杂查询和表结构。 + +```ts +const scores = storage.getDataStorage("scores"); +scores.increment("alice", 1); + +const rows = db.sql<{ name: string; score: number }>( + "SELECT name, score FROM scores ORDER BY score DESC LIMIT ?", + 10, +); +``` + +服务端还额外提供共享存储: + +```ts +const globalConfig = storage.getGroupStorage("config"); +globalConfig.set("season", "spring"); +``` + +## 跨端通信 + +服务端使用 `remoteChannel` 与客户端脚本通信。发送前建议检查玩家是否安装了 Box3JS 客户端: + +```ts +world.onPlayerJoin((entity) => { + if (!entity.hasBox3JSClient()) { + return; + } + + remoteChannel.sendClientEvent(entity, { + type: "welcome", + text: "欢迎来到服务器", + }); +}); +``` + +接收客户端事件: + +```ts +remoteChannel.onServerEvent<{ type: string; key?: string }>((event) => { + if (event.args.type === "hotkey") { + event.entity.player.actionBar("按键: " + event.args.key); + } +}); +``` + +事件数据必须能 JSON 序列化。不要传 Java 对象、函数或循环引用对象。 + +## 自定义注册表 + +`registries` 只在 `/box3script compile` 打包后的独立 JAR 中可用,解释模式为 `undefined`。 + +```ts +if (registries) { + const block = registries.getBlock("rainbow_cube"); + if (block) { + player.giveItem(block.itemId, 1); + } +} +``` + +自定义内容文件: + +| 文件 | 内容 | +|------|------| +| `registries/blocks.json` | 方块定义 | +| `registries/items.json` | 物品、食物、工具、护甲定义 | +| `registries/sounds.json` | 音效定义 | +| `registries/creativeTabs.json` | 创造模式标签页 | + +## 服务端 API 文档索引 + +| 文档 | 内容 | +|------|------| +| [world.md](world.md) | 世界状态、事件、计分板、BossBar、队伍、边界、粒子、音效 | +| [entity.md](entity.md) | 实体属性、AI、装备、效果、寻路、标签 | +| [player.md](player.md) | 玩家背包、消息、传送、飞行、游戏模式、经验 | +| [voxels.md](voxels.md) | 方块读写、区域填充、刷怪笼 | +| [storage.md](storage.md) | JSON 持久化存储 | +| [database.md](database.md) | SQLite 数据库 | +| [http.md](http.md) | HTTP 请求 | +| [registries.md](registries.md) | 自定义注册表与编译 JAR 模式 | +| [math.md](math.md) | 向量、包围盒、颜色、四元数 | + +## 推荐风格 + +- 服务端文件只引用服务端 API:世界、实体、玩家、方块、数据、网络。 +- 客户端 UI 和输入逻辑放到 `src/client/app.ts`,通过 `remoteChannel` 接收服务端状态。 +- 对所有长期事件监听保存 token,需要关闭玩法或重载模块时调用 `cancel()`。 +- 大范围 `voxels.fillVoxel()`、大量实体生成、同步 HTTP 请求都应控制频率,避免卡住服务器 tick。 +- 共享数据优先明确命名空间,例如 `storage.getDataStorage("arena/scores")` 或 `storage.getGroupStorage("global/season")`。 + diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md new file mode 100644 index 0000000..8345f37 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md @@ -0,0 +1,230 @@ +# server — Server-side API Overview + +Server scripts run on the Minecraft server thread. The entry file is `src/server/app.ts`, and the compiled output is `dist/server.js`. Server APIs handle world state, entities and players, block read/write, event callbacks, persistence, HTTP, and server-to-client event delivery. + +> Client UI, keyboard input, local sounds, and local GUI are not server APIs. See [client_en.md](client_en.md). + +## Server Globals + +| Object | Type | Runtime | Description | +|--------|------|---------|-------------| +| `world` | `GameWorld` | Server | World state, events, entity spawning, scoreboards, bossbars, teams, border, particles, sounds | +| `voxels` | `GameVoxels` | Server | Block read/write, region fill, block ID/name conversion, spawner control | +| `storage` | `GameStorage` | Server/Client | JSON persistence. Server data is stored under `config/box3/storage//` | +| `db` | `GameDatabase` | Server/Client | SQLite queries. Server DB is `config/box3/data/.db` | +| `http` | `GameHttpAPI` | Server/Client | HTTP requests, synchronous or callback-style async | +| `remoteChannel` | `RemoteChannel` | Both | Send client events from the server, or receive client events | +| `registries` | `GameRegistries \| undefined` | Compiled server JAR | Custom block/item/sound lookup in `/box3script compile` outputs | +| `console` | `GameConsole` | Both | `log` / `debug` / `warn` / `error` logging | + +Entities and players are not standalone globals. They usually come from event callbacks, queries, or spawn results: + +```ts +world.onPlayerJoin((entity) => { + const player = entity.player; + player.directMessage("Welcome, " + player.name); +}); +``` + +## Entry Point And Types + +Server code lives under `src/server/` and uses only the server type entry: + +```text +src/server/app.ts +types/server/index.d.ts +``` + +`tsconfig.server.json` does not include `types/client/index.d.ts`, so direct use of `client`, `audio`, `input`, `ui`, `chat`, or `gui` in server code is a TypeScript error. For cross-side behavior, send events to the client script through `remoteChannel`. + +```ts +remoteChannel.sendClientEvent(entity, { + type: "showToast", + text: "Quest complete", +}); +``` + +## World And Events + +`world` is the main server scripting entry point. + +| Capability | API | +|------------|-----| +| Current project name | `world.projectName()` | +| Current tick | `world.currentTick()` | +| Server identifier/MOTD | `world.serverId` | +| Broadcast chat | `world.say(text)` | +| Every-tick callback | `world.onTick(handler)` | +| Player join/leave | `world.onPlayerJoin(handler)` / `world.onPlayerLeave(handler)` | +| Chat handling | `world.onChat(handler)` | +| Entity interaction | `world.onInteract(handler)` | +| Block interaction | `world.onBlockActivate(handler)` | +| Block break/place | `world.onVoxelDestroy(handler)` / `world.onBlockPlace(handler)` | +| Timers | `world.setTimeout(fn, ticks)` / `world.setInterval(fn, ticks)` | + +```ts +world.onChat((entity, message) => { + if (message === "!tick") { + entity.player.directMessage("tick = " + world.currentTick()); + return false; + } + return true; +}); +``` + +Event registration returns a `GameEventHandlerToken`. Long-running systems should keep the token and cancel it when no longer needed: + +```ts +const token = world.onTick(() => { + // ... +}); + +token.cancel(); +``` + +## Players And Entities + +`GameEntity` represents a player or mob. `GamePlayerEntity` is the narrowed player-entity type. Check `entity.isPlayer()` before accessing `entity.player` when the value may be non-player. + +```ts +const target = world.querySelector("#some-uuid"); +if (target && target.isPlayer()) { + target.player.actionBar("You were selected"); +} +``` + +Common entity APIs: + +| Capability | API | +|------------|-----| +| Position/velocity | `entity.position` / `entity.velocity` | +| Health | `entity.hp` / `entity.maxHp` | +| Tags | `entity.addTag()` / `entity.removeTag()` / `entity.hasTag()` | +| Equipment | `entity.setEquipment(slot, itemId)` | +| Effects | `entity.addEffect()` / `entity.clearEffects()` | +| AI and navigation | `entity.setAI()` / `entity.navigateTo()` | + +Common player APIs: + +| Capability | API | +|------------|-----| +| Private message/action bar/title | `player.directMessage()` / `player.actionBar()` / `player.title()` | +| Teleport | `player.teleport(pos)` | +| Inventory | `player.giveItem()` / `player.clearInventory()` / `player.getHeldItem()` | +| Game mode | `player.gameMode` | +| Flight | `player.canFly` / `player.flying` | +| XP/hunger | `player.xp` / `player.food` / `player.saturation` | +| Commands | `player.runCommand(cmd)` | + +## Blocks And Voxels + +`voxels` handles block read/write. Prefer namespaced IDs such as `"minecraft:stone"`; shorthand like `"stone"` is also resolved when possible. + +```ts +const pos = new GameVector3(0, 80, 0); +voxels.setVoxel(pos, "minecraft:diamond_block"); + +const name = voxels.getVoxelName(pos); +world.say("block = " + name); +``` + +Region operations may modify many blocks at once; keep bounds controlled and avoid doing large fills every tick: + +```ts +voxels.fillVoxel(0, 70, 0, 10, 75, 10, "minecraft:glass"); +``` + +## Server Data + +Use `storage` for small JSON-shaped state, config, and cached leaderboards. Use `db` for structured tables and complex queries. + +```ts +const scores = storage.getDataStorage("scores"); +scores.increment("alice", 1); + +const rows = db.sql<{ name: string; score: number }>( + "SELECT name, score FROM scores ORDER BY score DESC LIMIT ?", + 10, +); +``` + +Server storage also has shared cross-project namespaces: + +```ts +const globalConfig = storage.getGroupStorage("config"); +globalConfig.set("season", "spring"); +``` + +## Cross-Side Events + +Use `remoteChannel` to communicate with client scripts. Before sending to a player, check whether their client supports Box3JS: + +```ts +world.onPlayerJoin((entity) => { + if (!entity.hasBox3JSClient()) { + return; + } + + remoteChannel.sendClientEvent(entity, { + type: "welcome", + text: "Welcome to the server", + }); +}); +``` + +Receive client events: + +```ts +remoteChannel.onServerEvent<{ type: string; key?: string }>((event) => { + if (event.args.type === "hotkey") { + event.entity.player.actionBar("key: " + event.args.key); + } +}); +``` + +Event payloads must be JSON-serializable. Do not send Java objects, functions, or cyclic objects. + +## Custom Registries + +`registries` is only available inside standalone JARs produced by `/box3script compile`. It is `undefined` in interpreted mode. + +```ts +if (registries) { + const block = registries.getBlock("rainbow_cube"); + if (block) { + player.giveItem(block.itemId, 1); + } +} +``` + +Custom content files: + +| File | Content | +|------|---------| +| `registries/blocks.json` | Block definitions | +| `registries/items.json` | Items, food, tools, armor | +| `registries/sounds.json` | Sound definitions | +| `registries/creativeTabs.json` | Creative mode tabs | + +## Server API Index + +| Document | Content | +|----------|---------| +| [world_en.md](world_en.md) | World state, events, scoreboard, bossbar, teams, border, particles, sounds | +| [entity_en.md](entity_en.md) | Entity properties, AI, equipment, effects, navigation, tags | +| [player_en.md](player_en.md) | Player inventory, messaging, teleport, flight, game mode, XP | +| [voxels_en.md](voxels_en.md) | Block read/write, region fill, spawners | +| [storage_en.md](storage_en.md) | JSON persistence | +| [database_en.md](database_en.md) | SQLite database | +| [http_en.md](http_en.md) | HTTP requests | +| [registries_en.md](registries_en.md) | Custom registries and compiled JAR mode | +| [math_en.md](math_en.md) | Vectors, bounds, colors, quaternions | + +## Recommended Style + +- Keep server files focused on server APIs: world, entities, players, blocks, data, network. +- Put client UI/input logic in `src/client/app.ts`, fed by server state through `remoteChannel`. +- Keep tokens for long-lived event listeners and call `cancel()` when disabling a game mode or subsystem. +- Rate-limit large `voxels.fillVoxel()` calls, mass entity spawning, and synchronous HTTP requests to avoid blocking server ticks. +- Give storage namespaces explicit names, such as `storage.getDataStorage("arena/scores")` or `storage.getGroupStorage("global/season")`. + diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index fc1e6d2..a69057a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -1,6 +1,8 @@ # storage — 数据存储 API -`storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。数据保存在 `config/box3/storage/<项目名>/` 目录下,每个项目自动拥有独立命名空间。 +`storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。 + +> **运行环境:** 服务端和客户端都可用。服务端数据保存在 `config/box3/storage/<项目名>/`;客户端数据保存在本地游戏目录的 `box3/client-storage/<项目名>/`。每个项目自动拥有独立命名空间。 ## 获取存储实例 @@ -12,6 +14,8 @@ 获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 +> 仅服务端可用。客户端本地存储只提供 `getDataStorage(name)`。 + ```js var store = storage.getDataStorage("leaderboard"); var config = storage.getDataStorage("settings"); @@ -135,12 +139,12 @@ if (!result.isLastPage) { - **同名存储**:同一文件路径多次 `getDataStorage` 返回共享同一份内存数据,避免重复 I/O - **项目隔离**:`getDataStorage("scores")` 在不同项目中访问不同文件(自动添加项目名前缀) -- **跨项目共享**:`getGroupStorage("leaderboard")` 所有项目访问同一个 `__shared__/leaderboard.json` +- **跨项目共享(仅服务端)**:`getGroupStorage("leaderboard")` 所有项目访问同一个 `__shared__/leaderboard.json` ## 完整示例:排行榜 ```js -// 跨项目共享排行榜 — 所有项目读写同一份数据 +// 服务端:跨项目共享排行榜 — 所有项目读写同一份数据 var lb = storage.getGroupStorage("leaderboard"); // 保存成绩 @@ -162,5 +166,3 @@ while (true) { result.nextPage(); } ``` - - diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md index 5cdc596..4e46b1b 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md @@ -1,6 +1,8 @@ # storage — Data Storage API -`storage` provides JSON file persistence with in-memory caching for fast reads/writes. Data is saved under `config/box3/storage//`. Each project automatically gets an independent namespace. +`storage` provides JSON file persistence with in-memory caching for fast reads/writes. + +> **Runtime:** Available on both server and client. Server data is saved under `config/box3/storage//`; client data is saved under the local game directory at `box3/client-storage//`. Each project automatically gets an independent namespace. ## Getting a Storage Instance @@ -12,6 +14,8 @@ Gets or creates a named storage. Same name returns the same instance. Gets a **cross-project shared** storage. All projects access the same data via the same `name` (uses `__shared__/` namespace internally). Useful for global leaderboards, shared config, etc. +> Server-side only. Client local storage only provides `getDataStorage(name)`. + ```js var store = storage.getDataStorage("leaderboard"); var config = storage.getDataStorage("settings"); @@ -135,12 +139,12 @@ All `GameDataStorage` instances share a memory cache (`ConcurrentHashMap`). Data - **Same-name storage**: multiple `getDataStorage` calls for the same file path share a single in-memory copy, avoiding redundant I/O - **Project isolation**: `getDataStorage("scores")` accesses different files in different projects (auto-prefixed with project name) -- **Cross-project sharing**: `getGroupStorage("leaderboard")` accesses the same `__shared__/leaderboard.json` from all projects +- **Cross-project sharing (server-side only)**: `getGroupStorage("leaderboard")` accesses the same `__shared__/leaderboard.json` from all projects ## Complete Example: Leaderboard ```js -// Cross-project shared leaderboard — all projects read/write the same data +// Server: cross-project shared leaderboard — all projects read/write the same data var lb = storage.getGroupStorage("leaderboard"); // Save score @@ -162,5 +166,3 @@ while (true) { result.nextPage(); } ``` - - diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index cc81b54..8eee60e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -4,12 +4,12 @@ ## 世界属性 -### world.projectName +### world.projectName() -只读属性。服务端 MOTD 字符串。为兼容旧代码也可作为方法调用 `world.projectName()`。 +只读方法。返回当前正在执行的脚本项目名。需要服务器标识/MOTD 时使用 `world.serverId`。 ```js -console.log(world.projectName); // "A Minecraft Server" +console.log(world.projectName()); // "mygame" ``` ### world.serverId @@ -21,12 +21,12 @@ world.serverId = "My Cool Server"; console.log(world.serverId); ``` -### world.currentTick +### world.currentTick() -只读属性。服务器自启动以来的总 tick 数。为兼容旧代码也可作为方法调用 `world.currentTick()`。 +只读方法。返回服务器自启动以来的总 tick 数。 ```js -var uptime = world.currentTick; +var uptime = world.currentTick(); world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md index cc010ac..979715d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md @@ -4,12 +4,12 @@ ## World Properties -### world.projectName +### world.projectName() -Read-only property. The server MOTD string. Also callable as `world.projectName()` for backward compatibility. +Read-only method. Returns the name of the currently executing script project. Use `world.serverId` for the server MOTD/identifier. ```js -console.log(world.projectName); // "A Minecraft Server" +console.log(world.projectName()); // "mygame" ``` ### world.serverId @@ -21,12 +21,12 @@ world.serverId = "My Cool Server"; console.log(world.serverId); ``` -### world.currentTick +### world.currentTick() -Read-only property. Total ticks since server startup. Also callable as `world.currentTick()` for backward compatibility. +Read-only method. Returns total ticks since server startup. ```js -var uptime = world.currentTick; +var uptime = world.currentTick(); world.say( "Server has been running for " + Math.floor(uptime / 20 / 60) + " minutes", ); diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md index a29bbe4..00bab8c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md @@ -107,17 +107,20 @@ config/box3/script/mygame/ ├── types/ │ ├── shared.d.ts ← 服务端&客户端共享类型 │ ├── server/ -│ │ ├── server.d.ts ← 服务端 API 类型声明 +│ │ ├── index.d.ts ← 服务端类型入口 +│ │ ├── server.d.ts │ │ ├── entity.d.ts │ │ ├── player.d.ts │ │ ├── world.d.ts │ │ └── voxels.d.ts │ └── client/ -│ ├── client.d.ts ← 客户端 API 类型声明 +│ ├── index.d.ts ← 客户端类型入口 +│ ├── client.d.ts │ ├── audio.d.ts │ ├── input.d.ts │ ├── ui.d.ts -│ └── chat.d.ts +│ ├── chat.d.ts +│ └── gui.d.ts ├── src/ │ ├── server/ │ │ └── app.ts ← ★ 服务端入口(你写代码的地方) diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md index ddbecd8..0272332 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md @@ -100,8 +100,12 @@ config/box3/script/mygame/ ├── eslint.config.mjs ← ESLint rules ├── types/ │ ├── shared.d.ts ← Shared server & client types -│ ├── server/ ← Server API type declarations -│ └── client/ ← Client API type declarations +│ ├── server/ +│ │ ├── index.d.ts ← Server type entry point +│ │ └── ... +│ └── client/ +│ ├── index.d.ts ← Client type entry point +│ └── ... ├── src/ │ ├── server/ │ │ └── app.ts ← ★ Server entry point (where you write code) diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md index a6f895b..10bc2eb 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md @@ -15,7 +15,7 @@ /box3script create hello ``` -这会在 `config/box3/script/hello/` 下生成一个完整的 TypeScript 项目。其中 `src/app.ts` 就是你要写代码的地方。 +这会在 `config/box3/script/hello/` 下生成一个完整的 TypeScript 项目。服务端玩法逻辑写在 `src/server/app.ts`;客户端 UI/输入逻辑写在 `src/client/app.ts`。 ## 第二步:构建 @@ -30,7 +30,7 @@ npm install && npm run build ## 第三步:写你的第一个脚本 -打开 `src/app.ts`,清空内容,写入: +打开 `src/server/app.ts`,清空内容,写入: ```js console.log("Hello, Box3JS!"); diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md index 19bb861..9faf13a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md @@ -15,7 +15,7 @@ Run one command in-game: /box3script create hello ``` -This generates a complete TypeScript project under `config/box3/script/hello/`. The file `src/app.ts` is where you write your code. +This generates a complete TypeScript project under `config/box3/script/hello/`. Server gameplay logic goes in `src/server/app.ts`; client UI/input logic goes in `src/client/app.ts`. ## Step 2: Build @@ -30,7 +30,7 @@ npm install && npm run build ## Step 3: Write Your First Script -Open `src/app.ts`, clear the contents, and write: +Open `src/server/app.ts`, clear the contents, and write: ```js console.log("Hello, Box3JS!"); diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md index 67b36e5..cc6c0de 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md @@ -458,7 +458,7 @@ function endPvPGame(): void { 核心机制:玩家走过地面 → 自动染色为队伍颜色 → 定时发药水 + 速度效果 → 90 秒后按占地数量排名。 -详见 `src/app.ts` 中的 Territory Rush 实现。 +详见 `src/server/app.ts` 中的 Territory Rush 实现。 ## 5.9 更多实用示例 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md index 3dcec2d..802b761 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md @@ -458,7 +458,7 @@ Implemented in the `colorzone` project's `app.ts`. Commands: `!cz` to join, `!cz Core mechanic: players walk over the ground → tiles auto-color to their team → periodic potions + speed buffs → after 90 seconds, ranking by territory claimed. -See `src/app.ts` for the full Territory Rush implementation. +See `src/server/app.ts` for the full Territory Rush implementation. ## 5.9 More Practical Examples diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java index daf0510..1fb4c5c 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java @@ -1,6 +1,7 @@ package com.box3lab.box3js.client; import com.box3lab.box3js.Box3JSNetwork; +import com.box3lab.box3js.script.GameEventHandlerToken; import com.mojang.logging.LogUtils; import net.minecraft.client.Minecraft; import net.minecraft.world.item.ItemStack; @@ -23,10 +24,15 @@ public class Box3JSGuiProxy { private final List slotClickCallbacks = new CopyOnWriteArrayList<>(); private final List closeCallbacks = new CopyOnWriteArrayList<>(); - private boolean callbacksRegistered; + private boolean slotClickRegistered; + private boolean closeRegistered; // ---- Slot manipulation (C→S packets) ---- + public void setItem(int slot, String itemId) { + setItem(slot, itemId, 1); + } + public void setItem(int slot, String itemId, int count) { PacketDistributor.sendToServer( new Box3JSNetwork.GUIServerboundPayload(1, "", 0, "", slot, itemId, Math.max(1, Math.min(count, 64)), false, false)); @@ -59,22 +65,28 @@ public NativeObject getItem(int slot) { // ---- Callbacks ---- - public void onSlotClick(Function callback) { + public GameEventHandlerToken onSlotClick(Function callback) { slotClickCallbacks.add(callback); - ensureCallbacksRegistered(); + ensureCallbacksRegistered(true, false); + return new GameEventHandlerToken(() -> slotClickCallbacks.remove(callback)); } - public void onClose(Function callback) { + public GameEventHandlerToken onClose(Function callback) { closeCallbacks.add(callback); - ensureCallbacksRegistered(); + ensureCallbacksRegistered(false, true); + return new GameEventHandlerToken(() -> closeCallbacks.remove(callback)); } - private void ensureCallbacksRegistered() { - if (callbacksRegistered) return; - callbacksRegistered = true; + private void ensureCallbacksRegistered(boolean wantsSlotClick, boolean wantsClose) { + boolean sendSlotClick = wantsSlotClick && !slotClickRegistered; + boolean sendClose = wantsClose && !closeRegistered; + if (!sendSlotClick && !sendClose) return; + + slotClickRegistered = slotClickRegistered || sendSlotClick; + closeRegistered = closeRegistered || sendClose; PacketDistributor.sendToServer( new Box3JSNetwork.GUIServerboundPayload(2, "", 0, "", 0, "", 0, - !slotClickCallbacks.isEmpty(), !closeCallbacks.isEmpty())); + sendSlotClick, sendClose)); } // ---- Close ---- diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java index 36236f9..39ed0a7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java @@ -34,6 +34,7 @@ public class Box3JSVoxels { private final MinecraftServer server; private final Box3ScriptSandbox sandbox; + private final Box3ScriptEngine engine; private final Map nameToId = new HashMap<>(); private final Map idToName = new HashMap<>(); private final Map resourceToBlock = new HashMap<>(); @@ -43,9 +44,10 @@ public class Box3JSVoxels { public final GameVector3 shape; public final String[] VoxelTypes; - public Box3JSVoxels(MinecraftServer server, Box3ScriptSandbox sandbox) { + public Box3JSVoxels(MinecraftServer server, Box3ScriptSandbox sandbox, Box3ScriptEngine engine) { this.server = server; this.sandbox = sandbox; + this.engine = engine; nameToId.put("air", 0); idToName.put(0, "air"); @@ -116,7 +118,7 @@ public int setVoxel(int x, int y, int z, Object voxel, Object rotation) { ServerLevel level = server.overworld(); BlockPos pos = new BlockPos(x, y, z); - if (sandbox != null) sandbox.trackBlock(Box3ScriptEngine.get().getCurrentProject(), pos); + if (sandbox != null) sandbox.trackBlock(engine.getCurrentProject(), pos); if (voxel == null) return 0; @@ -190,7 +192,7 @@ public int setVoxelId(int x, int y, int z, int voxel) { ServerLevel level = server.overworld(); BlockPos pos = new BlockPos(x, y, z); - if (sandbox != null) sandbox.trackBlock(Box3ScriptEngine.get().getCurrentProject(), pos); + if (sandbox != null) sandbox.trackBlock(engine.getCurrentProject(), pos); int rot = voxel / ROTATION_MULTIPLIER; int baseId = voxel % ROTATION_MULTIPLIER; diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java index d8e92e7..25c2f6f 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java @@ -74,8 +74,8 @@ private void trackIfSandboxed() { // ---- World properties ---- - public String projectName() { return server.getMotd(); } - public String getProjectName() { return server.getMotd(); } + public String projectName() { return projectName != null ? projectName : ""; } + public String getProjectName() { return projectName(); } public int currentTick() { return server.getTickCount(); } public int getCurrentTick() { return server.getTickCount(); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java index a96e8f1..32184ea 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -54,7 +54,7 @@ public void init(MinecraftServer server) { this.server = server; this.sandbox = new Box3ScriptSandbox(server.overworld()); this.worldBinding = new Box3JSWorld(server, this); - this.voxelsBinding = new Box3JSVoxels(server, sandbox); + this.voxelsBinding = new Box3JSVoxels(server, sandbox, this); this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); this.dbBinding = new Box3JSDatabase(server.getServerDirectory().resolve("config"), this); this.httpBinding = new Box3JSHttp(server); @@ -74,7 +74,7 @@ public static Box3ScriptEngine createStandalone(MinecraftServer server, String p engine.currentProject = projectName; engine.sandbox = new Box3ScriptSandbox(server.overworld()); engine.worldBinding = new Box3JSWorld(server, engine); - engine.voxelsBinding = new Box3JSVoxels(server, engine.sandbox); + engine.voxelsBinding = new Box3JSVoxels(server, engine.sandbox, engine); engine.storageBinding = new Box3JSStorage(storageRoot, engine); engine.dbBinding = new Box3JSDatabase(storageRoot, engine); engine.httpBinding = new Box3JSHttp(server); @@ -100,6 +100,10 @@ public long getCurrentTick() { return currentTick; } + public boolean isInitialized() { + return initialized; + } + /** * Handle an incoming {@code ClientEventPayload} from a player. * Dispatches to the server thread, sets project context, fires handlers. @@ -514,6 +518,9 @@ public void fireActionButton(ServerPlayer sp, String button) { // ---- Tick ---- public void fireTick() { + if (!initialized || server == null) { + return; + } prevTick = currentTick; currentTick = server.getTickCount(); fireTimers(); @@ -914,7 +921,7 @@ public void reset() { this.sandbox = new Box3ScriptSandbox(server.overworld()); this.sandbox.inheritEnabled(oldSandbox); this.worldBinding = new Box3JSWorld(server, this); - this.voxelsBinding = new Box3JSVoxels(server, sandbox); + this.voxelsBinding = new Box3JSVoxels(server, sandbox, this); this.storageBinding = new Box3JSStorage(server.getServerDirectory().resolve("config"), this); if (this.dbBinding != null) { this.dbBinding.closeAll(); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java index 7083720..e023377 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java @@ -19,16 +19,19 @@ public class Box3ScriptTemplate { "src/server/app.ts", "src/client/app.ts", "types/shared.d.ts", + "types/server/index.d.ts", "types/server/server.d.ts", "types/server/entity.d.ts", "types/server/player.d.ts", "types/server/world.d.ts", "types/server/voxels.d.ts", + "types/client/index.d.ts", "types/client/client.d.ts", "types/client/audio.d.ts", "types/client/input.d.ts", "types/client/ui.d.ts", "types/client/chat.d.ts", + "types/client/gui.d.ts", }; public static void copyTo(Path projectDir, String projectName) throws IOException { @@ -43,7 +46,7 @@ public static void copyTo(Path projectDir, String projectName) throws IOExceptio throw new IOException("Template file not found: " + resourcePath); Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); } - if (relPath.equals("src/server/app.ts")) { + if (relPath.equals("package.json") || relPath.equals("src/server/app.ts")) { String content = Files.readString(dest); Files.writeString(dest, content.replace("PROJECT_NAME", projectName)); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java index ad15b1c..71b97fd 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java @@ -32,6 +32,7 @@ class Box3ScriptWatcher { private final Box3ScriptConfig config; private WatchService watchService; private ScheduledExecutorService scheduler; + private Thread pollThread; private final Map> pending = new ConcurrentHashMap<>(); private volatile boolean running; @@ -61,15 +62,34 @@ void start() { return t; }); running = true; - new Thread(this::pollLoop, "Box3Script-Watcher-Poll").start(); + pollThread = new Thread(this::pollLoop, "Box3Script-Watcher-Poll"); + pollThread.setDaemon(true); + pollThread.start(); Box3JS.LOGGER.info("File watcher started (dist/ only) on {}", scriptDir); } catch (IOException e) { + running = false; + closeWatchService(); Box3JS.LOGGER.error("Failed to start watcher", e); } } void stop() { running = false; + closeWatchService(); + pending.values().forEach(future -> future.cancel(false)); + pending.clear(); + if (scheduler != null) { + scheduler.shutdownNow(); + scheduler = null; + } + if (pollThread != null) { + pollThread.interrupt(); + pollThread = null; + } + Box3JS.LOGGER.info("File watcher stopped"); + } + + private void closeWatchService() { if (watchService != null) { try { watchService.close(); @@ -77,12 +97,6 @@ void stop() { } watchService = null; } - if (scheduler != null) { - scheduler.shutdownNow(); - scheduler = null; - } - pending.clear(); - Box3JS.LOGGER.info("File watcher stopped"); } /** Register only dist/ directories under each project. */ @@ -149,7 +163,9 @@ private void retryRegister(String project) { distDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); Box3JS.LOGGER.info("Re-registered watch: {}", distDir); } - } catch (IOException | InterruptedException ignored) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException ignored) { } } @@ -157,10 +173,13 @@ private void debounceReload(String project) { if (!config.isEnabled(project)) return; synchronized (pending) { + ScheduledExecutorService activeScheduler = scheduler; + if (!running || activeScheduler == null || activeScheduler.isShutdown()) + return; ScheduledFuture existing = pending.remove(project); if (existing != null) existing.cancel(false); - pending.put(project, scheduler.schedule(() -> { + pending.put(project, activeScheduler.schedule(() -> { pending.remove(project); reloadProject(project); }, DEBOUNCE_MS, TimeUnit.MILLISECONDS)); diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs index be0f6cf..5e42a34 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs @@ -7,9 +7,16 @@ import babel from "@babel/core"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const __dirname = dirname(fileURLToPath(import.meta.url)); -const entryFile = resolve(__dirname, "src/server/app.ts"); +const args = new Set(process.argv.slice(2)); +const watchMode = args.has("--watch"); +const serverOnly = args.has("--server") && !args.has("--client"); +const clientOnly = args.has("--client") && !args.has("--server"); +const shouldBuildServer = !clientOnly; +const shouldBuildClient = !serverOnly; + +const serverEntry = resolve(__dirname, "src/server/app.ts"); const distDir = resolve(__dirname, "dist"); -const outFile = resolve(distDir, "server.js"); +const serverOutFile = resolve(distDir, "server.js"); const clientEntry = resolve(__dirname, "src/client/app.ts"); const clientOutFile = resolve(distDir, "client.js"); @@ -419,8 +426,8 @@ function sanitizeForRhino(code) { // ═══════════════════════════════════════════════════════════════ const buildOptions = { - entryPoints: [entryFile], - outfile: outFile, + entryPoints: [serverEntry], + outfile: serverOutFile, bundle: true, format: "cjs", platform: "neutral", @@ -430,13 +437,18 @@ const buildOptions = { }; async function runBuild() { + if (!existsSync(serverEntry)) { + console.error("Missing server entry: src/server/app.ts"); + process.exit(1); + } try { mkdirSync(distDir, { recursive: true }); + console.log("Building server script -> dist/server.js"); await esbuild.build({ ...buildOptions, metafile: true }); - const code = readFileSync(outFile, "utf8"); - writeFileSync(outFile, sanitizeForRhino(code), "utf-8"); + const code = readFileSync(serverOutFile, "utf8"); + writeFileSync(serverOutFile, sanitizeForRhino(code), "utf-8"); } catch (err) { console.error("Build failed:", err); process.exit(1); @@ -460,6 +472,8 @@ async function buildClient() { return; } try { + mkdirSync(distDir, { recursive: true }); + console.log("Building client script -> dist/client.js"); await esbuild.build({ ...clientBuildOptions, metafile: true }); const code = readFileSync(clientOutFile, "utf8"); @@ -473,28 +487,35 @@ async function buildClient() { // ── Entry ── -if (process.argv.includes("--watch")) { +if (watchMode) { console.log("Watch mode enabled..."); - const ctx = await esbuild.context({ - ...buildOptions, - plugins: [ - babelRhinoPlugin, - { - name: "post-process-plugin", - setup(build) { - build.onEnd(() => { - const code = readFileSync(outFile, "utf8"); - writeFileSync(outFile, sanitizeForRhino(code), "utf-8"); - }); + if (shouldBuildServer) { + if (!existsSync(serverEntry)) { + console.error("Missing server entry: src/server/app.ts"); + process.exit(1); + } + console.log("Server watch mode enabled..."); + const ctx = await esbuild.context({ + ...buildOptions, + plugins: [ + babelRhinoPlugin, + { + name: "post-process-plugin", + setup(build) { + build.onEnd(() => { + const code = readFileSync(serverOutFile, "utf8"); + writeFileSync(serverOutFile, sanitizeForRhino(code), "utf-8"); + }); + }, }, - }, - ], - }); + ], + }); - await ctx.watch(); + await ctx.watch(); + } - if (existsSync(clientEntry)) { + if (shouldBuildClient && existsSync(clientEntry)) { console.log("Client watch mode enabled..."); const clientCtx = await esbuild.context({ ...clientBuildOptions, @@ -512,8 +533,14 @@ if (process.argv.includes("--watch")) { ], }); await clientCtx.watch(); + } else if (shouldBuildClient) { + console.log("No src/client/app.ts found, skipping client watch."); } } else { - await runBuild(); - await buildClient(); + if (shouldBuildServer) { + await runBuild(); + } + if (shouldBuildClient) { + await buildClient(); + } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json index 00bf504..5673818 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/package.json @@ -14,6 +14,8 @@ "type": "module", "scripts": { "build": "node build.mjs", + "build:server": "node build.mjs --server", + "build:client": "node build.mjs --client", "check": "tsc -p tsconfig.server.json --noEmit && tsc -p tsconfig.client.json --noEmit", "check:server": "tsc -p tsconfig.server.json --noEmit", "check:client": "tsc -p tsconfig.client.json --noEmit", diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/client/app.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/client/app.ts index c841b27..aa1ee46 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/client/app.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/client/app.ts @@ -1,22 +1,23 @@ -// Box3JS Client Script -// This script runs on the player's Minecraft client. -// It is automatically sent from the server when a player joins. +// Client script: local UI, input, audio, chat helpers, and client-side events. +remoteChannel.sendServerEvent({ type: "clientReady" }); -// Called every client tick (20 times per second). -client.onTick(() => { - // Your per-frame logic here +remoteChannel.onClientEvent<{ type: string; message?: string }>((event) => { + if (event.args.type === "welcome" && event.args.message) { + ui.showOverlay(event.args.message); + audio.playSound("minecraft:block.note_block.pling", 0.6, 1.2); + } }); -// Show overlay text in the action bar. -// ui.showOverlay("Hello from client script!"); - -// Play a sound on the client. -// audio.playSound("minecraft:block.note_block.pling", 1.0, 1.0); - -// Poll keyboard state. -// if (input.isKeyDown("space")) { ... } +let ticks = 0; +client.onTick(() => { + ticks++; -// Send a chat message. -// chat.sendMessage("Hello!"); + if (ticks % 40 === 0) { + const player = client.getPlayer(); + if (player) { + ui.showActionBar(`FPS ${client.getFPS()} | ${player.name}`); + } + } +}); console.log("[client] loaded!"); diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts index 5f576cf..c884b42 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts @@ -1,23 +1,33 @@ -// Welcome players when they join the server. +// Server script: world logic, players, voxels, storage, and server-side events. world.onPlayerJoin((entity: GamePlayerEntity) => { const p = entity.player; world.say(`§e${p.name} §7joined the server`); - p.directMessage("§6Welcome to §eb §6!"); + p.directMessage("§6Welcome to §ePROJECT_NAME§6!"); p.directMessage("§7Type §e!hello §7to say hi"); + + if (entity.hasBox3JSClient()) { + remoteChannel.sendClientEvent(entity, { + type: "welcome", + message: `Welcome, ${p.name}`, + }); + } }); -// Handle chat commands sent by players. world.onChat((entity: GamePlayerEntity, message: string, _tick: number) => { const p = entity.player; - // Respond to a simple hello command. if (message === "!hello") { world.say(`§e${p.name}§7: Hello World!`); } }); -// Broadcast a periodic status message every 5 seconds (100 ticks). +remoteChannel.onServerEvent<{ type: string; fps?: number }>((event) => { + if (event.args.type === "clientReady") { + console.log(`[PROJECT_NAME] client ready: ${event.entity.player.name}`); + } +}); + let announceTicks = 0; world.onTick(function () { announceTicks++; @@ -25,17 +35,16 @@ world.onTick(function () { if (announceTicks >= 100) { announceTicks = 0; - // Count online entities and show runtime status in each player's action bar. const players = world.querySelectorAll("*"); for (let i = 0; i < players.length; i++) { const p = players[i].player; if (p) { p.actionBar( - `§a⚡ b is running §7| §f${String(players.length)} §7online`, + `§a⚡ PROJECT_NAME is running §7| §f${String(players.length)} §7online`, ); } } } }); -console.log("[b] loaded - Hello World!"); +console.log("[PROJECT_NAME] server loaded"); diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.base.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.base.json index c366687..4a9bdce 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.base.json +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.base.json @@ -10,6 +10,5 @@ "skipLibCheck": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts", "types/**/*.d.ts"] + } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.client.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.client.json index 866ba6e..930475e 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.client.json +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.client.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.base.json", - "include": ["src/client/**/*.ts", "types/shared.d.ts", "types/client/client.d.ts"] + "include": ["src/client/**/*.ts", "types/client/index.d.ts"] } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.server.json b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.server.json index edbd80f..59328f0 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.server.json +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/tsconfig.server.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.base.json", - "include": ["src/server/**/*.ts", "types/shared.d.ts", "types/server/server.d.ts"] + "include": ["src/server/**/*.ts", "types/server/index.d.ts"] } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts index 4982fc5..320b7fa 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/gui.d.ts @@ -12,8 +12,8 @@ interface GameGUI { * ```ts * const ctrl = gui.openGUI({ title: "Shop", rows: 3, slots: { 0: "minecraft:diamond" } }); * console.log(ctrl.getItem(0)); // { id: "minecraft:diamond", count: 1 } - * ctrl.onSlotClick((slot) => { console.log("Clicked slot:", slot); }); - * ctrl.onClose(() => { console.log("GUI closed"); }); + * const clickToken = ctrl.onSlotClick((slot) => { console.log("Clicked slot:", slot); }); + * const closeToken = ctrl.onClose(() => { console.log("GUI closed"); }); * ctrl.close(); * ``` * @@ -57,14 +57,14 @@ interface GuiController { * @en Registers a slot click callback (notification only, cannot cancel the click). * @param callback - @zh 回调函数,接收被点击的槽位索引 @en Callback receiving the clicked slot index */ - onSlotClick(callback: (slot: number) => void): void; + onSlotClick(callback: (slot: number) => void): GameEventHandlerToken; /** * @zh 注册关闭回调。 * @en Registers a close callback. * @param callback - @zh GUI 关闭时调用的回调函数 @en Callback called when the GUI is closed */ - onClose(callback: () => void): void; + onClose(callback: () => void): GameEventHandlerToken; /** * @zh 关闭 GUI。 diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/index.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/index.d.ts new file mode 100644 index 0000000..5154d68 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/index.d.ts @@ -0,0 +1,8 @@ +/// +/// +/// +/// +/// +/// +/// + diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/index.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/index.d.ts new file mode 100644 index 0000000..8e8068d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/index.d.ts @@ -0,0 +1,7 @@ +/// +/// +/// +/// +/// +/// + diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts index e0400fa..50ef9a7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts @@ -13,7 +13,10 @@ interface RemoteChannel { * @param entities - @zh 单个玩家实体或实体数组 @en A single player entity or an array of them * @param clientEvent - @zh 事件数据(任意 JSON 可序列化的值) @en Event data (any JSON‑serializable value) */ - sendClientEvent(entities: any | any[], clientEvent: T): void; + sendClientEvent( + entities: GamePlayerEntity | GamePlayerEntity[], + clientEvent: T, + ): void; /** * @zh 向所有玩家广播客户端事件。 @@ -33,7 +36,7 @@ interface RemoteChannel { /** @zh 事件到达时的服务端 tick @en Server tick when the event arrived */ tick: number; /** @zh 发送事件的玩家实体 @en The player entity that sent the event */ - entity: any; + entity: GamePlayerEntity; /** @zh 事件数据(已反序列化) @en Event data (deserialised) */ args: T; }) => void, diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts index 88831e7..674c11b 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts @@ -10,13 +10,13 @@ interface GameWorld { // ── @zh 世界属性 @en World properties ── - /** @zh 项目名称 (只读) @en Project name, readonly. */ + /** @zh 当前脚本项目名称 (只读方法) @en Current script project name, readonly method. */ projectName(): string; - /** @zh 服务器 MOTD (可读写, 同 projectName) @en Server MOTD (read/write, alias of projectName). */ + /** @zh 服务器 MOTD/标识符 (可读写) @en Server MOTD/identifier, read/write. */ serverId: string; - /** @zh 当前服务端 tick 计数 @en Current server tick count. */ + /** @zh 当前服务端 tick 计数 (只读方法) @en Current server tick count, readonly method. */ currentTick(): number; /**