@threenative/core 0.3.0 → 0.3.2

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.
Files changed (39) hide show
  1. package/README.md +10 -0
  2. package/capabilities.json +1881 -131
  3. package/dist/assets-kyoF7JlJ.d.ts +103 -0
  4. package/dist/{audio-Dp2mXpD3.d.ts → audio-BFiGneTL.d.ts} +62 -0
  5. package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
  6. package/dist/{game-CYIaKhgl.d.ts → game-XGrTzapq.d.ts} +350 -164
  7. package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
  8. package/dist/hot.d.ts +5 -3
  9. package/dist/hot.js +5 -1
  10. package/dist/index.d.ts +813 -143
  11. package/dist/index.js +4548 -941
  12. package/dist/net.d.ts +65 -0
  13. package/dist/net.js +643 -0
  14. package/dist/playtest.d.ts +29 -5
  15. package/dist/playtest.js +181 -35
  16. package/dist/react.d.ts +4 -2
  17. package/dist/{canvas-layer-CtrZHgIh.d.ts → renderer-C6hqZpoG.d.ts} +237 -75
  18. package/dist/world.d.ts +203 -4
  19. package/dist/world.js +2536 -25
  20. package/gpl/LICENSE.GPL +117 -0
  21. package/gpl/convert.py +192 -0
  22. package/gpl/recipes/_common.py +169 -0
  23. package/gpl/recipes/bake_ao.py +111 -0
  24. package/gpl/recipes/decimate.py +64 -0
  25. package/gpl/recipes/retarget.py +131 -0
  26. package/gpl/recipes/unwrap.py +71 -0
  27. package/mcp/blender-server.mjs +632 -0
  28. package/mcp/blender.mjs +27 -0
  29. package/mcp/engine-server.mjs +271 -23
  30. package/mcp/engine.mjs +15 -7
  31. package/mcp/install.d.mts +37 -0
  32. package/mcp/install.mjs +94 -26
  33. package/mcp/servers.d.mts +34 -0
  34. package/mcp/servers.mjs +110 -9
  35. package/package.json +36 -8
  36. package/patches/three@0.185.1.patch +249 -14
  37. package/scripts/ensure-mcp.mjs +20 -13
  38. package/scripts/bundle-engine-mcp.mjs +0 -15
  39. package/scripts/generate-version.mjs +0 -13
package/dist/net.js ADDED
@@ -0,0 +1,643 @@
1
+ // src/net-protocol.ts
2
+ var WIRE_VERSION = 1;
3
+ var KIND_HELLO = 1;
4
+ var KIND_WELCOME = 2;
5
+ var KIND_BIND = 3;
6
+ var KIND_BOUND = 4;
7
+ var KIND_DATA = 16;
8
+ var HEADER_BYTES = 8;
9
+ var MAX_HELLO_BYTES = 4096;
10
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
11
+ var DEFAULT_MAX_RELIABLE_MESSAGE_BYTES = 65536;
12
+ var DEFAULT_MAX_QUEUED_RELIABLE_BYTES = 1048576;
13
+ var DEFAULT_MAX_QUEUED_DATAGRAMS = 256;
14
+ function invalidArgument(message) {
15
+ return new Error(`TN_NET_INVALID_ARGUMENT: ${message}`);
16
+ }
17
+ function isPositiveSafeInteger(value) {
18
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
19
+ }
20
+ function normalizeOptions(url, options) {
21
+ let parsed;
22
+ try {
23
+ parsed = new URL(url);
24
+ } catch {
25
+ throw invalidArgument("url must be a valid URL");
26
+ }
27
+ if (parsed.protocol !== "https:") throw invalidArgument("url must use https");
28
+ if (options === null || typeof options !== "object")
29
+ throw invalidArgument("options are required");
30
+ if (typeof options.applicationProtocol !== "string")
31
+ throw invalidArgument("applicationProtocol is required");
32
+ if (options.applicationProtocol.length < 1 || options.applicationProtocol.length > 64 || !/^[\x20-\x7e]+$/u.test(options.applicationProtocol))
33
+ throw invalidArgument("applicationProtocol must be 1-64 printable ASCII characters");
34
+ if (typeof options.credential !== "string" || options.credential.length === 0)
35
+ throw invalidArgument("credential must be nonempty");
36
+ if (!Array.isArray(options.channels) || options.channels.length < 1 || options.channels.length > 32)
37
+ throw invalidArgument("channels must list 1-32 entries");
38
+ const seen = /* @__PURE__ */ new Set();
39
+ const channels = options.channels.map((channel) => {
40
+ if (channel === null || typeof channel !== "object")
41
+ throw invalidArgument("channel entries are required");
42
+ if (!Number.isInteger(channel.id) || channel.id < 1 || channel.id > 65535)
43
+ throw invalidArgument("channel ids must be integers 1-65535");
44
+ if (channel.delivery !== "unreliable" && channel.delivery !== "reliable-ordered")
45
+ throw invalidArgument("channel delivery is unknown");
46
+ if (seen.has(channel.id)) throw invalidArgument("channel ids must be unique");
47
+ seen.add(channel.id);
48
+ return { id: channel.id, delivery: channel.delivery };
49
+ });
50
+ if (options.signal !== void 0 && !(options.signal instanceof AbortSignal))
51
+ throw invalidArgument("signal must be an AbortSignal");
52
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
53
+ const maxReliableMessageBytes = options.maxReliableMessageBytes ?? DEFAULT_MAX_RELIABLE_MESSAGE_BYTES;
54
+ const maxQueuedReliableBytes = options.maxQueuedReliableBytes ?? DEFAULT_MAX_QUEUED_RELIABLE_BYTES;
55
+ const maxQueuedDatagrams = options.maxQueuedDatagrams ?? DEFAULT_MAX_QUEUED_DATAGRAMS;
56
+ if (!isPositiveSafeInteger(connectTimeoutMs))
57
+ throw invalidArgument("connectTimeoutMs must be positive");
58
+ if (!isPositiveSafeInteger(maxReliableMessageBytes))
59
+ throw invalidArgument("maxReliableMessageBytes must be positive");
60
+ if (!isPositiveSafeInteger(maxQueuedReliableBytes))
61
+ throw invalidArgument("maxQueuedReliableBytes must be positive");
62
+ if (!isPositiveSafeInteger(maxQueuedDatagrams))
63
+ throw invalidArgument("maxQueuedDatagrams must be positive");
64
+ if (maxReliableMessageBytes > maxQueuedReliableBytes)
65
+ throw invalidArgument("message limit cannot exceed queue limit");
66
+ return {
67
+ applicationProtocol: options.applicationProtocol,
68
+ credential: options.credential,
69
+ channels,
70
+ signal: options.signal,
71
+ connectTimeoutMs,
72
+ maxReliableMessageBytes,
73
+ maxQueuedReliableBytes,
74
+ maxQueuedDatagrams
75
+ };
76
+ }
77
+ function encodeFrame(kind, channel, payload) {
78
+ const out = new Uint8Array(HEADER_BYTES + payload.length);
79
+ const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
80
+ out[0] = WIRE_VERSION;
81
+ out[1] = kind;
82
+ view.setUint16(2, channel);
83
+ view.setUint32(4, payload.length);
84
+ out.set(payload, HEADER_BYTES);
85
+ return out;
86
+ }
87
+ function checkFrameHead(version, kind, channel) {
88
+ if (version !== WIRE_VERSION) throw new Error("TN_NET_PROTOCOL: unknown wire version");
89
+ if (kind !== KIND_HELLO && kind !== KIND_WELCOME && kind !== KIND_BIND && kind !== KIND_BOUND && kind !== KIND_DATA)
90
+ throw new Error("TN_NET_PROTOCOL: unknown frame kind");
91
+ if (kind === KIND_DATA && channel === 0) throw new Error("TN_NET_PROTOCOL: DATA on channel 0");
92
+ }
93
+ function decodeStandaloneFrame(bytes) {
94
+ if (bytes.length < HEADER_BYTES) throw new Error("TN_NET_PROTOCOL: truncated header");
95
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
96
+ const version = bytes[0] ?? 0;
97
+ const kind = bytes[1] ?? 0;
98
+ const channel = view.getUint16(2);
99
+ const length = view.getUint32(4);
100
+ checkFrameHead(version, kind, channel);
101
+ if (bytes.length !== HEADER_BYTES + length)
102
+ throw new Error("TN_NET_PROTOCOL: bad datagram length");
103
+ return { kind, channel, payload: bytes.slice(HEADER_BYTES) };
104
+ }
105
+ function readStreamFrame(buffer, limit) {
106
+ if (buffer.length < HEADER_BYTES) return void 0;
107
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
108
+ const version = buffer[0] ?? 0;
109
+ const kind = buffer[1] ?? 0;
110
+ const channel = view.getUint16(2);
111
+ const length = view.getUint32(4);
112
+ checkFrameHead(version, kind, channel);
113
+ if (length > limit) throw new Error("TN_NET_PROTOCOL: frame exceeds limit");
114
+ const frameBytes = HEADER_BYTES + length;
115
+ if (frameBytes > buffer.length) return void 0;
116
+ return {
117
+ frame: { kind, channel, payload: buffer.slice(HEADER_BYTES, frameBytes) },
118
+ rest: buffer.subarray(frameBytes)
119
+ };
120
+ }
121
+ function webTransportConstructor() {
122
+ const value = globalThis.WebTransport;
123
+ if (typeof value !== "function") return void 0;
124
+ return value;
125
+ }
126
+ function parseWelcome(payload, expectedProtocol) {
127
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
128
+ const parsed = JSON.parse(text);
129
+ if (Object.keys(parsed).sort().join(",") !== "applicationProtocol,channels,maxQueuedDatagrams,maxQueuedReliableBytes,maxReliableMessageBytes,sessionId")
130
+ throw new Error("TN_NET_PROTOCOL: WELCOME has unexpected keys");
131
+ if (parsed.applicationProtocol !== expectedProtocol)
132
+ throw new Error("TN_NET_PROTOCOL: WELCOME protocol mismatch");
133
+ if (typeof parsed.sessionId !== "string" || !/^[0-9a-f]{32}$/u.test(parsed.sessionId))
134
+ throw new Error("TN_NET_PROTOCOL: WELCOME session id is invalid");
135
+ if (!Array.isArray(parsed.channels))
136
+ throw new Error("TN_NET_PROTOCOL: WELCOME channels are invalid");
137
+ const channels = parsed.channels.map((entry) => {
138
+ if (entry === null || typeof entry !== "object")
139
+ throw new Error("TN_NET_PROTOCOL: bad channel");
140
+ const record = entry;
141
+ if (Object.keys(record).sort().join(",") !== "delivery,id")
142
+ throw new Error("TN_NET_PROTOCOL: bad channel shape");
143
+ if (!Number.isInteger(record.id) || record.id < 1 || record.id > 65535)
144
+ throw new Error("TN_NET_PROTOCOL: bad channel id");
145
+ if (record.delivery !== "unreliable" && record.delivery !== "reliable-ordered")
146
+ throw new Error("TN_NET_PROTOCOL: bad delivery");
147
+ return {
148
+ id: record.id,
149
+ delivery: record.delivery
150
+ };
151
+ });
152
+ for (const key of [
153
+ "maxReliableMessageBytes",
154
+ "maxQueuedReliableBytes",
155
+ "maxQueuedDatagrams"
156
+ ]) {
157
+ const value = parsed[key];
158
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
159
+ throw new Error("TN_NET_PROTOCOL: bad WELCOME limit");
160
+ }
161
+ const messageLimit = parsed.maxReliableMessageBytes;
162
+ const queueLimit = parsed.maxQueuedReliableBytes;
163
+ if (messageLimit > queueLimit) throw new Error("TN_NET_PROTOCOL: invalid negotiated limits");
164
+ return {
165
+ sessionId: parsed.sessionId,
166
+ channels,
167
+ maxReliableMessageBytes: messageLimit,
168
+ maxQueuedReliableBytes: queueLimit,
169
+ maxQueuedDatagrams: parsed.maxQueuedDatagrams
170
+ };
171
+ }
172
+ async function writeAll(writable, bytes) {
173
+ const writer = writable.getWriter();
174
+ try {
175
+ await writer.write(bytes);
176
+ } finally {
177
+ writer.releaseLock();
178
+ }
179
+ }
180
+ function concatBytes(left, right) {
181
+ const out = new Uint8Array(left.length + right.length);
182
+ out.set(left, 0);
183
+ out.set(right, left.length);
184
+ return out;
185
+ }
186
+ function asBytes(chunk) {
187
+ if (chunk instanceof Uint8Array) return Uint8Array.from(chunk);
188
+ if (chunk instanceof ArrayBuffer) return Uint8Array.from(new Uint8Array(chunk));
189
+ return Uint8Array.from(new Uint8Array(chunk));
190
+ }
191
+ var FrameReader = class {
192
+ #reader;
193
+ #buffer = new Uint8Array(0);
194
+ #limit;
195
+ #released = false;
196
+ constructor(readable, limit) {
197
+ this.#reader = readable.getReader();
198
+ this.#limit = limit;
199
+ }
200
+ async nextFrame(signal) {
201
+ for (; ; ) {
202
+ if (signal?.aborted) throw new Error("TN_NET_CANCELLED: aborted");
203
+ const decoded = readStreamFrame(this.#buffer, this.#limit);
204
+ if (decoded !== void 0) {
205
+ this.#buffer = decoded.rest.length === 0 ? new Uint8Array(0) : decoded.rest;
206
+ return decoded.frame;
207
+ }
208
+ if (this.#buffer.length > this.#limit + HEADER_BYTES)
209
+ throw new Error("TN_NET_PROTOCOL: frame exceeds limit");
210
+ const abort = new Promise((_, reject) => {
211
+ signal?.addEventListener("abort", () => reject(new Error("TN_NET_CANCELLED: aborted")), {
212
+ once: true
213
+ });
214
+ });
215
+ const read = signal === void 0 ? await this.#reader.read() : await Promise.race([this.#reader.read(), abort]);
216
+ if (read.done) throw new Error("TN_NET_PROTOCOL: stream ended");
217
+ const chunk = asBytes(read.value);
218
+ this.#buffer = this.#buffer.length === 0 ? chunk : concatBytes(this.#buffer, chunk);
219
+ }
220
+ }
221
+ release() {
222
+ if (this.#released) return;
223
+ this.#released = true;
224
+ try {
225
+ this.#reader.releaseLock();
226
+ } catch {
227
+ }
228
+ }
229
+ cancel(reason) {
230
+ try {
231
+ return this.#reader.cancel(reason);
232
+ } catch {
233
+ return Promise.resolve();
234
+ }
235
+ }
236
+ };
237
+
238
+ // src/net-session.ts
239
+ async function openConnection(url, normalized) {
240
+ if (normalized.signal?.aborted) throw new Error("TN_NET_CANCELLED: aborted before connect");
241
+ const Constructor = webTransportConstructor();
242
+ if (Constructor === void 0) throw new Error("TN_NET_UNAVAILABLE: WebTransport is missing");
243
+ const transport = new Constructor(url, {});
244
+ let timeoutId;
245
+ const onAbort = () => {
246
+ try {
247
+ transport.close();
248
+ } catch {
249
+ }
250
+ };
251
+ normalized.signal?.addEventListener("abort", onAbort, { once: true });
252
+ const deadline = new Promise((_, reject) => {
253
+ timeoutId = setTimeout(
254
+ () => reject(new Error("TN_NET_TIMEOUT: connect timed out")),
255
+ normalized.connectTimeoutMs
256
+ );
257
+ });
258
+ const cancelled = new Promise((_, reject) => {
259
+ if (normalized.signal === void 0) return;
260
+ if (normalized.signal.aborted) reject(new Error("TN_NET_CANCELLED: aborted"));
261
+ else
262
+ normalized.signal.addEventListener(
263
+ "abort",
264
+ () => reject(new Error("TN_NET_CANCELLED: aborted")),
265
+ { once: true }
266
+ );
267
+ });
268
+ try {
269
+ await Promise.race([Promise.resolve(transport.ready), deadline, cancelled]);
270
+ } finally {
271
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
272
+ normalized.signal?.removeEventListener("abort", onAbort);
273
+ }
274
+ if (normalized.signal?.aborted) throw new Error("TN_NET_CANCELLED: aborted");
275
+ return establishSession(transport, normalized);
276
+ }
277
+ async function establishSession(transport, normalized) {
278
+ const abort = new AbortController();
279
+ const timer = setTimeout(() => abort.abort(), normalized.connectTimeoutMs);
280
+ const readers = [];
281
+ const writers = [];
282
+ try {
283
+ const controlStream = await Promise.resolve(transport.createBidirectionalStream());
284
+ writers.push(controlStream.writable);
285
+ const controlReader = new FrameReader(controlStream.readable, MAX_HELLO_BYTES);
286
+ readers.push(controlReader);
287
+ const helloPayload = new TextEncoder().encode(
288
+ JSON.stringify({
289
+ applicationProtocol: normalized.applicationProtocol,
290
+ credential: normalized.credential,
291
+ channels: normalized.channels.map((channel) => ({
292
+ id: channel.id,
293
+ delivery: channel.delivery
294
+ })),
295
+ maxReliableMessageBytes: normalized.maxReliableMessageBytes,
296
+ maxQueuedReliableBytes: normalized.maxQueuedReliableBytes,
297
+ maxQueuedDatagrams: normalized.maxQueuedDatagrams
298
+ })
299
+ );
300
+ if (helloPayload.length > MAX_HELLO_BYTES)
301
+ throw invalidArgument("handshake payload exceeds 4096 bytes");
302
+ await writeAll(controlStream.writable, encodeFrame(KIND_HELLO, 0, helloPayload));
303
+ const welcomeFrame = await controlReader.nextFrame(abort.signal);
304
+ if (welcomeFrame.kind !== KIND_WELCOME || welcomeFrame.channel !== 0)
305
+ throw new Error("TN_NET_PROTOCOL: expected WELCOME");
306
+ const welcome = parseWelcome(welcomeFrame.payload, normalized.applicationProtocol);
307
+ const expected = [...normalized.channels].sort((left, right) => left.id - right.id);
308
+ const offered = [...welcome.channels].sort((left, right) => left.id - right.id);
309
+ if (JSON.stringify(expected) !== JSON.stringify(offered))
310
+ throw new Error("TN_NET_PROTOCOL: WELCOME channels mismatch");
311
+ const messageLimit = Math.min(
312
+ normalized.maxReliableMessageBytes,
313
+ welcome.maxReliableMessageBytes
314
+ );
315
+ const queueLimit = Math.min(normalized.maxQueuedReliableBytes, welcome.maxQueuedReliableBytes);
316
+ const datagramLimit = Math.min(normalized.maxQueuedDatagrams, welcome.maxQueuedDatagrams);
317
+ if (messageLimit > queueLimit) throw new Error("TN_NET_PROTOCOL: invalid negotiated limits");
318
+ const maxDatagramSize = transport.datagrams.maxDatagramSize;
319
+ const maxDatagramPayload = typeof maxDatagramSize === "number" && Number.isFinite(maxDatagramSize) ? maxDatagramSize - HEADER_BYTES : Number.NaN;
320
+ if (!Number.isSafeInteger(maxDatagramPayload) || maxDatagramPayload <= 0)
321
+ throw new Error("TN_NET_UNAVAILABLE: datagram support is missing");
322
+ const reliableIds = expected.filter((channel) => channel.delivery === "reliable-ordered").map((channel) => channel.id);
323
+ const reliableReaders = /* @__PURE__ */ new Map();
324
+ const reliableWriters = /* @__PURE__ */ new Map();
325
+ for (const channelId of reliableIds) {
326
+ const stream = await Promise.resolve(transport.createBidirectionalStream());
327
+ writers.push(stream.writable);
328
+ const reader = new FrameReader(stream.readable, messageLimit);
329
+ readers.push(reader);
330
+ await writeAll(stream.writable, encodeFrame(KIND_BIND, channelId, new Uint8Array(0)));
331
+ const bound = await reader.nextFrame(abort.signal);
332
+ if (bound.kind !== KIND_BOUND || bound.channel !== channelId)
333
+ throw new Error("TN_NET_PROTOCOL: expected BOUND");
334
+ if (bound.payload.length !== 0) throw new Error("TN_NET_PROTOCOL: BOUND carries payload");
335
+ reliableReaders.set(channelId, reader);
336
+ reliableWriters.set(channelId, stream.writable);
337
+ }
338
+ clearTimeout(timer);
339
+ return createConnection({
340
+ transport,
341
+ channelById: new Map(normalized.channels.map((channel) => [channel.id, channel.delivery])),
342
+ sessionId: welcome.sessionId,
343
+ maxDatagramPayload,
344
+ maxReliableMessageBytes: messageLimit,
345
+ maxQueuedReliableBytes: queueLimit,
346
+ maxQueuedDatagrams: datagramLimit,
347
+ readers,
348
+ reliableReaders,
349
+ reliableWriters
350
+ });
351
+ } catch (error) {
352
+ clearTimeout(timer);
353
+ abort.abort();
354
+ for (const reader of readers) {
355
+ await reader.cancel(error);
356
+ reader.release();
357
+ }
358
+ try {
359
+ transport.close();
360
+ } catch {
361
+ }
362
+ if (normalized.signal?.aborted) throw new Error("TN_NET_CANCELLED: aborted");
363
+ if (error instanceof Error && error.message.startsWith("TN_NET_")) throw error;
364
+ if (error instanceof Error) throw new Error(`TN_NET_PROTOCOL: ${error.message}`);
365
+ throw new Error("TN_NET_UNAVAILABLE: handshake failed");
366
+ }
367
+ }
368
+ function createConnection(established) {
369
+ let state = "connected";
370
+ let disconnectReason = null;
371
+ let disconnectReported = false;
372
+ let closePromise;
373
+ let releasePromise;
374
+ let droppedDatagrams = 0;
375
+ let queuedReliableBytes = 0;
376
+ let queuedReliableBytesReceived = 0;
377
+ const pendingReliable = /* @__PURE__ */ new Map();
378
+ const pendingDatagrams = [];
379
+ const receiveQueue = [];
380
+ let flushScheduled = false;
381
+ for (const channelId of established.reliableReaders.keys()) pendingReliable.set(channelId, []);
382
+ const statsBase = {
383
+ transport: "webtransport",
384
+ sessionId: established.sessionId,
385
+ maxDatagramPayload: established.maxDatagramPayload,
386
+ maxReliableMessageBytes: established.maxReliableMessageBytes,
387
+ maxQueuedReliableBytes: established.maxQueuedReliableBytes,
388
+ maxQueuedDatagrams: established.maxQueuedDatagrams
389
+ };
390
+ const markDisconnected = (reason) => {
391
+ if (state === "closed") return;
392
+ state = "closed";
393
+ disconnectReason = reason;
394
+ void releaseAll();
395
+ };
396
+ const closedReason = (result) => {
397
+ if (typeof result === "object" && result !== null && "reason" in result) {
398
+ const reason = result.reason;
399
+ if (reason === null || reason === void 0) return null;
400
+ if (typeof reason === "string" && reason.length > 0) return reason;
401
+ }
402
+ return "transport closed";
403
+ };
404
+ void Promise.resolve(established.transport.closed).then(
405
+ (result) => markDisconnected(closedReason(result)),
406
+ () => markDisconnected("transport error")
407
+ );
408
+ const enqueueReceived = (channel, data) => {
409
+ const delivery = established.channelById.get(channel);
410
+ if (delivery === "reliable-ordered") {
411
+ if (queuedReliableBytesReceived + data.length > established.maxQueuedReliableBytes) return;
412
+ queuedReliableBytesReceived += data.length;
413
+ receiveQueue.push({ channel, data });
414
+ return;
415
+ }
416
+ if (delivery !== "unreliable") {
417
+ droppedDatagrams += 1;
418
+ return;
419
+ }
420
+ const unreliableCount = receiveQueue.filter(
421
+ (message) => established.channelById.get(message.channel) === "unreliable"
422
+ ).length;
423
+ if (unreliableCount >= established.maxQueuedDatagrams) {
424
+ const index = receiveQueue.findIndex(
425
+ (message) => established.channelById.get(message.channel) === "unreliable"
426
+ );
427
+ if (index >= 0) receiveQueue.splice(index, 1);
428
+ droppedDatagrams += 1;
429
+ }
430
+ receiveQueue.push({ channel, data });
431
+ };
432
+ const pumpReliable = (channelId, reader) => {
433
+ void (async () => {
434
+ try {
435
+ for (; ; ) {
436
+ if (state !== "connected") return;
437
+ if (queuedReliableBytesReceived >= established.maxQueuedReliableBytes) {
438
+ await new Promise((resolve) => setTimeout(resolve, 5));
439
+ continue;
440
+ }
441
+ const frame = await reader.nextFrame();
442
+ if (frame.kind !== KIND_DATA || frame.channel !== channelId)
443
+ throw new Error("TN_NET_PROTOCOL: wrong stream frame");
444
+ enqueueReceived(frame.channel, frame.payload.slice());
445
+ }
446
+ } catch {
447
+ if (state === "connected") markDisconnected("transport error");
448
+ }
449
+ })();
450
+ };
451
+ let datagramReader;
452
+ const pumpDatagrams = () => {
453
+ const reader = established.transport.datagrams.readable.getReader();
454
+ datagramReader = reader;
455
+ void (async () => {
456
+ try {
457
+ for (; ; ) {
458
+ if (state !== "connected") {
459
+ try {
460
+ reader.releaseLock();
461
+ } catch {
462
+ }
463
+ datagramReader = void 0;
464
+ return;
465
+ }
466
+ const read = await reader.read();
467
+ if (read.done) {
468
+ markDisconnected("transport closed");
469
+ return;
470
+ }
471
+ let frame;
472
+ try {
473
+ frame = decodeStandaloneFrame(asBytes(read.value).slice());
474
+ } catch {
475
+ droppedDatagrams += 1;
476
+ continue;
477
+ }
478
+ if (frame.kind !== KIND_DATA || established.channelById.get(frame.channel) !== "unreliable") {
479
+ droppedDatagrams += 1;
480
+ continue;
481
+ }
482
+ enqueueReceived(frame.channel, frame.payload.slice());
483
+ }
484
+ } catch {
485
+ if (state === "connected") markDisconnected("transport error");
486
+ }
487
+ })();
488
+ };
489
+ for (const [channelId, reader] of established.reliableReaders) pumpReliable(channelId, reader);
490
+ pumpDatagrams();
491
+ const flushQueues = () => {
492
+ if (flushScheduled) return;
493
+ flushScheduled = true;
494
+ void Promise.resolve().then(async () => {
495
+ try {
496
+ if (state !== "connected") return;
497
+ while (pendingDatagrams.length > 0) {
498
+ if (state !== "connected") return;
499
+ const next = pendingDatagrams.shift();
500
+ if (next === void 0) return;
501
+ try {
502
+ await writeAll(
503
+ established.transport.datagrams.writable,
504
+ encodeFrame(KIND_DATA, next.channel, next.bytes)
505
+ );
506
+ } catch {
507
+ markDisconnected("transport error");
508
+ return;
509
+ }
510
+ }
511
+ for (const [channelId, queue] of pendingReliable) {
512
+ while (queue.length > 0) {
513
+ if (state !== "connected") return;
514
+ const writer = established.reliableWriters.get(channelId);
515
+ const next = queue.shift();
516
+ if (writer === void 0 || next === void 0) break;
517
+ try {
518
+ await writeAll(writer, encodeFrame(KIND_DATA, channelId, next));
519
+ queuedReliableBytes -= next.length;
520
+ } catch {
521
+ markDisconnected("transport error");
522
+ return;
523
+ }
524
+ }
525
+ }
526
+ } finally {
527
+ flushScheduled = false;
528
+ if (state === "connected" && (pendingDatagrams.length > 0 || [...pendingReliable.values()].some((queue) => queue.length > 0)))
529
+ flushQueues();
530
+ }
531
+ });
532
+ };
533
+ async function releaseAll() {
534
+ if (releasePromise !== void 0) return releasePromise;
535
+ releasePromise = (async () => {
536
+ for (const reader of established.readers) {
537
+ try {
538
+ await reader.cancel(new Error("TN_NET_CLOSED: connection closed"));
539
+ } catch {
540
+ }
541
+ reader.release();
542
+ }
543
+ if (datagramReader !== void 0) {
544
+ try {
545
+ await datagramReader.cancel(new Error("TN_NET_CLOSED: connection closed"));
546
+ } catch {
547
+ }
548
+ try {
549
+ datagramReader.releaseLock();
550
+ } catch {
551
+ }
552
+ datagramReader = void 0;
553
+ } else {
554
+ try {
555
+ await established.transport.datagrams.readable.cancel(
556
+ new Error("TN_NET_CLOSED: connection closed")
557
+ );
558
+ } catch {
559
+ }
560
+ }
561
+ try {
562
+ established.transport.close();
563
+ } catch {
564
+ }
565
+ })();
566
+ return releasePromise;
567
+ }
568
+ return {
569
+ send(channel, data) {
570
+ const delivery = established.channelById.get(channel);
571
+ if (delivery === void 0) throw new Error("TN_NET_INVALID_ARGUMENT: unknown channel");
572
+ if (!(data instanceof Uint8Array))
573
+ throw new Error("TN_NET_INVALID_ARGUMENT: data must be bytes");
574
+ if (state !== "connected") throw new Error("TN_NET_CLOSED: connection is closed");
575
+ const bytes = Uint8Array.from(data);
576
+ if (delivery === "reliable-ordered") {
577
+ if (bytes.length > established.maxReliableMessageBytes)
578
+ throw new Error("TN_NET_MESSAGE_TOO_LARGE: reliable message exceeds limit");
579
+ if (queuedReliableBytes + bytes.length > established.maxQueuedReliableBytes) return false;
580
+ pendingReliable.get(channel)?.push(bytes);
581
+ queuedReliableBytes += bytes.length;
582
+ flushQueues();
583
+ return true;
584
+ }
585
+ const payloadLimit = established.maxDatagramPayload;
586
+ if (bytes.length > payloadLimit)
587
+ throw new Error("TN_NET_MESSAGE_TOO_LARGE: datagram exceeds limit");
588
+ if (pendingDatagrams.length >= established.maxQueuedDatagrams) {
589
+ pendingDatagrams.shift();
590
+ droppedDatagrams += 1;
591
+ }
592
+ pendingDatagrams.push({ channel, bytes });
593
+ flushQueues();
594
+ return true;
595
+ },
596
+ poll() {
597
+ const room = established.maxQueuedDatagrams * 2;
598
+ const messages = receiveQueue.splice(0, Math.max(0, room));
599
+ queuedReliableBytesReceived = receiveQueue.filter((message) => established.channelById.get(message.channel) === "reliable-ordered").reduce((sum, message) => sum + message.data.length, 0);
600
+ if (state === "closed" && !disconnectReported) {
601
+ disconnectReported = true;
602
+ return { messages, disconnected: true, reason: disconnectReason };
603
+ }
604
+ return { messages, disconnected: false, reason: null };
605
+ },
606
+ getStats() {
607
+ return {
608
+ ...statsBase,
609
+ state,
610
+ queuedReliableBytes,
611
+ queuedDatagrams: pendingDatagrams.length,
612
+ droppedDatagrams,
613
+ rttMs: null
614
+ };
615
+ },
616
+ close() {
617
+ if (closePromise !== void 0) return closePromise;
618
+ if (state === "connected") state = "closing";
619
+ closePromise = (async () => {
620
+ receiveQueue.length = 0;
621
+ pendingDatagrams.length = 0;
622
+ for (const queue of pendingReliable.values()) queue.length = 0;
623
+ queuedReliableBytes = 0;
624
+ queuedReliableBytesReceived = 0;
625
+ await releaseAll();
626
+ state = "closed";
627
+ })();
628
+ return closePromise;
629
+ }
630
+ };
631
+ }
632
+
633
+ // src/net.ts
634
+ function connect(url, options) {
635
+ try {
636
+ const normalized = normalizeOptions(url, options);
637
+ return openConnection(url, normalized);
638
+ } catch (error) {
639
+ return Promise.reject(error);
640
+ }
641
+ }
642
+
643
+ export { connect };
@@ -1,8 +1,10 @@
1
- import { JsonValue } from '@threenative/playtest/protocol';
2
- import { b as IGamePluginHooks } from './game-CYIaKhgl.js';
1
+ import { JsonValue, JsonPrimitive } from '@threenative/playtest/protocol';
2
+ import { b as IGamePluginHooks } from './game-XGrTzapq.js';
3
+ import './assets-kyoF7JlJ.js';
3
4
  import 'three';
4
- import './canvas-layer-CtrZHgIh.js';
5
+ import './renderer-C6hqZpoG.js';
5
6
  import 'three/webgpu';
7
+ import './canvas-layer-BLVijiUJ.js';
6
8
  import 'zustand/vanilla';
7
9
 
8
10
  /**
@@ -32,11 +34,33 @@ declare const PLAYTEST_RUNNER_EXPECTED_GLOBAL = "__THREENATIVE_PLAYTEST_RUNNER_E
32
34
  interface IPlaytestOptions {
33
35
  readonly events?: () => JsonValue[];
34
36
  /**
35
- * Hold the frame loop until a runner attaches. Default false.
37
+ * Hold start-scene entry until a runner attaches. Defaults on for an announced runner.
36
38
  */
37
39
  readonly holdUntilAttached?: boolean;
38
40
  /** Milliseconds to wait when `holdUntilAttached` is set. Default {@link PLAYTEST_ATTACH_TIMEOUT_MS}. */
39
41
  readonly attachTimeoutMs?: number;
40
42
  }
43
+ /**
44
+ * When a published value changed, and to what — the other half of "because".
45
+ *
46
+ * A run has always been able to say *a contact happened* and *the state reads `won`*. Nothing
47
+ * related the two, and a scenario could only compare at step boundaries, where a win arriving one
48
+ * tick after the contact and a win arriving 199 ticks later inside the same step are the same
49
+ * observation. That is exactly the shape of a game whose terminal state comes from a timer or a
50
+ * distance check and merely happens to land near a contact.
51
+ *
52
+ * The watcher diffs the published surface once per tick and records every change with the tick it
53
+ * happened on. It watches what a scenario can name: each registered entity's `state`, and the
54
+ * top-level primitive fields of the game's own published state. A deeper path is not watched, and
55
+ * an assertion about one fails closed rather than being answered from a value nobody diffed.
56
+ */
57
+ declare const PLAYTEST_TRANSITION_LOG_LIMIT = 512;
58
+ interface IPlaytestTransition {
59
+ from: JsonPrimitive;
60
+ /** `states.<entity>` for a registered entity, or `state.<field>` for a published field. */
61
+ path: string;
62
+ tick: number;
63
+ to: JsonPrimitive;
64
+ }
41
65
 
42
- export { type IPlaytestOptions, PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, playtest };
66
+ export { type IPlaytestOptions, type IPlaytestTransition, PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, PLAYTEST_TRANSITION_LOG_LIMIT, playtest };