yeow-api 0.3.10 → 0.4.1
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/package.json +1 -1
- package/src/assets.ts +38 -14
- package/src/block.ts +0 -2
- package/src/core.ts +4 -5
- package/src/env.ts +2 -0
- package/src/event.ts +67 -51
- package/src/fs.ts +204 -59
- package/src/http.ts +18 -5
- package/src/instance-id.ts +7 -2
- package/src/lifecycle.ts +28 -9
- package/src/material.ts +1 -9
- package/src/player.ts +1 -1
- package/src/task.ts +13 -2
- package/src/world.ts +19 -4
package/package.json
CHANGED
package/src/assets.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FsEncoding, ReadFileOptions } from './fs.js';
|
|
2
|
+
|
|
1
3
|
function _sendAssets(payload: Record<string, unknown>): unknown {
|
|
2
4
|
const r = $send('assets', payload);
|
|
3
5
|
if (r == null) return undefined;
|
|
@@ -15,21 +17,43 @@ function _sendAssetsAsync(payload: Record<string, unknown>): Promise<unknown> {
|
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/** 归一化 encoding 参数(与 fs 一致:缺省 = Uint8Array;utf8/base64 → 字符串)。 */
|
|
21
|
+
function _encoding(options?: FsEncoding | ReadFileOptions): FsEncoding | undefined {
|
|
22
|
+
if (options == null) return undefined;
|
|
23
|
+
const e = typeof options === 'string' ? options : options.encoding;
|
|
24
|
+
if (e != null && e !== 'utf8' && e !== 'base64') {
|
|
25
|
+
throw new Error('Unsupported encoding: ' + String(e));
|
|
26
|
+
}
|
|
27
|
+
return e;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// 与 fs.readFile 相同语义:默认返回 Uint8Array;显式 utf8/base64 返回字符串。
|
|
31
|
+
type AssetsReadFn = {
|
|
32
|
+
(path: string): Promise<Uint8Array>;
|
|
33
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): Promise<string>;
|
|
34
|
+
(path: string, options: ReadFileOptions): Promise<Uint8Array | string>;
|
|
35
|
+
};
|
|
36
|
+
type AssetsReadSyncFn = {
|
|
37
|
+
(path: string): Uint8Array;
|
|
38
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): string;
|
|
39
|
+
(path: string, options: ReadFileOptions): Uint8Array | string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const readImpl = async (path: string, options?: FsEncoding | ReadFileOptions): Promise<Uint8Array | string> => {
|
|
43
|
+
const enc = _encoding(options);
|
|
44
|
+
const r = await _sendAssetsAsync({ t: enc === 'utf8' ? 'read' : 'readBase64', p: { path } }) as { data: string };
|
|
45
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
46
|
+
return Uint8Array.fromBase64(r.data);
|
|
47
|
+
};
|
|
48
|
+
const readSyncImpl = (path: string, options?: FsEncoding | ReadFileOptions): Uint8Array | string => {
|
|
49
|
+
const enc = _encoding(options);
|
|
50
|
+
const r = _sendAssets({ t: enc === 'utf8' ? 'read' : 'readBase64', p: { path } }) as { data: string };
|
|
51
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
52
|
+
return Uint8Array.fromBase64(r.data);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const read = readImpl as AssetsReadFn;
|
|
56
|
+
export const readSync = readSyncImpl as AssetsReadSyncFn;
|
|
33
57
|
|
|
34
58
|
export async function extract(path: string, dest?: string): Promise<string> {
|
|
35
59
|
const p: Record<string, unknown> = { path };
|
|
@@ -55,4 +79,4 @@ export function extractDirSync(path: string, dest?: string): string {
|
|
|
55
79
|
return (_sendAssets({ t: 'extractDir', p }) as { path: string }).path;
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
export const assets = { read, readSync,
|
|
82
|
+
export const assets = { read, readSync, extract, extractSync, extractDir, extractDirSync };
|
package/src/block.ts
CHANGED
|
@@ -58,8 +58,6 @@ export class Block {
|
|
|
58
58
|
|
|
59
59
|
isSolid(options?: TaskOptions): Promise<boolean> { return Material.isSolid(this.type, options); }
|
|
60
60
|
isSolidSync(options?: TaskOptions): boolean { return Material.isSolidSync(this.type, options); }
|
|
61
|
-
isLiquid(options?: TaskOptions): Promise<boolean> { return Material.isLiquid(this.type, options); }
|
|
62
|
-
isLiquidSync(options?: TaskOptions): boolean { return Material.isLiquidSync(this.type, options); }
|
|
63
61
|
isAir(options?: TaskOptions): Promise<boolean> { return Material.isAir(this.type, options); }
|
|
64
62
|
isAirSync(options?: TaskOptions): boolean { return Material.isAirSync(this.type, options); }
|
|
65
63
|
|
package/src/core.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference path="global.d.ts" />
|
|
2
2
|
/// <reference path="yeow-dev.d.ts" />
|
|
3
3
|
|
|
4
|
-
export { call, callBatch, postBatch } from './task.js';
|
|
4
|
+
export { call, post, callBatch, postBatch } from './task.js';
|
|
5
5
|
export type { BatchTask } from './task.js';
|
|
6
6
|
export { Location } from './location.js';
|
|
7
7
|
export { Player } from './player.js';
|
|
@@ -43,16 +43,15 @@ export {
|
|
|
43
43
|
export type { TpsInfo } from './server.js';
|
|
44
44
|
export {
|
|
45
45
|
fs,
|
|
46
|
-
readFile, readFileSync,
|
|
47
|
-
writeFile, writeFileSync,
|
|
46
|
+
readFile, readFileSync,
|
|
47
|
+
writeFile, writeFileSync,
|
|
48
48
|
appendFile, appendFileSync,
|
|
49
49
|
exists, existsSync, stat, statSync,
|
|
50
50
|
isDirectory, isDirectorySync,
|
|
51
51
|
deleteFile, deleteFileSync, mkdir, mkdirSync, list, listSync,
|
|
52
52
|
} from './fs.js';
|
|
53
|
-
export type { FileStat } from './fs.js';
|
|
53
|
+
export type { FileStat, FsEncoding, FsData, ReadFileOptions, WriteFileOptions } from './fs.js';
|
|
54
54
|
export { assets, read as assetsRead, readSync as assetsReadSync,
|
|
55
|
-
readBase64 as assetsReadBase64, readBase64Sync as assetsReadBase64Sync,
|
|
56
55
|
extract as assetsExtract, extractSync as assetsExtractSync,
|
|
57
56
|
extractDir as assetsExtractDir, extractDirSync as assetsExtractDirSync } from './assets.js';
|
|
58
57
|
export { path } from './path.js';
|
package/src/env.ts
CHANGED
package/src/event.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { call } from './task.js';
|
|
2
2
|
import { Player } from './player.js';
|
|
3
3
|
import { Location } from './location.js';
|
|
4
|
+
import type { ItemStack } from './item.js';
|
|
4
5
|
import type { Message } from './message.js';
|
|
5
6
|
|
|
6
7
|
// ── Event Data Interfaces ──────────────────────────────────────────
|
|
@@ -31,7 +32,7 @@ export interface PlayerInteractEvent {
|
|
|
31
32
|
player: Player;
|
|
32
33
|
action: string;
|
|
33
34
|
material: string | null;
|
|
34
|
-
block: {
|
|
35
|
+
block: { location: Location; type: string } | null;
|
|
35
36
|
cancelled?: boolean;
|
|
36
37
|
}
|
|
37
38
|
export interface PlayerCommandEvent {
|
|
@@ -53,14 +54,12 @@ export interface PlayerRespawnEvent {
|
|
|
53
54
|
}
|
|
54
55
|
export interface PlayerDropItemEvent {
|
|
55
56
|
player: Player;
|
|
56
|
-
|
|
57
|
-
amount: number;
|
|
57
|
+
item: ItemStack;
|
|
58
58
|
cancelled?: boolean;
|
|
59
59
|
}
|
|
60
60
|
export interface PlayerPickupItemEvent {
|
|
61
61
|
player: Player;
|
|
62
|
-
|
|
63
|
-
amount: number;
|
|
62
|
+
item: ItemStack;
|
|
64
63
|
cancelled?: boolean;
|
|
65
64
|
}
|
|
66
65
|
export interface PlayerBucketFillEvent {
|
|
@@ -111,10 +110,7 @@ export interface EntityDeathEvent {
|
|
|
111
110
|
export interface EntitySpawnEvent {
|
|
112
111
|
entity: string;
|
|
113
112
|
entityType: string;
|
|
114
|
-
|
|
115
|
-
y: number;
|
|
116
|
-
z: number;
|
|
117
|
-
world: string;
|
|
113
|
+
location: Location;
|
|
118
114
|
cancelled?: boolean;
|
|
119
115
|
}
|
|
120
116
|
export interface ProjectileLaunchEvent {
|
|
@@ -126,18 +122,14 @@ export interface ProjectileLaunchEvent {
|
|
|
126
122
|
export interface BlockBreakEvent {
|
|
127
123
|
player: Player;
|
|
128
124
|
block: string;
|
|
129
|
-
|
|
130
|
-
y: number;
|
|
131
|
-
z: number;
|
|
125
|
+
location: Location;
|
|
132
126
|
cancelled?: boolean;
|
|
133
127
|
}
|
|
134
128
|
export interface BlockPlaceEvent {
|
|
135
129
|
player: Player;
|
|
136
130
|
block: string;
|
|
137
131
|
blockAgainst: string;
|
|
138
|
-
|
|
139
|
-
y: number;
|
|
140
|
-
z: number;
|
|
132
|
+
location: Location;
|
|
141
133
|
cancelled?: boolean;
|
|
142
134
|
}
|
|
143
135
|
export interface InventoryOpenEvent {
|
|
@@ -177,7 +169,7 @@ export interface PlayerTeleportEvent {
|
|
|
177
169
|
}
|
|
178
170
|
export interface PlayerItemConsumeEvent {
|
|
179
171
|
player: Player;
|
|
180
|
-
|
|
172
|
+
item: ItemStack;
|
|
181
173
|
cancelled?: boolean;
|
|
182
174
|
}
|
|
183
175
|
export interface PlayerAdvancementDoneEvent {
|
|
@@ -199,9 +191,7 @@ export interface PlayerToggleFlightEvent {
|
|
|
199
191
|
export interface EntityExplodeEvent {
|
|
200
192
|
entity: string;
|
|
201
193
|
entityType: string;
|
|
202
|
-
|
|
203
|
-
y: number;
|
|
204
|
-
z: number;
|
|
194
|
+
location: Location;
|
|
205
195
|
blockCount: number;
|
|
206
196
|
cancelled?: boolean;
|
|
207
197
|
}
|
|
@@ -220,35 +210,27 @@ export interface ProjectileHitEvent {
|
|
|
220
210
|
entity: string;
|
|
221
211
|
projectileType: string;
|
|
222
212
|
hitEntity: string | null;
|
|
223
|
-
hitBlock: {
|
|
213
|
+
hitBlock: { location: Location; type: string } | null;
|
|
224
214
|
cancelled?: boolean;
|
|
225
215
|
}
|
|
226
216
|
export interface BlockFadeEvent {
|
|
227
217
|
block: string;
|
|
228
|
-
|
|
229
|
-
y: number;
|
|
230
|
-
z: number;
|
|
218
|
+
location: Location;
|
|
231
219
|
cancelled?: boolean;
|
|
232
220
|
}
|
|
233
221
|
export interface BlockGrowEvent {
|
|
234
222
|
block: string;
|
|
235
|
-
|
|
236
|
-
y: number;
|
|
237
|
-
z: number;
|
|
223
|
+
location: Location;
|
|
238
224
|
cancelled?: boolean;
|
|
239
225
|
}
|
|
240
226
|
export interface BlockSpreadEvent {
|
|
241
227
|
block: string;
|
|
242
|
-
|
|
243
|
-
y: number;
|
|
244
|
-
z: number;
|
|
228
|
+
location: Location;
|
|
245
229
|
cancelled?: boolean;
|
|
246
230
|
}
|
|
247
231
|
export interface BlockExplodeEvent {
|
|
248
232
|
block: string;
|
|
249
|
-
|
|
250
|
-
y: number;
|
|
251
|
-
z: number;
|
|
233
|
+
location: Location;
|
|
252
234
|
cancelled?: boolean;
|
|
253
235
|
}
|
|
254
236
|
export interface ServerCommandEvent {
|
|
@@ -286,16 +268,12 @@ export interface InventoryClickEvent {
|
|
|
286
268
|
isLeftClick: boolean;
|
|
287
269
|
isRightClick: boolean;
|
|
288
270
|
isShiftClick: boolean;
|
|
289
|
-
clickedItem:
|
|
290
|
-
cursorItem:
|
|
271
|
+
clickedItem: ItemStack | null;
|
|
272
|
+
cursorItem: ItemStack | null;
|
|
291
273
|
/** 若点击发生在 Yeow 自定义 Inventory(Inventory.create 创建):该 Inventory 的句柄 id(inventory.toString());否则缺省。 */
|
|
292
274
|
inventoryId?: string;
|
|
293
275
|
cancelled?: boolean;
|
|
294
276
|
}
|
|
295
|
-
interface ItemData {
|
|
296
|
-
type: string;
|
|
297
|
-
amount: number;
|
|
298
|
-
}
|
|
299
277
|
export interface PlayerResourcePackStatusEvent {
|
|
300
278
|
player: Player;
|
|
301
279
|
status: string;
|
|
@@ -382,6 +360,28 @@ function adaptEvent<K extends keyof EventMap>(type: K, data: RawEvent): { event:
|
|
|
382
360
|
if (data.to) initial.to = loc(data.to as Record<string, unknown>);
|
|
383
361
|
if (data.respawnLocation) initial.respawnLocation = loc(data.respawnLocation as Record<string, unknown>);
|
|
384
362
|
|
|
363
|
+
// 平铺坐标事件(entitySpawn / blockBreak 等)→ 统一 `location: Location`(与 from/to 同形;
|
|
364
|
+
// 原始 x/y/z/world 字段保留在事件对象上,兼容旧代码读取)。
|
|
365
|
+
if (typeof data.x === 'number' && typeof data.y === 'number' && typeof data.z === 'number') {
|
|
366
|
+
initial.location = loc(data);
|
|
367
|
+
}
|
|
368
|
+
// 嵌套方块坐标 → `{ location, type }`
|
|
369
|
+
if (type === 'playerInteract' && data.block && typeof data.block === 'object') {
|
|
370
|
+
const b = data.block as Record<string, unknown>;
|
|
371
|
+
initial.block = { type: b.type as string, location: loc(b) };
|
|
372
|
+
}
|
|
373
|
+
if (type === 'projectileHit' && data.hitBlock && typeof data.hitBlock === 'object') {
|
|
374
|
+
const b = data.hitBlock as Record<string, unknown>;
|
|
375
|
+
initial.hitBlock = { type: b.type as string, location: loc(b) };
|
|
376
|
+
}
|
|
377
|
+
// 物品事件字段统一为 ItemStack({type, amount} 数据快照;原始 itemType/amount 保留)
|
|
378
|
+
if ((type === 'playerDropItem' || type === 'playerPickupItem') && data.itemType) {
|
|
379
|
+
initial.item = { type: data.itemType as string, amount: (data.amount as number) ?? 1 };
|
|
380
|
+
}
|
|
381
|
+
if (type === 'playerItemConsume' && data.itemType) {
|
|
382
|
+
initial.item = { type: data.itemType as string };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
385
|
// 修改收集:所有字段经 getter/setter——handler 直接赋值(e.xxx = ...)即记录为回写 mods。
|
|
386
386
|
// cancelled 单独处理(仅可取消事件暴露;读取语义保持原状:未设置时返回 false)。
|
|
387
387
|
const mods: Record<string, unknown> = {};
|
|
@@ -436,26 +436,42 @@ export function eventOn<K extends keyof EventMap>(
|
|
|
436
436
|
const { event: wrapped, mods: mutatedMods } = adaptEvent(eventType, data);
|
|
437
437
|
const eventId = data?._eventId;
|
|
438
438
|
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
// 释放事件(event.complete)。处理器抛错时也必须释放——否则事件桥等待 complete
|
|
440
|
+
// 直到 5s 超时(EventBridge.timeoutMs),每次抛错都会卡住该事件并产生 event.timeout 噪音。
|
|
441
|
+
const release = (mods?: Record<string, unknown>) => {
|
|
442
|
+
try {
|
|
441
443
|
$send('task', {
|
|
442
444
|
type: 'event.complete',
|
|
443
|
-
params: { eventId, mods
|
|
445
|
+
params: { eventId, mods },
|
|
444
446
|
cb: '',
|
|
445
447
|
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
+
} catch { /* 桥故障:保留原始错误(若有),事件由 Java 侧超时兜底 */ }
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
if (manualRelease) {
|
|
452
|
+
const complete = (result?: Record<string, unknown>) => release(result);
|
|
453
|
+
try {
|
|
454
|
+
(handler as ManualHandler<typeof eventType>)(wrapped, complete);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
release(); // 处理器同步抛错:立即释放(complete 幂等,重复调用无副作用)
|
|
457
|
+
throw e;
|
|
458
|
+
}
|
|
448
459
|
return;
|
|
449
460
|
}
|
|
450
461
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
462
|
+
let result: unknown;
|
|
463
|
+
try {
|
|
464
|
+
result = (handler as EventHandler<typeof eventType>)(wrapped);
|
|
465
|
+
} finally {
|
|
466
|
+
// 回写合并:返回值(mods)优先合并,事件参数直接赋值(e.xxx = ...)覆盖之——
|
|
467
|
+
// 返回 Promise 时视为无修改(Promise 不展开),事件立即释放。
|
|
468
|
+
// finally 保证处理器抛错时也释放事件。
|
|
469
|
+
const mods: Record<string, unknown> = {
|
|
470
|
+
...(result && typeof result === 'object' ? result : {}),
|
|
471
|
+
...mutatedMods,
|
|
472
|
+
};
|
|
473
|
+
release(mods);
|
|
474
|
+
}
|
|
459
475
|
}, { persistent: true });
|
|
460
476
|
|
|
461
477
|
const gh = globalThis as any;
|
package/src/fs.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
type FsLevel = 'plugin' | 'server' | 'outer';
|
|
2
2
|
|
|
3
|
-
import { stringToBytes } from './util.js';
|
|
3
|
+
import { stringToBytes, bytesToString } from './util.js';
|
|
4
|
+
|
|
5
|
+
/** 文件编码:utf8 = 文本;base64 = 将字符串视为 Base64 编码的二进制数据。 */
|
|
6
|
+
export type FsEncoding = 'utf8' | 'base64';
|
|
7
|
+
|
|
8
|
+
/** readFile 选项(Node 风格:缺省 encoding = 返回 Uint8Array)。 */
|
|
9
|
+
export interface ReadFileOptions {
|
|
10
|
+
encoding?: FsEncoding;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** writeFile / appendFile 选项:字符串默认 utf8;可指定 base64 将字符串视为二进制。 */
|
|
14
|
+
export interface WriteFileOptions {
|
|
15
|
+
encoding?: FsEncoding;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FsData = string | Uint8Array;
|
|
4
19
|
|
|
5
20
|
function _sendFs(payload: Record<string, unknown>): unknown {
|
|
6
21
|
const r = $send('fs', payload);
|
|
@@ -19,46 +34,138 @@ function _sendFsAsync(payload: Record<string, unknown>): Promise<unknown> {
|
|
|
19
34
|
});
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
/** 归一化 encoding 参数(string | { encoding });未知编码抛错。 */
|
|
38
|
+
function _encoding(options?: FsEncoding | { encoding?: FsEncoding }): FsEncoding | undefined {
|
|
39
|
+
if (options == null) return undefined;
|
|
40
|
+
const e = typeof options === 'string' ? options : options.encoding;
|
|
41
|
+
if (e != null && e !== 'utf8' && e !== 'base64') {
|
|
42
|
+
throw new Error('Unsupported encoding: ' + String(e));
|
|
43
|
+
}
|
|
44
|
+
return e;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** UTF-8 流式解码器:跨块保留不完整的多字节序列,EOF 时冲刷(非法序列替换为 U+FFFD)。 */
|
|
48
|
+
function _makeUtf8StreamDecoder() {
|
|
49
|
+
let pending: Uint8Array = new Uint8Array(0);
|
|
50
|
+
|
|
51
|
+
const seqLen = (b: number) => {
|
|
52
|
+
if (b < 0x80) return 1;
|
|
53
|
+
if ((b & 0xE0) === 0xC0) return 2;
|
|
54
|
+
if ((b & 0xF0) === 0xE0) return 3;
|
|
55
|
+
if ((b & 0xF8) === 0xF0) return 4;
|
|
56
|
+
return 1; // 非法起始字节:交给 bytesToString 以替换字符处理
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
push(chunk: Uint8Array): string | null {
|
|
61
|
+
const buf = pending.length
|
|
62
|
+
? (() => { const b = new Uint8Array(pending.length + chunk.length); b.set(pending, 0); b.set(chunk, pending.length); return b; })()
|
|
63
|
+
: chunk;
|
|
64
|
+
if (!buf.length) return null;
|
|
65
|
+
|
|
66
|
+
let cut = buf.length;
|
|
67
|
+
const last = buf.length - 1;
|
|
68
|
+
if ((buf[last] & 0x80) !== 0) {
|
|
69
|
+
// 回溯找到可能的多字节序列起点(最多 3 个续字节)
|
|
70
|
+
let start = last;
|
|
71
|
+
let cont = 0;
|
|
72
|
+
while (cont < 3 && start > 0 && (buf[start] & 0xC0) === 0x80) { start--; cont++; }
|
|
73
|
+
const need = seqLen(buf[start]);
|
|
74
|
+
const have = buf.length - start;
|
|
75
|
+
if (need > 1 && have < need && need <= 4) {
|
|
76
|
+
// 仅当确实是合法起始字节时视为“序列未结束”,留给下一块
|
|
77
|
+
const validLead =
|
|
78
|
+
(need === 2 && (buf[start] & 0xE0) === 0xC0) ||
|
|
79
|
+
(need === 3 && (buf[start] & 0xF0) === 0xE0) ||
|
|
80
|
+
(need === 4 && (buf[start] & 0xF8) === 0xF0);
|
|
81
|
+
if (validLead) cut = start;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pending = cut === buf.length ? new Uint8Array(0) : buf.slice(cut);
|
|
86
|
+
const complete = cut === buf.length ? buf : buf.subarray(0, cut);
|
|
87
|
+
return complete.length ? bytesToString(complete) : null;
|
|
88
|
+
},
|
|
89
|
+
/** EOF:把剩余的不完整序列交给 Java 解码(通常产出 U+FFFD)。 */
|
|
90
|
+
flush(): string | null {
|
|
91
|
+
if (!pending.length) return null;
|
|
92
|
+
const s = bytesToString(pending);
|
|
93
|
+
pending = new Uint8Array(0);
|
|
94
|
+
return s;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
22
99
|
/** 按 fs 级别(plugin/server/outer)生成全套文件操作。 */
|
|
23
100
|
function _makeFs(level: FsLevel) {
|
|
24
101
|
const t = (op: string) => `${level}.${op}`;
|
|
25
102
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
function readFileBase64Sync(path: string): string {
|
|
39
|
-
return (_sendFs({ t: t('readBase64'), p: { path } }) as { data: string }).data;
|
|
40
|
-
}
|
|
103
|
+
// ── 读文件:默认 Uint8Array;encoding 指定后返回字符串 ──────────
|
|
104
|
+
type ReadFileFn = {
|
|
105
|
+
(path: string): Promise<Uint8Array>;
|
|
106
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): Promise<string>;
|
|
107
|
+
(path: string, options: ReadFileOptions): Promise<Uint8Array | string>;
|
|
108
|
+
};
|
|
109
|
+
type ReadFileSyncFn = {
|
|
110
|
+
(path: string): Uint8Array;
|
|
111
|
+
(path: string, options: FsEncoding | (ReadFileOptions & { encoding: FsEncoding })): string;
|
|
112
|
+
(path: string, options: ReadFileOptions): Uint8Array | string;
|
|
113
|
+
};
|
|
41
114
|
|
|
42
|
-
async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
115
|
+
const readFileImpl = async (path: string, options?: FsEncoding | ReadFileOptions): Promise<Uint8Array | string> => {
|
|
116
|
+
const enc = _encoding(options);
|
|
117
|
+
const r = await _sendFsAsync({ t: t(enc === 'utf8' ? 'readFile' : 'readBase64'), p: { path } }) as { data: string };
|
|
118
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
119
|
+
return Uint8Array.fromBase64(r.data);
|
|
120
|
+
};
|
|
121
|
+
const readFileSyncImpl = (path: string, options?: FsEncoding | ReadFileOptions): Uint8Array | string => {
|
|
122
|
+
const enc = _encoding(options);
|
|
123
|
+
const r = _sendFs({ t: t(enc === 'utf8' ? 'readFile' : 'readBase64'), p: { path } }) as { data: string };
|
|
124
|
+
if (enc === 'utf8' || enc === 'base64') return r.data;
|
|
125
|
+
return Uint8Array.fromBase64(r.data);
|
|
126
|
+
};
|
|
127
|
+
const readFile = readFileImpl as ReadFileFn;
|
|
128
|
+
const readFileSync = readFileSyncImpl as ReadFileSyncFn;
|
|
48
129
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
// ── 写文件:string 默认 utf8(可指定 base64),Uint8Array 写原始字节 ──
|
|
131
|
+
const writeFile = async (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): Promise<void> => {
|
|
132
|
+
const enc = _encoding(options);
|
|
133
|
+
if (typeof data === 'string') {
|
|
134
|
+
if (enc === 'base64') await _sendFsAsync({ t: t('writeBase64'), p: { path, data } });
|
|
135
|
+
else await _sendFsAsync({ t: t('writeFile'), p: { path, data } });
|
|
136
|
+
} else {
|
|
137
|
+
await _sendFsAsync({ t: t('writeBase64'), p: { path, data: data.toBase64() } });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const writeFileSync = (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): void => {
|
|
141
|
+
const enc = _encoding(options);
|
|
142
|
+
if (typeof data === 'string') {
|
|
143
|
+
if (enc === 'base64') _sendFs({ t: t('writeBase64'), p: { path, data } });
|
|
144
|
+
else _sendFs({ t: t('writeFile'), p: { path, data } });
|
|
145
|
+
} else {
|
|
146
|
+
_sendFs({ t: t('writeBase64'), p: { path, data: data.toBase64() } });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
55
149
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
150
|
+
// ── 追加:与 writeFile 相同的数据/编码语义 ────────────────────────
|
|
151
|
+
const appendFile = async (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): Promise<void> => {
|
|
152
|
+
const enc = _encoding(options);
|
|
153
|
+
if (typeof data === 'string') {
|
|
154
|
+
if (enc === 'base64') await _sendFsAsync({ t: t('appendBase64'), p: { path, data } });
|
|
155
|
+
else await _sendFsAsync({ t: t('appendFile'), p: { path, data } });
|
|
156
|
+
} else {
|
|
157
|
+
await _sendFsAsync({ t: t('appendBase64'), p: { path, data: data.toBase64() } });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const appendFileSync = (path: string, data: FsData, options?: FsEncoding | WriteFileOptions): void => {
|
|
161
|
+
const enc = _encoding(options);
|
|
162
|
+
if (typeof data === 'string') {
|
|
163
|
+
if (enc === 'base64') _sendFs({ t: t('appendBase64'), p: { path, data } });
|
|
164
|
+
else _sendFs({ t: t('appendFile'), p: { path, data } });
|
|
165
|
+
} else {
|
|
166
|
+
_sendFs({ t: t('appendBase64'), p: { path, data: data.toBase64() } });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
62
169
|
|
|
63
170
|
async function exists(path: string): Promise<boolean> {
|
|
64
171
|
const r = await _sendFsAsync({ t: t('exists'), p: { path } });
|
|
@@ -123,19 +230,47 @@ function _makeFs(level: FsLevel) {
|
|
|
123
230
|
return (_sendFs({ t: t('getServerPath') }) as { path: string }).path;
|
|
124
231
|
}
|
|
125
232
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
233
|
+
// ── 读流:encoding 在创建时固定(默认 Uint8Array;utf8/base64 → string)──
|
|
234
|
+
type CreateReadStreamFn = {
|
|
235
|
+
(path: string): Promise<ReadStream<Uint8Array>>;
|
|
236
|
+
(path: string, options: ReadStreamOptions & { encoding?: undefined }): Promise<ReadStream<Uint8Array>>;
|
|
237
|
+
(path: string, options: ReadStreamOptions & { encoding: FsEncoding }): Promise<ReadStream<string>>;
|
|
238
|
+
(path: string, options: ReadStreamOptions): Promise<ReadStream<Uint8Array> | ReadStream<string>>;
|
|
239
|
+
};
|
|
130
240
|
|
|
241
|
+
const createReadStreamImpl = async (path: string, options?: ReadStreamOptions): Promise<ReadStream<Uint8Array> | ReadStream<string>> => {
|
|
242
|
+
const enc = _encoding(options);
|
|
243
|
+
const r = await _sendFsAsync({ t: t('openRead'), p: { path, start: options?.start, end: options?.end } }) as { id: string };
|
|
244
|
+
if (enc === 'base64') {
|
|
245
|
+
return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, {
|
|
246
|
+
push: (b64: string) => b64,
|
|
247
|
+
flush: () => null,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (enc === 'utf8') {
|
|
251
|
+
const dec = _makeUtf8StreamDecoder();
|
|
252
|
+
return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, {
|
|
253
|
+
push: (b64: string) => dec.push(Uint8Array.fromBase64(b64)),
|
|
254
|
+
flush: () => dec.flush(),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return _makeReadStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, {
|
|
258
|
+
push: (b64: string) => Uint8Array.fromBase64(b64),
|
|
259
|
+
flush: () => null,
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
const createReadStream = createReadStreamImpl as CreateReadStreamFn;
|
|
263
|
+
|
|
264
|
+
// ── 写流:与 writeFile 相同的数据/编码语义(encoding 创建时固定)──
|
|
131
265
|
async function createWriteStream(path: string, options?: WriteStreamOptions): Promise<WriteStream> {
|
|
132
|
-
const
|
|
133
|
-
|
|
266
|
+
const enc = _encoding(options);
|
|
267
|
+
const r = await _sendFsAsync({ t: t('openWrite'), p: { path, flags: options?.flags } }) as { id: string };
|
|
268
|
+
return _makeWriteStream((op, p2) => _sendFsAsync({ t: t(op), p: p2 }), r.id, enc);
|
|
134
269
|
}
|
|
135
270
|
|
|
136
271
|
return {
|
|
137
|
-
readFile, readFileSync,
|
|
138
|
-
writeFile, writeFileSync,
|
|
272
|
+
readFile, readFileSync,
|
|
273
|
+
writeFile, writeFileSync,
|
|
139
274
|
appendFile, appendFileSync,
|
|
140
275
|
exists, existsSync, stat, statSync,
|
|
141
276
|
isDirectory, isDirectorySync,
|
|
@@ -157,12 +292,8 @@ export const fs = {
|
|
|
157
292
|
// 顶层函数 = plugin 级别(与 fs.* 一致)
|
|
158
293
|
export const readFile = fs.readFile;
|
|
159
294
|
export const readFileSync = fs.readFileSync;
|
|
160
|
-
export const readFileBase64 = fs.readFileBase64;
|
|
161
|
-
export const readFileBase64Sync = fs.readFileBase64Sync;
|
|
162
295
|
export const writeFile = fs.writeFile;
|
|
163
296
|
export const writeFileSync = fs.writeFileSync;
|
|
164
|
-
export const writeFileBase64 = fs.writeFileBase64;
|
|
165
|
-
export const writeFileBase64Sync = fs.writeFileBase64Sync;
|
|
166
297
|
export const appendFile = fs.appendFile;
|
|
167
298
|
export const appendFileSync = fs.appendFileSync;
|
|
168
299
|
export const exists = fs.exists;
|
|
@@ -182,15 +313,17 @@ export const createWriteStream = fs.createWriteStream;
|
|
|
182
313
|
|
|
183
314
|
// ── 流式读写(有状态句柄;背压 = 显式响应——每个操作 await 结果后才发起下一块)──
|
|
184
315
|
|
|
185
|
-
/** 读流选项:字节偏移区间(start 含、end
|
|
316
|
+
/** 读流选项:字节偏移区间(start 含、end 含);encoding 创建时固定(默认 Uint8Array)。 */
|
|
186
317
|
export interface ReadStreamOptions {
|
|
187
318
|
start?: number;
|
|
188
319
|
end?: number;
|
|
320
|
+
encoding?: FsEncoding;
|
|
189
321
|
}
|
|
190
322
|
|
|
191
|
-
/**
|
|
323
|
+
/** 写流选项:打开模式 + 编码(encoding 创建时固定,运行时不可修改)。 */
|
|
192
324
|
export interface WriteStreamOptions {
|
|
193
325
|
flags?: 'w' | 'a' | 'wx';
|
|
326
|
+
encoding?: FsEncoding;
|
|
194
327
|
}
|
|
195
328
|
|
|
196
329
|
/** 文件状态(stat)。mtimeMs / ctimeMs 为 epoch 毫秒。 */
|
|
@@ -203,32 +336,39 @@ export interface FileStat {
|
|
|
203
336
|
}
|
|
204
337
|
|
|
205
338
|
/** 文件读流:read() 一次返回一块(默认 1 MiB),null = EOF;可 for await。 */
|
|
206
|
-
export interface ReadStream {
|
|
207
|
-
read(maxBytes?: number): Promise<
|
|
339
|
+
export interface ReadStream<Chunk = Uint8Array> {
|
|
340
|
+
read(maxBytes?: number): Promise<Chunk | null>;
|
|
208
341
|
close(): Promise<void>;
|
|
209
|
-
[Symbol.asyncIterator](): AsyncIterator<
|
|
342
|
+
[Symbol.asyncIterator](): AsyncIterator<Chunk>;
|
|
210
343
|
}
|
|
211
344
|
|
|
212
345
|
/** 文件写流:write(chunk) 等到写入完成(显式响应背压),end() 冲刷并关闭。 */
|
|
213
346
|
export interface WriteStream {
|
|
347
|
+
/** 创建时固定的编码;未指定时字符串 chunk 按 UTF-8 编码。运行时不可修改。 */
|
|
348
|
+
readonly encoding?: FsEncoding;
|
|
214
349
|
write(chunk: Uint8Array | string): Promise<void>;
|
|
215
350
|
/** 冲刷缓冲并关闭(调用后不可再 write)。 */
|
|
216
351
|
end(): Promise<void>;
|
|
217
352
|
close(): Promise<void>;
|
|
218
353
|
}
|
|
219
354
|
|
|
220
|
-
function _makeReadStream(
|
|
355
|
+
function _makeReadStream<Chunk>(
|
|
221
356
|
op: (name: string, p: Record<string, unknown>) => Promise<unknown>,
|
|
222
357
|
id: string,
|
|
223
|
-
):
|
|
358
|
+
decoder: { push(b64: string): Chunk | null; flush(): Chunk | null },
|
|
359
|
+
): ReadStream<Chunk> {
|
|
224
360
|
let closed = false;
|
|
225
361
|
const check = () => { if (closed) throw new Error('read stream closed'); };
|
|
226
362
|
return {
|
|
227
|
-
async read(maxBytes?: number) {
|
|
363
|
+
async read(maxBytes?: number): Promise<Chunk | null> {
|
|
228
364
|
check();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
365
|
+
while (true) {
|
|
366
|
+
const r = await op('read', { id, maxBytes }) as { data?: string; eof?: boolean };
|
|
367
|
+
if (r.eof) return decoder.flush();
|
|
368
|
+
const chunk = decoder.push(r.data as string);
|
|
369
|
+
// UTF-8 解码器可能因跨块多字节序列暂未产出——继续读下一块
|
|
370
|
+
if (chunk !== null) return chunk;
|
|
371
|
+
}
|
|
232
372
|
},
|
|
233
373
|
async close() {
|
|
234
374
|
if (closed) return;
|
|
@@ -248,14 +388,19 @@ function _makeReadStream(
|
|
|
248
388
|
function _makeWriteStream(
|
|
249
389
|
op: (name: string, p: Record<string, unknown>) => Promise<unknown>,
|
|
250
390
|
id: string,
|
|
391
|
+
encoding?: FsEncoding,
|
|
251
392
|
): WriteStream {
|
|
252
393
|
let closed = false;
|
|
253
394
|
const check = () => { if (closed) throw new Error('write stream closed'); };
|
|
395
|
+
const toBytes = (chunk: Uint8Array | string): Uint8Array => {
|
|
396
|
+
if (typeof chunk !== 'string') return chunk; // Uint8Array 始终按原始字节写入
|
|
397
|
+
return encoding === 'base64' ? Uint8Array.fromBase64(chunk) : stringToBytes(chunk);
|
|
398
|
+
};
|
|
254
399
|
return {
|
|
400
|
+
encoding,
|
|
255
401
|
async write(chunk: Uint8Array | string) {
|
|
256
402
|
check();
|
|
257
|
-
|
|
258
|
-
await op('write', { id, data: data.toBase64() });
|
|
403
|
+
await op('write', { id, data: toBytes(chunk).toBase64() });
|
|
259
404
|
},
|
|
260
405
|
async end() {
|
|
261
406
|
check();
|
package/src/http.ts
CHANGED
|
@@ -11,10 +11,13 @@ interface HttpResult {
|
|
|
11
11
|
export interface RespondOptions {
|
|
12
12
|
/** HTTP 状态码(默认 200)。 */
|
|
13
13
|
status?: number;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/**
|
|
15
|
+
* 响应体:`Uint8Array` 直接作为二进制写出;字符串按 `encoding` 解释
|
|
16
|
+
* (默认 UTF-8 文本;`'base64'` 时视为 base64 编码的二进制数据)。
|
|
17
|
+
*/
|
|
18
|
+
body?: string | Uint8Array;
|
|
19
|
+
/** body 为字符串时的编码:`'utf8'`(默认)或 `'base64'`(base64 编码的二进制);Uint8Array 时忽略。 */
|
|
20
|
+
encoding?: 'utf8' | 'base64';
|
|
18
21
|
/** 响应头。 */
|
|
19
22
|
headers?: Record<string, string>;
|
|
20
23
|
}
|
|
@@ -44,7 +47,17 @@ export function listen(callback: (req: Record<string, unknown>) => void, port?:
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function respond(serverId: string, connId: string, opts: RespondOptions = {}): void {
|
|
47
|
-
|
|
50
|
+
const p: Record<string, unknown> = { serverId, connId };
|
|
51
|
+
if (opts.body instanceof Uint8Array) {
|
|
52
|
+
p.body = opts.body.toBase64(); // Uint8Array → 二进制(base64 承载)
|
|
53
|
+
p.encoding = 'base64';
|
|
54
|
+
} else {
|
|
55
|
+
p.body = opts.body;
|
|
56
|
+
if (opts.encoding === 'base64') p.encoding = 'base64';
|
|
57
|
+
}
|
|
58
|
+
if (opts.status !== undefined) p.status = opts.status;
|
|
59
|
+
if (opts.headers !== undefined) p.headers = opts.headers;
|
|
60
|
+
_sendHttp({ t: 'respond', p });
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
export function close(serverId: string): void {
|
package/src/instance-id.ts
CHANGED
|
@@ -2,11 +2,16 @@ let _seq = 0;
|
|
|
2
2
|
// 每个插件 QuickJS 上下文独立(模块级变量彼此隔离);随机种子保证
|
|
3
3
|
// 跨插件/跨 Worker 生成的 id 全局唯一——id 是不透明句柄,不携带任何业务信息。
|
|
4
4
|
const _seed = Math.random().toString(36).slice(2, 12);
|
|
5
|
-
|
|
5
|
+
// GC 队列必须复用运行时(init.js)创建的全局数组(读已有 / 否则创建):
|
|
6
|
+
// 1) init.js 的 _flushGC 在每次消息分发后冲刷该数组并上报 gc-collect;
|
|
7
|
+
// 若此处新建数组覆盖全局,FinalizationRegistry 回调推入的 id 永远不会被冲刷。
|
|
8
|
+
// 2) 多 yeow-api 副本共存时(peer 范围不重叠、npm 安装独立副本),共享同一
|
|
9
|
+
// 数组可保证所有副本的句柄回收都进入同一条上报通道。
|
|
10
|
+
const _g = globalThis as any;
|
|
11
|
+
const _gcQueue: string[] = _g.__yeowGcQueue || (_g.__yeowGcQueue = []);
|
|
6
12
|
const _gcReg = typeof FinalizationRegistry !== 'undefined'
|
|
7
13
|
? new FinalizationRegistry<string>((raw: string) => { _gcQueue.push(raw); })
|
|
8
14
|
: null;
|
|
9
|
-
(globalThis as any).__yeowGcQueue = _gcQueue;
|
|
10
15
|
|
|
11
16
|
export class InstanceId {
|
|
12
17
|
readonly _raw: string;
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// 生命周期钩子注册表(共享全局数组)。
|
|
2
|
+
//
|
|
3
|
+
// 多 yeow-api 副本共存(npm 语义化版本规则:peer 范围不重叠时依赖包自带
|
|
4
|
+
// 独立副本)时,每个副本的模块级数组必须指向**同一个**全局数组——
|
|
5
|
+
// 若各副本直接 `globalThis.__yeowInitCbs = <自身数组>`,后加载的副本会
|
|
6
|
+
// 覆盖先加载副本注册的回调(表现为 onLoad 不执行)。读已有 / 否则创建:
|
|
7
|
+
// 首个加载的副本创建数组,后续副本复用,运行时(init.js)读取同一数组。
|
|
8
|
+
const _global = globalThis as any;
|
|
9
|
+
const _initCbs: (() => void)[] = _global.__yeowInitCbs || (_global.__yeowInitCbs = []);
|
|
10
|
+
const _loadCbs: (() => void)[] = _global.__yeowLoadCbs || (_global.__yeowLoadCbs = []);
|
|
11
|
+
const _unloadCbs: (() => void)[] = _global.__yeowUnloadCbs || (_global.__yeowUnloadCbs = []);
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
// dev 模式栈追踪:在注册点捕获调用栈挂到回调函数上(__yeowNode)。
|
|
14
|
+
// 运行时(init.js)分发钩子时优先使用它作为该钩子的栈上下文——比 init.js
|
|
15
|
+
// 分发点的内部帧更能还原用户调用链(外层回调经 _getCurrentCbStack 连接)。
|
|
16
|
+
// 仅 $dev 时捕获(QuickJS 的 Error.stack 为懒构建 getter,无访问即无成本)。
|
|
17
|
+
function _attachNode(cb: () => void): void {
|
|
18
|
+
try {
|
|
19
|
+
if (!_global.$dev) return;
|
|
20
|
+
const getCb = _global._getCurrentCbStack;
|
|
21
|
+
(cb as any).__yeowNode = {
|
|
22
|
+
stack: new Error().stack,
|
|
23
|
+
outer: typeof getCb === 'function' ? getCb() : null,
|
|
24
|
+
};
|
|
25
|
+
} catch { /* 注册点捕获失败不影响钩子注册 */ }
|
|
26
|
+
}
|
|
8
27
|
|
|
9
|
-
(
|
|
10
|
-
(
|
|
11
|
-
(
|
|
28
|
+
export function onInit(cb: () => void): void { _attachNode(cb); _initCbs.push(cb); }
|
|
29
|
+
export function onLoad(cb: () => void): void { _attachNode(cb); _loadCbs.push(cb); }
|
|
30
|
+
export function onUnload(cb: () => void): void { _attachNode(cb); _unloadCbs.push(cb); }
|
package/src/material.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function getItems(options?: TaskOptions): Promise<string[]> {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Material —— 材料级静态判断对象(不依赖坐标/状态)。
|
|
40
|
-
* 基于方块类型(material
|
|
40
|
+
* 基于方块类型(material)判断其固有属性:固体/空气。
|
|
41
41
|
*/
|
|
42
42
|
export const Material = {
|
|
43
43
|
/** 是否为固体方块(基于类型,状态不影响)。 */
|
|
@@ -48,14 +48,6 @@ export const Material = {
|
|
|
48
48
|
return call<boolean>('material.isSolid', { type }, options);
|
|
49
49
|
},
|
|
50
50
|
|
|
51
|
-
/** 是否为液体(水 / 熔岩)。 */
|
|
52
|
-
isLiquid(type: string, options?: TaskOptions): Promise<boolean> {
|
|
53
|
-
return post<boolean>('material.isLiquid', { type }, options);
|
|
54
|
-
},
|
|
55
|
-
isLiquidSync(type: string, options?: TaskOptions): boolean {
|
|
56
|
-
return call<boolean>('material.isLiquid', { type }, options);
|
|
57
|
-
},
|
|
58
|
-
|
|
59
51
|
/** 是否为空气(空方块)。 */
|
|
60
52
|
isAir(type: string, options?: TaskOptions): Promise<boolean> {
|
|
61
53
|
return post<boolean>('material.isAir', { type }, options);
|
package/src/player.ts
CHANGED
|
@@ -79,7 +79,7 @@ export class Player {
|
|
|
79
79
|
isOpAsync(options?: TaskOptions): Promise<boolean> { return post<boolean>('player.isOp', { uuid: this.uuid }, options); }
|
|
80
80
|
|
|
81
81
|
get online(): boolean { return call<boolean>('player.isOnline', { uuid: this.uuid }); }
|
|
82
|
-
|
|
82
|
+
isOnlineAsync(options?: TaskOptions): Promise<boolean> { return post<boolean>('player.isOnline', { uuid: this.uuid }, options); }
|
|
83
83
|
|
|
84
84
|
get isFlying(): boolean { return call<boolean>('player.isFlying', { uuid: this.uuid }); }
|
|
85
85
|
set isFlying(v: boolean) { call('player.setFlying', { uuid: this.uuid, value: v }); }
|
package/src/task.ts
CHANGED
|
@@ -73,12 +73,23 @@ export interface BatchTask {
|
|
|
73
73
|
priority?: 'high' | 'normal' | 'low';
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/** 同步批量:阻塞直到全部任务完成,返回结果数组(顺序对应 tasks;单个任务失败时对应项为 `{err}`
|
|
76
|
+
/** 同步批量:阻塞直到全部任务完成,返回结果数组(顺序对应 tasks;单个任务失败时对应项为 `{err}` 对象——与单任务 call/post 抛 Error 不同,批量保留部分结果)。 */
|
|
77
77
|
export function callBatch(tasks: BatchTask[]): unknown[] {
|
|
78
78
|
if (tasks.length === 0) return [];
|
|
79
79
|
const r = $send('task', { tasks });
|
|
80
80
|
if (r == null) return [];
|
|
81
|
-
if ((r as any)?.err)
|
|
81
|
+
if ((r as any)?.err) {
|
|
82
|
+
// 与 call/post 对齐的错误上下文(type/task/Java 堆栈)
|
|
83
|
+
const errObj = r as any;
|
|
84
|
+
const msg = errObj.type ? `[${errObj.type}] ${errObj.err}` : errObj.err;
|
|
85
|
+
const e = new Error(msg);
|
|
86
|
+
if (errObj.stack) {
|
|
87
|
+
e.stack += '\n --- runtime executer error(for reference) ---\n' + errObj.stack;
|
|
88
|
+
}
|
|
89
|
+
(e as any).javaType = errObj.type || null;
|
|
90
|
+
(e as any).taskType = errObj.task || null;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
82
93
|
return r as unknown[];
|
|
83
94
|
}
|
|
84
95
|
|
package/src/world.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { TaskOptions } from './task.js';
|
|
|
3
3
|
import { Location, LocationData } from './location.js';
|
|
4
4
|
import { Block } from './block.js';
|
|
5
5
|
import type { BlockState } from './block.js';
|
|
6
|
+
import type { ItemStack } from './item.js';
|
|
6
7
|
import { Chunk, ChunkData } from './chunk.js';
|
|
7
8
|
|
|
8
9
|
interface WorldData {
|
|
@@ -103,14 +104,23 @@ export class World {
|
|
|
103
104
|
setBorderDamage(amount?: number, buffer?: number, options?: TaskOptions): Promise<boolean> {
|
|
104
105
|
return post<boolean>('world.setBorderDamage', { world: this.name, amount, buffer }, options);
|
|
105
106
|
}
|
|
107
|
+
setBorderDamageSync(amount?: number, buffer?: number, options?: TaskOptions): boolean {
|
|
108
|
+
return call<boolean>('world.setBorderDamage', { world: this.name, amount, buffer }, options);
|
|
109
|
+
}
|
|
106
110
|
/** 边界警告(distance 方块距离;time 秒)。 */
|
|
107
111
|
setBorderWarning(distance?: number, time?: number, options?: TaskOptions): Promise<boolean> {
|
|
108
112
|
return post<boolean>('world.setBorderWarning', { world: this.name, distance, time }, options);
|
|
109
113
|
}
|
|
114
|
+
setBorderWarningSync(distance?: number, time?: number, options?: TaskOptions): boolean {
|
|
115
|
+
return call<boolean>('world.setBorderWarning', { world: this.name, distance, time }, options);
|
|
116
|
+
}
|
|
110
117
|
/** 边界平滑移动(from → to,seconds 秒)。 */
|
|
111
118
|
setBorderMoving(from: number, to: number, seconds: number, options?: TaskOptions): Promise<boolean> {
|
|
112
119
|
return post<boolean>('world.setBorderMoving', { world: this.name, from, to, seconds }, options);
|
|
113
120
|
}
|
|
121
|
+
setBorderMovingSync(from: number, to: number, seconds: number, options?: TaskOptions): boolean {
|
|
122
|
+
return call<boolean>('world.setBorderMoving', { world: this.name, from, to, seconds }, options);
|
|
123
|
+
}
|
|
114
124
|
getChunkAt(x: number, z: number, options?: TaskOptions): Promise<Chunk> {
|
|
115
125
|
return post<ChunkData>('world.getChunkAt', { world: this.name, x, z }, options).then((d) => Chunk.from(d));
|
|
116
126
|
}
|
|
@@ -211,11 +221,16 @@ export class World {
|
|
|
211
221
|
getNearbyEntitiesSync(x: number, y: number, z: number, radius: number, options?: TaskOptions): string[] {
|
|
212
222
|
return call<string[]>('world.getNearbyEntities', { world: this.name, x, y, z, radius }, options);
|
|
213
223
|
}
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
/** 在指定位置掉落物品(`item` 为 ItemStack 数据快照或材质名字符串;字符串时可用 `amount` 指定数量——旧式参数兼容)。 */
|
|
225
|
+
dropItem(x: number, y: number, z: number, item: ItemStack | string, amount?: number, options?: TaskOptions): Promise<void> {
|
|
226
|
+
const it: Record<string, unknown> = typeof item === 'string' ? { type: item } : { ...item };
|
|
227
|
+
if (typeof item === 'string' && amount !== undefined) it.amount = amount;
|
|
228
|
+
return post('world.dropItem', { world: this.name, x, y, z, item: it }, options);
|
|
216
229
|
}
|
|
217
|
-
dropItemSync(x: number, y: number, z: number,
|
|
218
|
-
|
|
230
|
+
dropItemSync(x: number, y: number, z: number, item: ItemStack | string, amount?: number, options?: TaskOptions): void {
|
|
231
|
+
const it: Record<string, unknown> = typeof item === 'string' ? { type: item } : { ...item };
|
|
232
|
+
if (typeof item === 'string' && amount !== undefined) it.amount = amount;
|
|
233
|
+
call('world.dropItem', { world: this.name, x, y, z, item: it }, options);
|
|
219
234
|
}
|
|
220
235
|
strikeLightning(x: number, y: number, z: number, options?: TaskOptions): Promise<void> {
|
|
221
236
|
return post('world.strikeLightning', { world: this.name, x, y, z }, options);
|