@threenative/core 0.3.2 → 0.3.4

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.
@@ -1,10 +1,10 @@
1
1
  import { JsonValue, JsonPrimitive } from '@threenative/playtest/protocol';
2
- import { b as IGamePluginHooks } from './game-XGrTzapq.js';
3
- import './assets-kyoF7JlJ.js';
2
+ import { b as IGamePluginHooks } from './game-CljaDv4D.js';
3
+ import './assets-CqvE429w.js';
4
4
  import 'three';
5
- import './renderer-C6hqZpoG.js';
5
+ import './renderer-CfsS2hxi.js';
6
6
  import 'three/webgpu';
7
- import './canvas-layer-BLVijiUJ.js';
7
+ import './canvas-layer-DDmC_VVF.js';
8
8
  import 'zustand/vanilla';
9
9
 
10
10
  /**
@@ -31,6 +31,12 @@ declare const PLAYTEST_ATTACH_TIMEOUT_MS = 30000;
31
31
  */
32
32
  /** The global a playtest runner sets before the page loads, so a game can tell one is coming. */
33
33
  declare const PLAYTEST_RUNNER_EXPECTED_GLOBAL = "__THREENATIVE_PLAYTEST_RUNNER_EXPECTED__";
34
+ /**
35
+ * The clock a consumer asks this game to run on, named in the protocol's own vocabulary. Set before
36
+ * the game module evaluates — a production profile does it in the instrumentation it injects ahead
37
+ * of the bundle, on the browser and on native alike.
38
+ */
39
+ declare const PLAYTEST_CLOCK_GLOBAL = "__THREENATIVE_PLAYTEST_CLOCK__";
34
40
  interface IPlaytestOptions {
35
41
  readonly events?: () => JsonValue[];
36
42
  /**
@@ -63,4 +69,4 @@ interface IPlaytestTransition {
63
69
  to: JsonPrimitive;
64
70
  }
65
71
 
66
- export { type IPlaytestOptions, type IPlaytestTransition, PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, PLAYTEST_TRANSITION_LOG_LIMIT, playtest };
72
+ export { type IPlaytestOptions, type IPlaytestTransition, PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_CLOCK_GLOBAL, PLAYTEST_RUNNER_EXPECTED_GLOBAL, PLAYTEST_TRANSITION_LOG_LIMIT, playtest };
package/dist/playtest.js CHANGED
@@ -1,27 +1,48 @@
1
1
  import { assertJsonSafe, PLAYTEST_PROTOCOL_LIMITS } from '@threenative/playtest/protocol';
2
2
  import { installThreePlaytestBridge } from '@threenative/playtest/three';
3
- import { Object3D } from 'three';
3
+ import { Sphere, Vector3, Object3D } from 'three';
4
4
 
5
5
  // src/playtest.ts
6
- var buses = /* @__PURE__ */ new Set();
6
+ var AUDIO_STATE = /* @__PURE__ */ Symbol.for("threenative.audio.runtime");
7
+ function audioState() {
8
+ const host = globalThis;
9
+ const existing = host[AUDIO_STATE];
10
+ if (existing !== void 0) return existing;
11
+ const created = { buses: /* @__PURE__ */ new Set(), cueCounts: /* @__PURE__ */ new Map(), cueLog: [] };
12
+ host[AUDIO_STATE] = created;
13
+ return created;
14
+ }
7
15
  function audioRuntimeSnapshot() {
8
16
  let queued = 0;
9
17
  let voices = 0;
10
18
  let pooled = 0;
11
19
  let paused = 0;
12
20
  const unsupported = /* @__PURE__ */ new Set();
13
- for (const bus of buses) {
21
+ const state = audioState();
22
+ for (const bus of state.buses) {
14
23
  queued += bus.queued;
15
24
  voices += bus.voices;
16
25
  pooled += bus.pooled;
17
26
  paused += bus.pausedVoices;
18
27
  for (const option of bus.unsupported) unsupported.add(option);
19
28
  }
20
- return { paused, pooled, queued, unsupported: [...unsupported].sort(), voices };
29
+ return {
30
+ cues: Object.fromEntries(state.cueCounts),
31
+ paused,
32
+ pooled,
33
+ queued,
34
+ recentCues: state.cueLog.map((entry) => ({ ...entry })),
35
+ unsupported: [...unsupported].sort(),
36
+ voices
37
+ };
21
38
  }
39
+ var GEOMETRY_CAPTURE_CAPABILITY = "runtime.geometry";
40
+ new Sphere();
41
+ new Vector3();
42
+ new Vector3();
22
43
 
23
44
  // src/version.ts
24
- var CORE_VERSION = "0.3.2";
45
+ var CORE_VERSION = "0.3.4";
25
46
 
26
47
  // src/pipeline-census.ts
27
48
  var PIPELINE_CENSUS_CAPABILITY = "runtime.pipelineCensus";
@@ -53,6 +74,7 @@ function playtest(options = {}) {
53
74
  let attached;
54
75
  let startSceneEntered;
55
76
  let disposePipelineCensus;
77
+ let disposeGeometryCapture;
56
78
  let contactHistory = [];
57
79
  const watcher = createTransitionWatcher();
58
80
  let readTick;
@@ -70,6 +92,8 @@ function playtest(options = {}) {
70
92
  setup: async (ctx, runtime) => {
71
93
  readTick = runtime?.tick;
72
94
  const seed = runtime?.seed ?? null;
95
+ const clockMode = requestedClockMode();
96
+ const wallClock = clockMode === "wall-clock";
73
97
  const replayRuntime = runtime?.seed === null || runtime?.seed === void 0 ? void 0 : {
74
98
  agent: currentAgent,
75
99
  core: CORE_VERSION,
@@ -79,9 +103,15 @@ function playtest(options = {}) {
79
103
  };
80
104
  const installation = installThreePlaytestBridge({
81
105
  camera: ctx.camera,
106
+ ...clockMode === void 0 ? {} : { clockMode },
82
107
  entities: () => bridgeEntities(ctx),
83
108
  components: () => componentObservations(ctx.entities.snapshot()),
84
- ...runtime === void 0 ? {} : { fixedStep: runtime.fixedStep, tick: runtime.tick },
109
+ // The live clock has to be driven by the host's own frame pump, so a live run's `advance`
110
+ // waits the span of wall time the count names and reports the ticks that pump ran.
111
+ ...runtime === void 0 ? {} : {
112
+ fixedStep: wallClock ? wallClockAdvance(runtime) : runtime.fixedStep,
113
+ tick: runtime.tick
114
+ },
85
115
  ...runtime?.runtimeDiagnosticsSeries === void 0 ? {} : { runtimeDiagnosticsSeries: runtime.runtimeDiagnosticsSeries },
86
116
  ...options.events === void 0 ? {} : { events: options.events },
87
117
  gameplay: () => gameplayObservations(ctx, contactHistory, seed, replayRuntime, watcher),
@@ -122,14 +152,27 @@ function playtest(options = {}) {
122
152
  sample: () => ({ pipelineCensus: pipelineCensus() })
123
153
  });
124
154
  }
155
+ const geometryCapture = runtime?.geometryCapture;
156
+ if (runtime !== void 0 && geometryCapture !== void 0) {
157
+ disposeGeometryCapture = runtime.observations.contribute({
158
+ capabilities: [GEOMETRY_CAPTURE_CAPABILITY],
159
+ // Nothing is armed, walked or hooked unless this request asked for a capture: the
160
+ // capability says the runtime can answer, not that every sample pays for one.
161
+ sample: async (request) => request.geometry === void 0 ? {} : { geometry: await geometryCapture(request.geometry) }
162
+ });
163
+ }
125
164
  installRuntimeChannels(installation.bridge, runtime);
126
- if (globalThis[PLAYTEST_RUNNER_EXPECTED_GLOBAL] === true)
165
+ if (runnerAnnounced()) {
127
166
  runtime?.enableRuntimeDiagnostics?.();
167
+ if (!wallClock) runtime?.freezeClock?.();
168
+ }
128
169
  dispose = installation.dispose;
129
170
  attached = holdUntilAttached(installation.bridge, options, () => startSceneEntered);
130
171
  const cleanup = () => {
131
172
  disposePipelineCensus?.();
132
173
  disposePipelineCensus = void 0;
174
+ disposeGeometryCapture?.();
175
+ disposeGeometryCapture = void 0;
133
176
  dispose?.();
134
177
  dispose = void 0;
135
178
  attached = void 0;
@@ -155,8 +198,32 @@ function playtest(options = {}) {
155
198
  }
156
199
  var PLAYTEST_ATTACH_TIMEOUT_MS = 3e4;
157
200
  var PLAYTEST_RUNNER_EXPECTED_GLOBAL = "__THREENATIVE_PLAYTEST_RUNNER_EXPECTED__";
201
+ var PLAYTEST_CLOCK_GLOBAL = "__THREENATIVE_PLAYTEST_CLOCK__";
202
+ function requestedClockMode() {
203
+ const requested = globalThis[PLAYTEST_CLOCK_GLOBAL];
204
+ if (requested === void 0) return void 0;
205
+ if (requested === "wall-clock") return "wall-clock";
206
+ throw new Error(
207
+ `TN_PLAYTEST_CLOCK_UNSUPPORTED: '${PLAYTEST_CLOCK_GLOBAL}' is ${JSON.stringify(requested)}; this protocol knows 'wall-clock' and nothing else.`
208
+ );
209
+ }
210
+ var WALL_CLOCK_PUMP_STEPS = 4;
211
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
212
+ function wallClockAdvance(runtime) {
213
+ const stepMs = runtime.step * 1e3;
214
+ return async (ticks) => {
215
+ const before = runtime.tick();
216
+ await sleep(ticks * stepMs);
217
+ for (let waited = 0; waited < WALL_CLOCK_PUMP_STEPS && runtime.tick() === before; waited += 1)
218
+ await sleep(stepMs / 2);
219
+ return runtime.tick() - before;
220
+ };
221
+ }
158
222
  function shouldHoldUntilAttached(options) {
159
223
  if (options.holdUntilAttached !== void 0) return options.holdUntilAttached;
224
+ return runnerAnnounced();
225
+ }
226
+ function runnerAnnounced() {
160
227
  const host = globalThis;
161
228
  return host[PLAYTEST_RUNNER_EXPECTED_GLOBAL] === true || host.TN_PLAYTEST_ENDPOINT !== void 0;
162
229
  }
@@ -227,10 +294,10 @@ function addRuntimeCapabilities(description, contributions) {
227
294
  }
228
295
  return { ...description, capabilities };
229
296
  }
230
- function addRuntimeObservations(snapshot, request, contributions) {
297
+ async function addRuntimeObservations(snapshot, request, contributions) {
231
298
  const result = { ...snapshot };
232
299
  for (const contribution of contributions) {
233
- const slice = contribution.sample(request);
300
+ const slice = await contribution.sample(request);
234
301
  if (typeof slice !== "object" || slice === null || Array.isArray(slice)) {
235
302
  throw new TypeError("A runtime observation contribution must return a top-level object.");
236
303
  }
@@ -434,11 +501,29 @@ function stateResources(ctx) {
434
501
  // `state` is canonical. Keep `GameState` as a compatibility alias until published
435
502
  // scenarios have migrated, then remove the alias in a future breaking release.
436
503
  ["state", value],
437
- ["GameState", value]
504
+ ["GameState", value],
505
+ // Where every asset was actually served from, on every target and not only in a browser
506
+ // console log: a game whose manifest never loaded runs on the uncompiled source directory
507
+ // and looks healthy, and without this nothing observable says so.
508
+ ["assets", assetResolutions(ctx.assets)]
438
509
  ]);
439
510
  }
440
511
  };
441
512
  }
513
+ function assetResolutions(assets) {
514
+ const nested = {};
515
+ for (const [logical, record] of assets.resolved) {
516
+ const parts = logical.split(".");
517
+ let node = nested;
518
+ for (const part of parts.slice(0, -1)) {
519
+ const child = node[part] ?? {};
520
+ node[part] = child;
521
+ node = child;
522
+ }
523
+ node[parts.at(-1) ?? logical] = { url: record.url, via: record.via };
524
+ }
525
+ return nested;
526
+ }
442
527
  function cloneJsonValue(value) {
443
528
  if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
444
529
  if (typeof value === "object" && value !== null) {
@@ -449,4 +534,4 @@ function cloneJsonValue(value) {
449
534
  return value;
450
535
  }
451
536
 
452
- export { PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_RUNNER_EXPECTED_GLOBAL, PLAYTEST_TRANSITION_LOG_LIMIT, playtest };
537
+ export { PLAYTEST_ATTACH_TIMEOUT_MS, PLAYTEST_CLOCK_GLOBAL, PLAYTEST_RUNNER_EXPECTED_GLOBAL, PLAYTEST_TRANSITION_LOG_LIMIT, playtest };
package/dist/react.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
- import { C as CanvasLayer } from './canvas-layer-BLVijiUJ.js';
2
+ import { C as CanvasLayer } from './canvas-layer-DDmC_VVF.js';
3
3
  import 'three';
4
- import './renderer-C6hqZpoG.js';
4
+ import './renderer-CfsS2hxi.js';
5
5
  import 'three/webgpu';
6
6
 
7
7
  /**