@bkm016/calcite 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -15
- package/dist/accounts.d.ts +23 -4
- package/dist/accounts.js +120 -40
- package/dist/accounts.js.map +1 -1
- package/dist/cli.js +114 -6
- package/dist/cli.js.map +1 -1
- package/dist/client-core.d.ts +68 -0
- package/dist/client-core.js +556 -0
- package/dist/client-core.js.map +1 -0
- package/dist/client.d.ts +107 -213
- package/dist/client.js +114 -579
- package/dist/client.js.map +1 -1
- package/dist/display.js +42 -13
- package/dist/display.js.map +1 -1
- package/dist/hmc.d.ts +31 -11
- package/dist/hmc.js +68 -30
- package/dist/hmc.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +48 -0
- package/dist/install.js +119 -0
- package/dist/install.js.map +1 -0
- package/dist/java.d.ts +8 -2
- package/dist/java.js +14 -10
- package/dist/java.js.map +1 -1
- package/dist/loaders.d.ts +41 -0
- package/dist/loaders.js +86 -0
- package/dist/loaders.js.map +1 -0
- package/dist/mcp.js +163 -6
- package/dist/mcp.js.map +1 -1
- package/dist/mods.d.ts +23 -0
- package/dist/mods.js +164 -0
- package/dist/mods.js.map +1 -0
- package/dist/mojang.d.ts +0 -16
- package/dist/mojang.js +1 -37
- package/dist/mojang.js.map +1 -1
- package/dist/names.d.ts +53 -0
- package/dist/names.js +267 -0
- package/dist/names.js.map +1 -0
- package/dist/net.d.ts +5 -0
- package/dist/net.js +41 -1
- package/dist/net.js.map +1 -1
- package/dist/paths.d.ts +5 -3
- package/dist/paths.js +1 -0
- package/dist/paths.js.map +1 -1
- package/dist/probe-server.d.ts +1 -1
- package/dist/probe-server.js +5 -1
- package/dist/probe-server.js.map +1 -1
- package/dist/types.d.ts +265 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/dist/zip.d.ts +5 -0
- package/dist/zip.js +65 -0
- package/dist/zip.js.map +1 -0
- package/package.json +1 -1
- package/vendor/calcite-probe.jar +0 -0
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ Calcite 用于以编程方式驱动**真实的 Minecraft Java 版客户端**。
|
|
|
28
28
|
- [Node.js API](#nodejs-api)
|
|
29
29
|
- [平台与账号](#平台与账号)
|
|
30
30
|
- [渲染模式](#渲染模式)
|
|
31
|
+
- [模组](#模组)
|
|
32
|
+
- [扩展](#扩展)
|
|
31
33
|
- [版本兼容性](#版本兼容性)
|
|
32
34
|
- [数据目录与环境变量](#数据目录与环境变量)
|
|
33
35
|
- [故障排查](#故障排查)
|
|
@@ -40,8 +42,11 @@ Calcite 用于以编程方式驱动**真实的 Minecraft Java 版客户端**。
|
|
|
40
42
|
- **跨平台**:Linux、Windows、macOS。
|
|
41
43
|
- **账号**:支持离线账号,也支持微软正版账号。正版账号使用设备码登录,凭据持久保存并自动刷新。
|
|
42
44
|
- **无显卡环境截图**:在 Linux 服务器上通过 Xvfb 与 Mesa 软件渲染完成截图。
|
|
45
|
+
- **模组加载器**:一行参数安装 Fabric、Forge 或 NeoForge,并从本地文件、URL 或 Modrinth 加载模组(自动补全前置依赖),探针功能在模组环境中同样可用。
|
|
46
|
+
- **玩家操作**:转向、移动、寻路到坐标、攻击、使用物品与方块、挖掘、切换快捷栏、丢弃物品,以及打开箱子等容器并点击槽位。操作走原版客户端逻辑,与真实玩家输入等价。
|
|
43
47
|
- **按需渲染**:默认不渲染画面,截图时临时渲染。单次截图约 0.5 秒,进服后首次截图需等待区块编译,约 2–4 秒。
|
|
44
|
-
- **Java 自动管理**:按游戏版本自动选择 Java
|
|
48
|
+
- **Java 自动管理**:按游戏版本自动选择 Java(render `off` 时原本需要 Java 8 的版本改用 Java 17,HeadlessMC 3 的 LWJGL 替身不支持 Java 8);本机缺少时从 Eclipse Adoptium 下载 Temurin,并校验 SHA-256。HeadlessMC 3 本身运行在 Java 25 上,同样按需下载。
|
|
49
|
+
- **可扩展**:第三方可用 Java 编写扩展 jar,为 bot 增加自定义命令与事件;游戏内的模组也可以零依赖地注册命令。Node.js、MCP、命令行均可直接调用,见[扩展](#扩展)。
|
|
45
50
|
- **自动重连**:掉线或崩溃后按指数退避自动重连。
|
|
46
51
|
- **多客户端**:一个进程可同时管理多个相互隔离的客户端实例。
|
|
47
52
|
|
|
@@ -50,7 +55,7 @@ Calcite 用于以编程方式驱动**真实的 Minecraft Java 版客户端**。
|
|
|
50
55
|
| 项目 | 要求 |
|
|
51
56
|
| --- | --- |
|
|
52
57
|
| Node.js | 20.18.1 或更高版本 |
|
|
53
|
-
| Java |
|
|
58
|
+
| Java | 无需预装;缺少时自动下载(游戏所需版本,以及 HeadlessMC 所需的 Java 25) |
|
|
54
59
|
| 磁盘 | 每个游戏版本约 0.5–1 GB,资源文件在各版本间共享 |
|
|
55
60
|
| Linux 截图 | `xvfb`、`libgl1-mesa-dri`;Minecraft 26.x 另需 `libegl1`、`libegl-mesa0` |
|
|
56
61
|
|
|
@@ -91,6 +96,20 @@ calcite launch play.example.com -V 1.21.11 -n Bot1
|
|
|
91
96
|
| `:shot [文件]` | 截图并保存为 PNG |
|
|
92
97
|
| `:render on\|off` | 开启或关闭持续渲染 |
|
|
93
98
|
| `:respawn` | 死亡后重生 |
|
|
99
|
+
| `:look <yaw> <pitch>` / `:lookat <x> <y> <z>` | 设置视角 / 看向坐标 |
|
|
100
|
+
| `:goto <x> <z>` | 直线走到指定坐标,遇到台阶自动跳跃 |
|
|
101
|
+
| `:move <按键,...> [tick]` | 按住按键若干 tick(默认 20),按键为 `forward`、`back`、`left`、`right`、`jump`、`sneak`、`sprint`、`attack`、`use` |
|
|
102
|
+
| `:stop` | 停止当前移动、挖掘等持续操作 |
|
|
103
|
+
| `:attack [实体ID]` | 攻击指定实体;省略时攻击准星所指 |
|
|
104
|
+
| `:use [实体ID \| x y z] [tick]` | 右键实体或方块;省略目标时使用手中物品,可指定按住时长 |
|
|
105
|
+
| `:dig <x> <y> <z>` | 挖掘方块直至破坏 |
|
|
106
|
+
| `:block <x> <y> <z>` / `:target` | 查看方块 / 查看准星目标 |
|
|
107
|
+
| `:inv` / `:slot <0-8>` | 查看背包 / 切换快捷栏 |
|
|
108
|
+
| `:container` / `:close` | 查看 / 关闭当前打开的容器 |
|
|
109
|
+
| `:click <槽位> [按键] [模式]` | 点击容器槽位,模式见下文 |
|
|
110
|
+
| `:drop [all]` | 丢弃手中物品(`all` 丢弃整组) |
|
|
111
|
+
| `:ext` | 列出扩展命令 |
|
|
112
|
+
| `:call <命令> [JSON 参数]` | 调用扩展命令,例如 `:call hud.tablist {"limit":5}` |
|
|
94
113
|
| `:quit` | 退出并关闭客户端 |
|
|
95
114
|
|
|
96
115
|
### 命令一览
|
|
@@ -98,7 +117,7 @@ calcite launch play.example.com -V 1.21.11 -n Bot1
|
|
|
98
117
|
| 命令 | 说明 |
|
|
99
118
|
| --- | --- |
|
|
100
119
|
| `calcite launch [server]` | 启动客户端并在终端中交互 |
|
|
101
|
-
| `calcite install [version]` | 预下载指定版本(客户端、库、资源、Java
|
|
120
|
+
| `calcite install [version]` | 预下载指定版本(客户端、库、资源、Java),不启动游戏;`-l, --loader` 同时安装模组加载器 |
|
|
102
121
|
| `calcite versions` | 列出可用版本(`-t release\|snapshot\|old_beta\|old_alpha\|all`,`-l <数量>`) |
|
|
103
122
|
| `calcite login` | 微软账号登录(设备码) |
|
|
104
123
|
| `calcite accounts` | 列出已保存的微软账号 |
|
|
@@ -115,6 +134,9 @@ calcite launch play.example.com -V 1.21.11 -n Bot1
|
|
|
115
134
|
| `-u, --username <name>` | 实例名 | 离线用户名 |
|
|
116
135
|
| `-m, --microsoft [account]` | — | 使用已保存的微软账号(省略名称时使用主账号) |
|
|
117
136
|
| `-r, --render <mode>` | `on-demand` | 渲染模式:`on-demand`、`always`、`off` |
|
|
137
|
+
| `-l, --loader <loader>` | — | 模组加载器:`fabric`、`forge`、`neoforge`,可加 `@<版本>`,见[模组](#模组) |
|
|
138
|
+
| `--mod <spec>` | — | 加载模组,可重复;需同时指定加载器 |
|
|
139
|
+
| `--ext <jar>` | — | 加载探针扩展(本地 jar 或 URL),可重复;扩展事件输出到标准错误 |
|
|
118
140
|
| `--java <path>` | 自动选择 | 指定游戏使用的 Java |
|
|
119
141
|
| `--no-java-download` | — | 禁止自动下载 Java |
|
|
120
142
|
| `--memory <size>` | `2G` | 最大堆内存 |
|
|
@@ -148,13 +170,13 @@ Claude Code 也可以通过命令添加:
|
|
|
148
170
|
claude mcp add calcite -- npx -y @bkm016/calcite mcp
|
|
149
171
|
```
|
|
150
172
|
|
|
151
|
-
`calcite mcp` 支持 `-r, --render <mode>` 和 `--
|
|
173
|
+
`calcite mcp` 支持 `-r, --render <mode>`、`--memory <size>` 和 `--ext <jar>`(可重复),用于设置新客户端的默认值。
|
|
152
174
|
|
|
153
175
|
### 工具列表
|
|
154
176
|
|
|
155
177
|
| 工具 | 说明 |
|
|
156
178
|
| --- | --- |
|
|
157
|
-
| `launch_client` |
|
|
179
|
+
| `launch_client` | 启动客户端,可指定服务器、版本、离线用户名或微软账号、渲染模式、模组加载器与模组 |
|
|
158
180
|
| `stop_client` | 停止客户端 |
|
|
159
181
|
| `list_clients` | 列出当前管理的客户端 |
|
|
160
182
|
| `get_state` | 获取生命周期阶段与游戏状态(界面、坐标、生命值、维度、帧率、断开原因) |
|
|
@@ -162,12 +184,30 @@ claude mcp add calcite -- npx -y @bkm016/calcite mcp
|
|
|
162
184
|
| `send_chat` | 发送聊天消息 |
|
|
163
185
|
| `run_command` | 执行指令,并返回执行期间收到的聊天 |
|
|
164
186
|
| `screenshot` | 渲染并返回 PNG 截图 |
|
|
165
|
-
| `wait_for` |
|
|
187
|
+
| `wait_for` | 等待条件成立:聊天匹配正则、实体出现或消失、进入指定阶段、收到扩展事件 |
|
|
166
188
|
| `get_chat` | 获取聊天记录 |
|
|
167
189
|
| `get_logs` | 获取游戏日志与 Calcite 日志,用于排查崩溃 |
|
|
168
190
|
| `set_render` | 开启或关闭持续渲染 |
|
|
169
191
|
| `respawn` | 死亡后重生 |
|
|
170
|
-
| `
|
|
192
|
+
| `look` | 设置视角,或看向指定坐标 |
|
|
193
|
+
| `walk_to` | 直线走到指定坐标,遇障碍自动跳跃,返回是否到达 |
|
|
194
|
+
| `move` | 按住移动、跳跃、潜行、疾跑等按键若干 tick |
|
|
195
|
+
| `stop_actions` | 停止所有持续操作并松开按键 |
|
|
196
|
+
| `attack` | 攻击实体(按 ID 或准星目标) |
|
|
197
|
+
| `use` | 右键:使用手中物品、与方块交互(开箱、放置)、与实体交互 |
|
|
198
|
+
| `dig` | 挖掘方块直至破坏 |
|
|
199
|
+
| `get_block` | 查询方块 ID 与方块状态 |
|
|
200
|
+
| `get_target` | 查询准星所指的方块或实体 |
|
|
201
|
+
| `get_inventory` | 查询背包、快捷栏与手持物品 |
|
|
202
|
+
| `select_slot` | 切换快捷栏槽位 |
|
|
203
|
+
| `get_container` | 查询当前打开的容器(类型、标题、槽位物品) |
|
|
204
|
+
| `click_slot` | 点击容器槽位,支持拾取、快速移动、数字键交换、丢弃等模式 |
|
|
205
|
+
| `close_container` | 关闭当前容器 |
|
|
206
|
+
| `drop_item` | 丢弃手中物品 |
|
|
207
|
+
| `list_extensions` | 列出扩展与模组注册的命令(名称、说明、参数 schema)及已加载的扩展 jar |
|
|
208
|
+
| `call_extension` | 调用扩展命令 |
|
|
209
|
+
| `get_events` | 获取扩展事件(可按序号、名称正则过滤) |
|
|
210
|
+
| `install_version` | 预下载指定版本,可同时安装模组加载器 |
|
|
171
211
|
| `list_versions` | 列出可用版本 |
|
|
172
212
|
| `account_login_start` | 开始微软账号登录,返回验证链接 |
|
|
173
213
|
| `account_login_status` | 查询登录进度 |
|
|
@@ -210,6 +250,9 @@ await bot.stop();
|
|
|
210
250
|
| `server` | `host[:port]`;省略时停留在标题界面 |
|
|
211
251
|
| `account` | `{ type: 'offline', username }` 或 `{ type: 'microsoft', name? }` |
|
|
212
252
|
| `render` | `on-demand`(默认)、`always`、`off` |
|
|
253
|
+
| `loader` | 模组加载器:`fabric`、`forge`、`neoforge`,可加 `@<版本>` |
|
|
254
|
+
| `mods` | 模组列表:本地 jar 或目录、http(s) URL、`modrinth:<项目>[@<版本>]` |
|
|
255
|
+
| `extensions` | 探针扩展 jar:本地路径或 http(s) URL |
|
|
213
256
|
| `memory` | 最大堆内存,默认 `2G` |
|
|
214
257
|
| `javaPath` / `allowJavaDownload` | 指定 Java 路径 / 是否允许自动下载 |
|
|
215
258
|
| `jvmArgs` / `gameArgs` | 额外的 JVM 参数 / 游戏参数 |
|
|
@@ -228,11 +271,41 @@ await bot.stop();
|
|
|
228
271
|
| `entities(query)` | 实体列表,`query` 支持 `radius`、`type`、`name`、`uuid`、`limit` |
|
|
229
272
|
| `chat(text)` / `command(cmd)` | 发送聊天 / 执行指令 |
|
|
230
273
|
| `screenshot({ keep })` | 返回 `{ png, path? }` |
|
|
231
|
-
| `waitFor(cond, timeoutMs)` |
|
|
274
|
+
| `waitFor(cond, timeoutMs)` | 等待聊天、实体、阶段条件或扩展事件(`{ event: '正则' }`) |
|
|
232
275
|
| `setRender(enabled)` / `respawn()` | 切换持续渲染 / 重生 |
|
|
233
276
|
| `chatSince(seq)` / `logsSince(opts)` | 读取聊天与日志缓冲 |
|
|
277
|
+
| `look({ yaw, pitch } \| { x, y, z })` | 设置视角或看向坐标 |
|
|
278
|
+
| `walkTo(x, z, { range, sprint, timeoutMs })` | 走到坐标,返回 `{ arrived, reason?, distance, x, y, z }` |
|
|
279
|
+
| `move(controls, { ticks })` / `stopActions()` | 按住按键若干 tick / 停止所有持续操作 |
|
|
280
|
+
| `attack(entityId?)` | 攻击实体,省略时攻击准星目标 |
|
|
281
|
+
| `use({ entityId?, block?, holdTicks? })` | 右键实体、方块或使用手中物品 |
|
|
282
|
+
| `dig(block, { timeoutMs })` | 挖掘方块,返回 `{ broken, block, ticks?, reason? }` |
|
|
283
|
+
| `block(pos)` / `target()` | 查询方块 / 准星目标 |
|
|
284
|
+
| `inventory()` / `selectSlot(n)` | 查询背包 / 切换快捷栏 |
|
|
285
|
+
| `container({ waitMs })` / `closeContainer()` | 查询 / 关闭当前容器 |
|
|
286
|
+
| `click(slot, { button, mode })` | 点击容器槽位 |
|
|
287
|
+
| `drop({ all })` | 丢弃手中物品 |
|
|
288
|
+
| `extensions()` / `call(name, args)` | 列出扩展命令 / 调用扩展命令 |
|
|
289
|
+
| `eventsSince({ since, name, limit })` | 读取扩展事件缓冲(最多保留 1000 条) |
|
|
290
|
+
|
|
291
|
+
事件:`phase`、`state`、`chat`、`log`、`exit`、`extension`(扩展事件 `{ seq, time, name, data }`)。
|
|
292
|
+
|
|
293
|
+
### 玩家操作示例
|
|
234
294
|
|
|
235
|
-
|
|
295
|
+
```js
|
|
296
|
+
await bot.walkTo(100.5, 200.5); // 走到 (100.5, 200.5)
|
|
297
|
+
await bot.dig({ x: 101, y: 64, z: 200 }); // 挖掉一个方块
|
|
298
|
+
await bot.use({ block: { x: 100, y: 64, z: 202 } }); // 右键打开箱子
|
|
299
|
+
const chest = await bot.container({ waitMs: 3000 }); // 等待容器界面打开
|
|
300
|
+
for (const item of chest.items.filter((i) => i.slot < chest.containerSlots)) {
|
|
301
|
+
await bot.click(item.slot, { mode: 'quick_move' }); // Shift+点击 取出物品
|
|
302
|
+
}
|
|
303
|
+
await bot.closeContainer();
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
容器槽位编号与原版一致:容器自身的槽位在前(`0` 到 `containerSlots - 1`),玩家背包在后;`item.inventorySlot` 给出对应的背包槽位。`click` 的 `mode` 可取 `pickup`(默认,`button` 0 为左键、1 为右键)、`quick_move`(Shift+点击)、`swap`(数字键,`button` 为快捷栏序号 0–8)、`clone`、`throw`、`quick_craft`、`pickup_all`;槽位 `-999` 表示点击界面外部。未打开容器时,`click` 作用于玩家自身的背包界面。
|
|
307
|
+
|
|
308
|
+
操作有距离限制(6 格):目标过远时返回 `out_of_reach` 错误,需先用 `walkTo` 靠近。
|
|
236
309
|
|
|
237
310
|
此外还导出 `ClientManager`(多客户端管理)、`installVersion`、`startLogin`、`listAccounts`、`removeAccount` 等函数,类型定义随包发布。
|
|
238
311
|
|
|
@@ -243,10 +316,12 @@ await bot.stop();
|
|
|
243
316
|
| Linux | 全部功能(在 Xvfb 中渲染) | 全部功能 |
|
|
244
317
|
| Windows / macOS | 无渲染运行,不支持截图,其余功能正常 | 全部功能 |
|
|
245
318
|
|
|
246
|
-
HeadlessMC
|
|
319
|
+
HeadlessMC 仅在检测到 Xvfb 时允许离线账号渲染,否则强制以无渲染方式运行。这是上游策略,Calcite 遵循该策略。如需在 Windows 或 macOS 上截图,请使用正版账号。
|
|
247
320
|
|
|
248
321
|
离线账号只能进入 `online-mode=false` 的服务器。
|
|
249
322
|
|
|
323
|
+
从 0.1.x 升级:Calcite 现使用 HeadlessMC 3,其凭据格式与 HeadlessMC 2 不兼容,已保存的微软账号需要重新执行一次 `calcite login`。
|
|
324
|
+
|
|
250
325
|
## 渲染模式
|
|
251
326
|
|
|
252
327
|
| 模式 | 说明 |
|
|
@@ -255,11 +330,134 @@ HeadlessMC 仅允许离线账号在 Linux 虚拟显示器中渲染。这是上
|
|
|
255
330
|
| `always` | 持续渲染,适合在本地桌面观察 |
|
|
256
331
|
| `off` | 以无渲染器方式运行(替换 LWJGL),资源占用最低,不支持截图 |
|
|
257
332
|
|
|
333
|
+
## 模组
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# Fabric + Fabric API(从 Modrinth 下载,自动补全前置)
|
|
337
|
+
calcite launch localhost -V 1.21.11 -l fabric --mod modrinth:fabric-api --mod ./mods/my-mod.jar
|
|
338
|
+
|
|
339
|
+
# 指定加载器版本
|
|
340
|
+
calcite launch localhost -V 1.20.1 -l forge@47.4.26
|
|
341
|
+
calcite install 1.21.11 -l neoforge
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
new Client({ version: '1.21.11', loader: 'fabric', mods: ['modrinth:fabric-api', './my-mod.jar'] });
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
<p align="center">
|
|
349
|
+
<img src="docs/images/mods-fabric.jpg" alt="Fabric 1.21.11:Xaero's Minimap 与 Jade" width="49%">
|
|
350
|
+
<img src="docs/images/mods-neoforge.jpg" alt="NeoForge 1.21.11:JEI 物品列表" width="49%">
|
|
351
|
+
</p>
|
|
352
|
+
<p align="center"><sub>左:Fabric 1.21.11,加载 Fabric API、Xaero's Minimap、Jade、Mod Menu,准星对准羊时 Jade 显示其信息。右:NeoForge 1.21.11,加载 JEI 与 Xaero's Minimap,通过 <code>use</code> 打开箱子后右侧为 JEI 物品列表。模组均以 <code>modrinth:</code> 参数自动下载。</sub></p>
|
|
353
|
+
|
|
354
|
+
- 加载器由 HeadlessMC 安装到共享的 `minecraft/versions/`,省略版本时使用最新版本(已安装过则复用本地最新版)。
|
|
355
|
+
- 模组来源:本地 jar、包含 jar 的目录、http(s) URL,或 `modrinth:<项目>[@<版本号>]`。Modrinth 模组按游戏版本与加载器挑选最新正式版,并自动下载必需的前置模组;下载文件按哈希校验并缓存在 `mods/`。
|
|
356
|
+
- 模组复制到实例的 `mods/` 目录。Calcite 只管理自己放入的文件(记录在 `mods/.calcite-mods.json`),手动放入的模组不受影响。
|
|
357
|
+
- 模组加载失败时,客户端会停在错误界面;Calcite 检测到后立即结束进程并返回 `mod_loading_failed`,错误信息附带相关日志。
|
|
358
|
+
|
|
359
|
+
| 加载器 | 支持的版本 | 探针 |
|
|
360
|
+
| --- | --- | --- |
|
|
361
|
+
| Fabric | Fabric 支持的所有版本 | 全部功能(自动将 Mojang 映射转换为 intermediary 名称) |
|
|
362
|
+
| NeoForge | 1.20.2 及以上 | 全部功能 |
|
|
363
|
+
| Forge | 1.17 及以上 | 全部功能(1.20.4 及以前自动转换为 SRG 名称) |
|
|
364
|
+
| Forge | 1.16.5 及以前 | 可以启动,探针功能不可用 |
|
|
365
|
+
|
|
366
|
+
Forge 与 NeoForge 的安装器要求与游戏版本完全一致的 Java 主版本(例如 1.20.1 需要 Java 17),缺少时自动下载。Forge 与 NeoForge 不支持在 render `off` 下运行原本需要 Java 8 的版本。
|
|
367
|
+
|
|
368
|
+
已验证:Fabric 1.21.11 / 26.3(含 Fabric API)、NeoForge 1.20.4 / 1.21.11、Forge 1.20.1 / 1.21.1。
|
|
369
|
+
|
|
370
|
+
## 扩展
|
|
371
|
+
|
|
372
|
+
内置操作之外的能力可以由第三方补充,有两种方式:
|
|
373
|
+
|
|
374
|
+
- **扩展 jar**:用 Java 编写,通过探针 API 注册命令、发送事件,启动时用 `extensions` / `--ext` 加载。适合为 bot 增加读取界面、执行复杂操作等能力,不需要模组加载器。
|
|
375
|
+
- **模组桥接**:游戏中已安装的模组无需依赖 Calcite,直接通过系统属性注册命令。适合模组作者为自己的模组提供测试接口。
|
|
376
|
+
|
|
377
|
+
两种方式注册的命令都可以从 Node.js、MCP 与命令行调用;事件推送到 Node.js 端,可以监听、查询,也可以在 `wait_for` 中等待。
|
|
378
|
+
|
|
379
|
+
### 使用扩展
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
const bot = new Client({ version: '1.21.11', server: 'localhost', extensions: ['./calcite-hud.jar'] });
|
|
383
|
+
bot.on('extension', (e) => console.log(e.name, e.data)); // 例如 hud.title { title: 'Hello', subtitle: null }
|
|
384
|
+
await bot.start();
|
|
385
|
+
|
|
386
|
+
console.log(await bot.extensions()); // { commands: [...], extensions: [{ jar, ids }] }
|
|
387
|
+
console.log(await bot.call('hud.sidebar')); // { title: 'Kills', lines: [{ name: 'Bot1', score: 7 }] }
|
|
388
|
+
await bot.waitFor({ event: '^hud\\.title$' }, 10_000);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
calcite launch localhost -V 1.21.11 --ext ./calcite-hud.jar # 交互模式中用 :ext 与 :call
|
|
393
|
+
calcite mcp --ext ./calcite-hud.jar # MCP 中用 list_extensions / call_extension / get_events
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
[`examples/extension-hud`](examples/extension-hud) 是一个完整示例,提供 `hud.bossbars`(Boss 栏)、`hud.sidebar`(计分板侧边栏)、`hud.tablist`(Tab 列表)、`hud.title`(标题与动作栏)四个命令,并在标题或动作栏变化时发送 `hud.title`、`hud.actionbar` 事件。已在原版 1.21.11、Fabric 1.21.11、Forge 1.20.1 上验证。构建方法:`npm run build:probe && node examples/extension-hud/build.mjs`。
|
|
397
|
+
|
|
398
|
+
### 编写扩展 jar
|
|
399
|
+
|
|
400
|
+
扩展针对探针 API 编译。API 位于 npm 包内的 `vendor/calcite-probe.jar`(包名 `calcite.probe.api`),只在编译时需要,不要打包进扩展。以 Java 8 为目标编译即可在所有版本上运行。
|
|
401
|
+
|
|
402
|
+
```java
|
|
403
|
+
package com.example;
|
|
404
|
+
|
|
405
|
+
import calcite.probe.api.*;
|
|
406
|
+
|
|
407
|
+
public final class MyExtension implements CalciteExtension {
|
|
408
|
+
@Override public String id() { return "my"; } // 命令与事件名的前缀
|
|
409
|
+
|
|
410
|
+
@Override public void init(final Calcite c) {
|
|
411
|
+
c.command("health", "玩家生命值", null, args -> c.onGameThread(() -> {
|
|
412
|
+
Object player = c.ref().get(c.minecraft(), "player"); // Mojang 官方名称,任何加载器下都可用
|
|
413
|
+
return player == null ? null : c.ref().call(player, "getHealth");
|
|
414
|
+
}, 5000)); // 注册为 my.health
|
|
415
|
+
c.emit("ready", c.minecraftVersion()); // 发送事件 my.ready
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
在 jar 中添加 `META-INF/services/calcite.probe.api.CalciteExtension`,内容为实现类的全名(一个 jar 可以包含多个扩展)。
|
|
421
|
+
|
|
422
|
+
`Calcite` 接口:
|
|
423
|
+
|
|
424
|
+
| 方法 | 说明 |
|
|
425
|
+
| --- | --- |
|
|
426
|
+
| `command(name, [description, schema,] handler)` | 注册命令 `<id>.<name>`。`handler` 接收参数 `Map`,返回值序列化为 JSON(支持 `Map`、`List`、数组、字符串、数字、布尔、`null`);`schema` 为参数的 JSON Schema,会显示在 `list_extensions` 中 |
|
|
427
|
+
| `emit(name, data)` | 发送事件 `<id>.<name>` |
|
|
428
|
+
| `onGameThread(task, timeoutMs)` | 在游戏主线程上执行;读写游戏状态必须在主线程进行。命令处理器本身运行在工作线程 |
|
|
429
|
+
| `ref()` | 反射工具,按 Mojang 官方名称访问类、字段与方法(`cls`、`get`、`call`、`method` 等),自动转换为运行时的 intermediary / SRG 名称 |
|
|
430
|
+
| `minecraft()` / `minecraftVersion()` / `headless()` | `Minecraft` 实例 / 版本号 / 是否以无渲染方式运行 |
|
|
431
|
+
| `call(op, args)` | 调用探针内置操作,例如 `state`、`entities`、`chat` |
|
|
432
|
+
| `log(message)` | 写入游戏日志 |
|
|
433
|
+
|
|
434
|
+
处理器抛出 `CalciteException(code, message)` 时,调用方收到对应的错误码;其他异常的错误码为 `error`。扩展的类加载器可以看到游戏(含模组)的全部类,但为了兼容所有加载器与版本,推荐通过 `ref()` 以官方名称反射访问。
|
|
435
|
+
|
|
436
|
+
### 模组桥接
|
|
437
|
+
|
|
438
|
+
模组无需依赖 Calcite。探针在游戏启动前通过系统属性发布两个对象:
|
|
439
|
+
|
|
440
|
+
| 系统属性 | 类型 | 用途 |
|
|
441
|
+
| --- | --- | --- |
|
|
442
|
+
| `calcite.commands` | `Map<String, Object>` | 放入命令。值可以是 `Function<Map<String, Object>, Object>`、`Callable`、`Runnable`,或 `Map`:`{ handler, description, schema }` |
|
|
443
|
+
| `calcite.emit` | `BiConsumer<String, Object>` | 发送事件 |
|
|
444
|
+
|
|
445
|
+
```java
|
|
446
|
+
@SuppressWarnings("unchecked")
|
|
447
|
+
static void registerCalcite() {
|
|
448
|
+
Object commands = System.getProperties().get("calcite.commands");
|
|
449
|
+
if (!(commands instanceof Map)) return; // 不是由 Calcite 启动
|
|
450
|
+
((Map<String, Object>) commands).put("mymod.status", (Function<Map<String, Object>, Object>) args -> MyMod.status());
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
模组命令在 `list_extensions` 中的 `source` 为 `mod`,命令名不会自动加前缀,建议使用 `<模组 ID>.<命令>`。
|
|
455
|
+
|
|
258
456
|
## 版本兼容性
|
|
259
457
|
|
|
260
458
|
| 版本范围 | 支持情况 |
|
|
261
459
|
| --- | --- |
|
|
262
|
-
| 1.14.4 及以上 |
|
|
460
|
+
| 1.14.4 及以上 | 全部功能(状态、实体、聊天、指令、截图、玩家操作) |
|
|
263
461
|
| 1.14.4 以前 | 可以启动并进入服务器;Mojang 未发布这些版本的映射表,探针功能不可用 |
|
|
264
462
|
|
|
265
463
|
已验证的版本:1.16.5、1.20.4、1.21.11、26.3。
|
|
@@ -272,14 +470,17 @@ HeadlessMC 仅允许离线账号在 Linux 虚拟显示器中渲染。这是上
|
|
|
272
470
|
| --- | --- |
|
|
273
471
|
| `minecraft/` | 各版本共享的游戏文件、库与资源 |
|
|
274
472
|
| `instances/<name>/` | 各实例独立的游戏目录(`options.txt`、日志、截图) |
|
|
275
|
-
| `hmc-home/
|
|
473
|
+
| `hmc-home/.auth/default/` | 已保存的微软登录凭据(权限 600,请妥善保管) |
|
|
474
|
+
| `instances/<name>/.headlessmc/` | 该实例的 HeadlessMC 配置、账号副本与缓存 |
|
|
276
475
|
| `java/` | 自动下载的 Java 运行时 |
|
|
476
|
+
| `mods/` | 从 URL 与 Modrinth 下载的模组缓存 |
|
|
477
|
+
| `mappings/` | 映射表及为模组加载器转换后的名称表 |
|
|
277
478
|
|
|
278
479
|
| 环境变量 | 说明 |
|
|
279
480
|
| --- | --- |
|
|
280
481
|
| `CALCITE_HOME` | 数据目录 |
|
|
281
482
|
| `CALCITE_LOG_LEVEL` | 日志级别:`debug`、`info`、`warn`、`error`、`silent` |
|
|
282
|
-
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` |
|
|
483
|
+
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | HTTP 代理。Calcite 自身的下载使用该代理,并将其转换为 Java 系统属性传给 HeadlessMC 与游戏进程(Java 不支持代理认证;游戏与服务器之间的连接不经过代理) |
|
|
283
484
|
| `CALCITE_JAVA_<主版本>` | 指定某个 Java 主版本的路径,例如 `CALCITE_JAVA_8` |
|
|
284
485
|
| `CALCITE_HMC_JAR` / `CALCITE_HMC_URL` | 使用自定义的 HeadlessMC |
|
|
285
486
|
| `CALCITE_PROBE_DIR` | 探针目录,路径中不能包含空格 |
|
|
@@ -298,14 +499,20 @@ HeadlessMC 仅允许离线账号在 Linux 虚拟显示器中渲染。这是上
|
|
|
298
499
|
| `not_logged_in` / `unknown_account` | 未登录微软账号或账号名不存在。请先执行 `calcite login` |
|
|
299
500
|
| `instance_busy` | 同名实例已在其他进程中运行 |
|
|
300
501
|
| `unsupported_version` | 版本不存在或不受支持 |
|
|
502
|
+
| `bad_loader` / `unsupported_loader` | 加载器名称或版本格式错误 / 该版本组合无法运行 |
|
|
503
|
+
| `install_failed` | HeadlessMC 未能安装加载器(该游戏版本可能没有对应构建) |
|
|
504
|
+
| `bad_mod` / `mod_not_found` | 模组参数错误 / 找不到模组文件或 Modrinth 上没有适配的版本 |
|
|
505
|
+
| `mod_loading_failed` | 模组加载失败(缺少前置、版本不兼容等),错误信息附带日志 |
|
|
506
|
+
| `extension_not_found` | 找不到扩展 jar |
|
|
507
|
+
| `unknown_command` | 没有该扩展命令,可用 `list_extensions` 查看;扩展 jar 加载失败的原因见 `extensions()` 返回的 `error` |
|
|
301
508
|
|
|
302
509
|
排查环境问题时,建议先运行 `calcite doctor`。
|
|
303
510
|
|
|
304
511
|
## 工作原理
|
|
305
512
|
|
|
306
513
|
1. Calcite 解析 Mojang 版本清单,下载客户端、库与资源文件,并逐一校验哈希。
|
|
307
|
-
2. 使用 [HeadlessMC](https://github.com/headlesshq/headlessmc)
|
|
308
|
-
3. 向游戏注入一个轻量 Java Agent(探针)。探针借助 Mojang
|
|
514
|
+
2. 使用 [HeadlessMC](https://github.com/headlesshq/headlessmc) 3 启动游戏。每个实例拥有独立的游戏目录、HeadlessMC 配置和启动参数,并发启动互不干扰。
|
|
515
|
+
3. 向游戏注入一个轻量 Java Agent(探针)。探针借助 Mojang 官方映射表(模组环境下转换为 intermediary 或 SRG 名称)定位游戏内部类,并通过本地 TCP 连接与 Calcite 交换 JSON 消息。
|
|
309
516
|
4. 在 Linux 无显示环境中,Calcite 启动共享的 Xvfb,并配置 Mesa 软件渲染。Minecraft 26.x 的渲染器改用 EGL。
|
|
310
517
|
|
|
311
518
|
## 许可
|
package/dist/accounts.d.ts
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import type { CalcitePaths } from './paths.js';
|
|
2
|
-
/** HeadlessMC
|
|
2
|
+
/** HeadlessMC files location holding the Microsoft sessions created by `calcite login`. */
|
|
3
|
+
export declare function accountsHome(paths: CalcitePaths): string;
|
|
4
|
+
/** Microsoft sessions of `calcite login`; Calcite restricts the file to the current user. */
|
|
3
5
|
export declare function accountsFile(paths: CalcitePaths): string;
|
|
4
6
|
export declare function accountsLock(paths: CalcitePaths): string;
|
|
5
7
|
export interface AccountInfo {
|
|
6
8
|
name: string;
|
|
7
9
|
uuid?: string;
|
|
8
|
-
/** The account
|
|
10
|
+
/** The account used when a client does not name one. */
|
|
9
11
|
primary: boolean;
|
|
10
12
|
}
|
|
11
|
-
/** Stored Microsoft accounts (no secrets).
|
|
13
|
+
/** Stored Microsoft accounts (no secrets). */
|
|
12
14
|
export declare function listAccounts(paths: CalcitePaths): Promise<AccountInfo[]>;
|
|
13
15
|
export declare function removeAccount(paths: CalcitePaths, name: string): Promise<boolean>;
|
|
16
|
+
/** UUID the vanilla server assigns to an offline player (version 3, from "OfflinePlayer:<name>"), without dashes. */
|
|
17
|
+
export declare function offlineUuid(name: string): string;
|
|
18
|
+
export type LaunchAccount = {
|
|
19
|
+
type: 'offline';
|
|
20
|
+
username: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'microsoft';
|
|
23
|
+
name?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Prepares the private HeadlessMC location of one client so that HeadlessMC launches exactly this account.
|
|
27
|
+
* Microsoft sessions are copied from the `calcite login` store; {@link syncAccount} writes refreshed tokens back.
|
|
28
|
+
* Returns the account name.
|
|
29
|
+
*/
|
|
30
|
+
export declare function prepareAccount(paths: CalcitePaths, location: string, account: LaunchAccount): Promise<string>;
|
|
31
|
+
/** Copies a Microsoft session refreshed by a launch back into the `calcite login` store. */
|
|
32
|
+
export declare function syncAccount(paths: CalcitePaths, location: string, name: string): Promise<void>;
|
|
14
33
|
export interface LoginHandle {
|
|
15
34
|
/** Microsoft device login URL (contains the code); open it in a browser and approve. */
|
|
16
35
|
url: Promise<string>;
|
|
@@ -20,7 +39,7 @@ export interface LoginHandle {
|
|
|
20
39
|
}
|
|
21
40
|
/**
|
|
22
41
|
* Starts a Microsoft device-code login through HeadlessMC. The session (including the refresh token) is stored
|
|
23
|
-
* in
|
|
42
|
+
* in Calcite's HeadlessMC location and refreshed automatically on every launch.
|
|
24
43
|
*/
|
|
25
44
|
export declare function startLogin(paths: CalcitePaths, { timeoutMs }?: {
|
|
26
45
|
timeoutMs?: number | undefined;
|
package/dist/accounts.js
CHANGED
|
@@ -1,70 +1,151 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { ensureHmc, killTree, runHmc } from './hmc.js';
|
|
4
5
|
import { ensureLauncherJava } from './java.js';
|
|
5
6
|
import { withLock } from './lock.js';
|
|
6
|
-
/** HeadlessMC
|
|
7
|
+
/** HeadlessMC account providers. */
|
|
8
|
+
const MICROSOFT = 'default';
|
|
9
|
+
const OFFLINE = 'offline';
|
|
10
|
+
/** HeadlessMC files location holding the Microsoft sessions created by `calcite login`. */
|
|
11
|
+
export function accountsHome(paths) {
|
|
12
|
+
return paths.hmcHome;
|
|
13
|
+
}
|
|
14
|
+
/** HeadlessMC 3 stores the sessions (including refresh tokens) of one provider in this file. */
|
|
15
|
+
function storeFile(location, provider) {
|
|
16
|
+
return join(location, '.auth', provider, '.accounts.json');
|
|
17
|
+
}
|
|
18
|
+
function lastUsedFile(location) {
|
|
19
|
+
return join(location, '.auth', 'last', '.accounts.json');
|
|
20
|
+
}
|
|
21
|
+
/** Microsoft sessions of `calcite login`; Calcite restricts the file to the current user. */
|
|
7
22
|
export function accountsFile(paths) {
|
|
8
|
-
return
|
|
23
|
+
return storeFile(accountsHome(paths), MICROSOFT);
|
|
9
24
|
}
|
|
10
25
|
export function accountsLock(paths) {
|
|
11
26
|
return join(paths.hmcHome, 'accounts.lock');
|
|
12
27
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
async function readStore(file) {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(await readFile(file, 'utf8'));
|
|
31
|
+
return data.accounts && typeof data.accounts === 'object' && !Array.isArray(data.accounts) ? data.accounts : {};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function writeStore(file, accounts) {
|
|
38
|
+
await mkdir(dirname(file), { recursive: true });
|
|
39
|
+
await writeFile(file, JSON.stringify({ accounts, version: 0 }, null, 2), { mode: 0o600 });
|
|
40
|
+
if (process.platform !== 'win32')
|
|
41
|
+
await chmod(file, 0o600).catch(() => undefined); // Windows: the profile ACL applies
|
|
42
|
+
}
|
|
43
|
+
function findKey(accounts, name) {
|
|
44
|
+
return Object.keys(accounts).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
/** The Minecraft profile id inside a stored MinecraftAuth session (an object carrying both the name and the id). */
|
|
47
|
+
function findUuid(session, name) {
|
|
48
|
+
const stack = [session];
|
|
16
49
|
while (stack.length) {
|
|
17
50
|
const node = stack.pop();
|
|
18
51
|
if (!node || typeof node !== 'object')
|
|
19
52
|
continue;
|
|
20
53
|
const obj = node;
|
|
21
|
-
if (obj.
|
|
22
|
-
|
|
23
|
-
return { name: typeof p.name === 'string' ? p.name : undefined, uuid: typeof p.id === 'string' ? p.id : undefined };
|
|
24
|
-
}
|
|
54
|
+
if (typeof obj.name === 'string' && obj.name.toLowerCase() === name.toLowerCase() && typeof obj.id === 'string')
|
|
55
|
+
return obj.id;
|
|
25
56
|
for (const v of Object.values(obj))
|
|
26
57
|
if (v && typeof v === 'object')
|
|
27
58
|
stack.push(v);
|
|
28
59
|
}
|
|
29
|
-
return
|
|
60
|
+
return undefined;
|
|
30
61
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
62
|
+
/** Name of the most recently used Microsoft account of a HeadlessMC location. */
|
|
63
|
+
async function lastUsed(location) {
|
|
64
|
+
const latest = (await readStore(lastUsedFile(location))).latest;
|
|
65
|
+
if (!Array.isArray(latest))
|
|
66
|
+
return undefined;
|
|
67
|
+
const entry = latest.find((e) => e && typeof e === 'object' && e.provider === MICROSOFT);
|
|
68
|
+
return typeof entry?.name === 'string' ? entry.name : undefined;
|
|
39
69
|
}
|
|
40
|
-
/** Stored Microsoft accounts (no secrets).
|
|
70
|
+
/** Stored Microsoft accounts (no secrets). */
|
|
41
71
|
export async function listAccounts(paths) {
|
|
42
|
-
|
|
72
|
+
const accounts = await readStore(accountsFile(paths));
|
|
73
|
+
const names = Object.keys(accounts);
|
|
74
|
+
const last = await lastUsed(accountsHome(paths));
|
|
75
|
+
const primary = (last && findKey(accounts, last)) ?? names[0];
|
|
76
|
+
return names.map((name) => ({ name, uuid: findUuid(accounts[name], name), primary: name === primary }));
|
|
43
77
|
}
|
|
44
78
|
export async function removeAccount(paths, name) {
|
|
45
79
|
return withLock(accountsLock(paths), async () => {
|
|
46
|
-
const accounts = await
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
80
|
+
const accounts = await readStore(accountsFile(paths));
|
|
81
|
+
const key = findKey(accounts, name);
|
|
82
|
+
if (!key)
|
|
49
83
|
return false;
|
|
50
|
-
|
|
51
|
-
await
|
|
84
|
+
delete accounts[key];
|
|
85
|
+
await writeStore(accountsFile(paths), accounts);
|
|
52
86
|
return true;
|
|
53
87
|
});
|
|
54
88
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
/** UUID the vanilla server assigns to an offline player (version 3, from "OfflinePlayer:<name>"), without dashes. */
|
|
90
|
+
export function offlineUuid(name) {
|
|
91
|
+
const hash = createHash('md5').update(`OfflinePlayer:${name}`, 'utf8').digest();
|
|
92
|
+
hash[6] = (hash[6] & 0x0f) | 0x30;
|
|
93
|
+
hash[8] = (hash[8] & 0x3f) | 0x80;
|
|
94
|
+
return hash.toString('hex');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Prepares the private HeadlessMC location of one client so that HeadlessMC launches exactly this account.
|
|
98
|
+
* Microsoft sessions are copied from the `calcite login` store; {@link syncAccount} writes refreshed tokens back.
|
|
99
|
+
* Returns the account name.
|
|
100
|
+
*/
|
|
101
|
+
export async function prepareAccount(paths, location, account) {
|
|
102
|
+
if (account.type === 'offline') {
|
|
103
|
+
const name = account.username;
|
|
104
|
+
await rm(storeFile(location, MICROSOFT), { force: true });
|
|
105
|
+
await writeStore(storeFile(location, OFFLINE), {
|
|
106
|
+
[name]: { provider: OFFLINE, name, uuid: offlineUuid(name), token: '', type: 'msa', xuid: '' },
|
|
107
|
+
});
|
|
108
|
+
await writeStore(lastUsedFile(location), { latest: [{ provider: OFFLINE, name }] });
|
|
109
|
+
return name;
|
|
110
|
+
}
|
|
111
|
+
return withLock(accountsLock(paths), async () => {
|
|
112
|
+
const accounts = await listAccounts(paths);
|
|
113
|
+
if (!accounts.length)
|
|
114
|
+
throw Object.assign(new Error('No Microsoft account is stored; run `calcite login` first'), { code: 'not_logged_in' });
|
|
115
|
+
const wanted = account.name;
|
|
116
|
+
const match = wanted ? accounts.find((a) => a.name.toLowerCase() === wanted.toLowerCase()) : accounts.find((a) => a.primary);
|
|
117
|
+
if (!match) {
|
|
118
|
+
throw Object.assign(new Error(`No stored Microsoft account named "${wanted}" (have: ${accounts.map((a) => a.name).join(', ')})`), { code: 'unknown_account' });
|
|
119
|
+
}
|
|
120
|
+
const session = (await readStore(accountsFile(paths)))[match.name];
|
|
121
|
+
await rm(storeFile(location, OFFLINE), { force: true });
|
|
122
|
+
await writeStore(storeFile(location, MICROSOFT), { [match.name]: session });
|
|
123
|
+
await writeStore(lastUsedFile(location), { latest: [{ provider: MICROSOFT, name: match.name }] });
|
|
124
|
+
return match.name;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Copies a Microsoft session refreshed by a launch back into the `calcite login` store. */
|
|
128
|
+
export async function syncAccount(paths, location, name) {
|
|
129
|
+
const local = await readStore(storeFile(location, MICROSOFT));
|
|
130
|
+
const key = findKey(local, name);
|
|
131
|
+
if (!key)
|
|
132
|
+
return;
|
|
133
|
+
await withLock(accountsLock(paths), async () => {
|
|
134
|
+
const accounts = await readStore(accountsFile(paths));
|
|
135
|
+
const central = findKey(accounts, name);
|
|
136
|
+
if (!central || JSON.stringify(accounts[central]) === JSON.stringify(local[key]))
|
|
137
|
+
return; // removed meanwhile, or unchanged
|
|
138
|
+
accounts[central] = local[key];
|
|
139
|
+
await writeStore(accountsFile(paths), accounts);
|
|
140
|
+
});
|
|
59
141
|
}
|
|
60
142
|
/**
|
|
61
143
|
* Starts a Microsoft device-code login through HeadlessMC. The session (including the refresh token) is stored
|
|
62
|
-
* in
|
|
144
|
+
* in Calcite's HeadlessMC location and refreshed automatically on every launch.
|
|
63
145
|
*/
|
|
64
146
|
export async function startLogin(paths, { timeoutMs = 15 * 60_000 } = {}) {
|
|
65
147
|
const java = await ensureLauncherJava(paths);
|
|
66
148
|
const hmcJar = await ensureHmc(paths);
|
|
67
|
-
await ensureHmcHome(paths);
|
|
68
149
|
let resolveUrl;
|
|
69
150
|
let rejectUrl;
|
|
70
151
|
const url = new Promise((res, rej) => {
|
|
@@ -76,17 +157,16 @@ export async function startLogin(paths, { timeoutMs = 15 * 60_000 } = {}) {
|
|
|
76
157
|
const run = runHmc({
|
|
77
158
|
javaPath: java.path,
|
|
78
159
|
hmcJar,
|
|
79
|
-
paths,
|
|
80
|
-
props: { 'hmc.store.accounts': 'true' },
|
|
160
|
+
location: accountsHome(paths),
|
|
81
161
|
command: ['login'],
|
|
82
162
|
onLine: (line) => {
|
|
83
|
-
const go = /
|
|
163
|
+
const go = /To login visit (https?:\/\/\S+)/.exec(line);
|
|
84
164
|
if (go)
|
|
85
165
|
resolveUrl(go[1]);
|
|
86
|
-
const ok = /Logged
|
|
166
|
+
const ok = /Logged in to account (.+?) successfully/.exec(line);
|
|
87
167
|
if (ok)
|
|
88
168
|
account = ok[1];
|
|
89
|
-
if (/Failed to login|You can't play|
|
|
169
|
+
if (/AuthException|Failed to login|You can't play|Exception:/.test(line) && !failure)
|
|
90
170
|
failure = line.trim();
|
|
91
171
|
},
|
|
92
172
|
});
|
|
@@ -98,7 +178,7 @@ export async function startLogin(paths, { timeoutMs = 15 * 60_000 } = {}) {
|
|
|
98
178
|
rejectUrl(err);
|
|
99
179
|
throw err;
|
|
100
180
|
}
|
|
101
|
-
await
|
|
181
|
+
await chmod(accountsFile(paths), 0o600).catch(() => undefined);
|
|
102
182
|
return account;
|
|
103
183
|
});
|
|
104
184
|
done.catch(() => undefined);
|