@aethervtt/shard-procgen 0.0.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.
@@ -0,0 +1,421 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import {
3
+ box,
4
+ capsule,
5
+ cone,
6
+ cube,
7
+ cylinder,
8
+ plane,
9
+ sphere,
10
+ torus
11
+ } from "@aethervtt/shard-mesh";
12
+ function icosphere(detail) {
13
+ if (!Number.isInteger(detail) || detail < 0 || detail > 8) {
14
+ throw new ShardError("procgen/bad-mesh", `icosphere detail must be 0 to 8, got ${detail}`, {
15
+ hint: "Detail 6 is already 40 962 vertices."
16
+ });
17
+ }
18
+ const phi = (1 + Math.sqrt(5)) / 2;
19
+ const base = [
20
+ -1,
21
+ phi,
22
+ 0,
23
+ 1,
24
+ phi,
25
+ 0,
26
+ -1,
27
+ -phi,
28
+ 0,
29
+ 1,
30
+ -phi,
31
+ 0,
32
+ 0,
33
+ -1,
34
+ phi,
35
+ 0,
36
+ 1,
37
+ phi,
38
+ 0,
39
+ -1,
40
+ -phi,
41
+ 0,
42
+ 1,
43
+ -phi,
44
+ phi,
45
+ 0,
46
+ -1,
47
+ phi,
48
+ 0,
49
+ 1,
50
+ -phi,
51
+ 0,
52
+ -1,
53
+ -phi,
54
+ 0,
55
+ 1
56
+ ];
57
+ let faces = [
58
+ 0,
59
+ 11,
60
+ 5,
61
+ 0,
62
+ 5,
63
+ 1,
64
+ 0,
65
+ 1,
66
+ 7,
67
+ 0,
68
+ 7,
69
+ 10,
70
+ 0,
71
+ 10,
72
+ 11,
73
+ 1,
74
+ 5,
75
+ 9,
76
+ 5,
77
+ 11,
78
+ 4,
79
+ 11,
80
+ 10,
81
+ 2,
82
+ 10,
83
+ 7,
84
+ 6,
85
+ 7,
86
+ 1,
87
+ 8,
88
+ 3,
89
+ 9,
90
+ 4,
91
+ 3,
92
+ 4,
93
+ 2,
94
+ 3,
95
+ 2,
96
+ 6,
97
+ 3,
98
+ 6,
99
+ 8,
100
+ 3,
101
+ 8,
102
+ 9,
103
+ 4,
104
+ 9,
105
+ 5,
106
+ 2,
107
+ 4,
108
+ 11,
109
+ 6,
110
+ 2,
111
+ 10,
112
+ 8,
113
+ 6,
114
+ 7,
115
+ 9,
116
+ 8,
117
+ 1
118
+ ];
119
+ const vertexCount = 10 * 4 ** detail + 2;
120
+ const pos = new Float64Array(vertexCount * 3);
121
+ let count = 0;
122
+ const add = (x, y, z) => {
123
+ const inv = 1 / Math.sqrt(x * x + y * y + z * z);
124
+ pos[count * 3] = x * inv;
125
+ pos[count * 3 + 1] = y * inv;
126
+ pos[count * 3 + 2] = z * inv;
127
+ return count++;
128
+ };
129
+ for (let i = 0; i < 12; i++) add(base[i * 3], base[i * 3 + 1], base[i * 3 + 2]);
130
+ for (let level = 0; level < detail; level++) {
131
+ const midpoints = /* @__PURE__ */ new Map();
132
+ const mid = (a, b) => {
133
+ const key = a < b ? a * vertexCount + b : b * vertexCount + a;
134
+ let m = midpoints.get(key);
135
+ if (m === void 0) {
136
+ m = add(
137
+ pos[a * 3] + pos[b * 3],
138
+ pos[a * 3 + 1] + pos[b * 3 + 1],
139
+ pos[a * 3 + 2] + pos[b * 3 + 2]
140
+ );
141
+ midpoints.set(key, m);
142
+ }
143
+ return m;
144
+ };
145
+ const next = [];
146
+ for (let f = 0; f < faces.length; f += 3) {
147
+ const a = faces[f];
148
+ const b = faces[f + 1];
149
+ const c = faces[f + 2];
150
+ const ab = mid(a, b);
151
+ const bc = mid(b, c);
152
+ const ca = mid(c, a);
153
+ next.push(a, ab, ca, b, bc, ab, c, ca, bc, ab, bc, ca);
154
+ }
155
+ faces = next;
156
+ }
157
+ const positions = new Float32Array(pos);
158
+ const indices = count > 65535 ? new Uint32Array(faces) : new Uint16Array(faces);
159
+ return { positions, normals: positions.slice(), indices };
160
+ }
161
+ const PRIMITIVES = {
162
+ box: (p) => box(p),
163
+ cube: (p) => cube(p),
164
+ sphere: (p) => sphere(p),
165
+ plane: (p) => plane(p),
166
+ cylinder: (p) => cylinder(p),
167
+ cone: (p) => cone(p),
168
+ capsule: (p) => capsule(p),
169
+ torus: (p) => torus(p)
170
+ };
171
+ function triangles(mesh) {
172
+ if (mesh.indices) return mesh.indices;
173
+ const n = mesh.positions.length / 3;
174
+ const out = n > 65535 ? new Uint32Array(n) : new Uint16Array(n);
175
+ for (let i = 0; i < n; i++) out[i] = i;
176
+ return out;
177
+ }
178
+ function computeNormals(mesh) {
179
+ const p = mesh.positions;
180
+ const tri = triangles(mesh);
181
+ const acc = new Float64Array(p.length);
182
+ for (let t = 0; t < tri.length; t += 3) {
183
+ const a = tri[t] * 3;
184
+ const b = tri[t + 1] * 3;
185
+ const c = tri[t + 2] * 3;
186
+ const ux = p[b] - p[a];
187
+ const uy = p[b + 1] - p[a + 1];
188
+ const uz = p[b + 2] - p[a + 2];
189
+ const vx = p[c] - p[a];
190
+ const vy = p[c + 1] - p[a + 1];
191
+ const vz = p[c + 2] - p[a + 2];
192
+ const nx = uy * vz - uz * vy;
193
+ const ny = uz * vx - ux * vz;
194
+ const nz = ux * vy - uy * vx;
195
+ for (const v of [a, b, c]) {
196
+ acc[v] = acc[v] + nx;
197
+ acc[v + 1] = acc[v + 1] + ny;
198
+ acc[v + 2] = acc[v + 2] + nz;
199
+ }
200
+ }
201
+ const normals = new Float32Array(p.length);
202
+ for (let v = 0; v < p.length; v += 3) {
203
+ const x = acc[v];
204
+ const y = acc[v + 1];
205
+ const z = acc[v + 2];
206
+ const len = Math.sqrt(x * x + y * y + z * z);
207
+ if (len > 0) {
208
+ normals[v] = x / len;
209
+ normals[v + 1] = y / len;
210
+ normals[v + 2] = z / len;
211
+ } else normals[v + 1] = 1;
212
+ }
213
+ return { ...mesh, normals };
214
+ }
215
+ function computeTangents(mesh) {
216
+ const p = mesh.positions;
217
+ const n = mesh.normals;
218
+ const uv = mesh.uvs;
219
+ const count = p.length / 3;
220
+ const out = new Float32Array(count * 4);
221
+ const tan = new Float64Array(count * 3);
222
+ const bit = new Float64Array(count * 3);
223
+ if (uv) {
224
+ const tri = triangles(mesh);
225
+ for (let t = 0; t < tri.length; t += 3) {
226
+ const a = tri[t];
227
+ const b = tri[t + 1];
228
+ const c = tri[t + 2];
229
+ const e1x = p[b * 3] - p[a * 3];
230
+ const e1y = p[b * 3 + 1] - p[a * 3 + 1];
231
+ const e1z = p[b * 3 + 2] - p[a * 3 + 2];
232
+ const e2x = p[c * 3] - p[a * 3];
233
+ const e2y = p[c * 3 + 1] - p[a * 3 + 1];
234
+ const e2z = p[c * 3 + 2] - p[a * 3 + 2];
235
+ const du1 = uv[b * 2] - uv[a * 2];
236
+ const dv1 = uv[b * 2 + 1] - uv[a * 2 + 1];
237
+ const du2 = uv[c * 2] - uv[a * 2];
238
+ const dv2 = uv[c * 2 + 1] - uv[a * 2 + 1];
239
+ const det = du1 * dv2 - du2 * dv1;
240
+ if (det === 0) continue;
241
+ const r = 1 / det;
242
+ const tx = (e1x * dv2 - e2x * dv1) * r;
243
+ const ty = (e1y * dv2 - e2y * dv1) * r;
244
+ const tz = (e1z * dv2 - e2z * dv1) * r;
245
+ const bx = (e2x * du1 - e1x * du2) * r;
246
+ const by = (e2y * du1 - e1y * du2) * r;
247
+ const bz = (e2z * du1 - e1z * du2) * r;
248
+ for (const v of [a, b, c]) {
249
+ tan[v * 3] = tan[v * 3] + tx;
250
+ tan[v * 3 + 1] = tan[v * 3 + 1] + ty;
251
+ tan[v * 3 + 2] = tan[v * 3 + 2] + tz;
252
+ bit[v * 3] = bit[v * 3] + bx;
253
+ bit[v * 3 + 1] = bit[v * 3 + 1] + by;
254
+ bit[v * 3 + 2] = bit[v * 3 + 2] + bz;
255
+ }
256
+ }
257
+ }
258
+ for (let v = 0; v < count; v++) {
259
+ const nx = n[v * 3];
260
+ const ny = n[v * 3 + 1];
261
+ const nz = n[v * 3 + 2];
262
+ let tx = tan[v * 3];
263
+ let ty = tan[v * 3 + 1];
264
+ let tz = tan[v * 3 + 2];
265
+ if (tx === 0 && ty === 0 && tz === 0) {
266
+ const ax = Math.abs(nx);
267
+ const ay = Math.abs(ny);
268
+ const az = Math.abs(nz);
269
+ if (ax <= ay && ax <= az) tx = 1;
270
+ else if (ay <= az) ty = 1;
271
+ else tz = 1;
272
+ }
273
+ const d = nx * tx + ny * ty + nz * tz;
274
+ tx -= nx * d;
275
+ ty -= ny * d;
276
+ tz -= nz * d;
277
+ const len = Math.sqrt(tx * tx + ty * ty + tz * tz) || 1;
278
+ tx /= len;
279
+ ty /= len;
280
+ tz /= len;
281
+ const cx = ny * tz - nz * ty;
282
+ const cy = nz * tx - nx * tz;
283
+ const cz = nx * ty - ny * tx;
284
+ const w = cx * bit[v * 3] + cy * bit[v * 3 + 1] + cz * bit[v * 3 + 2] < 0 ? -1 : 1;
285
+ out[v * 4] = tx;
286
+ out[v * 4 + 1] = ty;
287
+ out[v * 4 + 2] = tz;
288
+ out[v * 4 + 3] = w;
289
+ }
290
+ return out;
291
+ }
292
+ function cluster(mesh, cells) {
293
+ const p = mesh.positions;
294
+ const count = p.length / 3;
295
+ let minX = Infinity;
296
+ let minY = Infinity;
297
+ let minZ = Infinity;
298
+ let maxX = -Infinity;
299
+ let maxY = -Infinity;
300
+ let maxZ = -Infinity;
301
+ for (let v = 0; v < count; v++) {
302
+ minX = Math.min(minX, p[v * 3]);
303
+ minY = Math.min(minY, p[v * 3 + 1]);
304
+ minZ = Math.min(minZ, p[v * 3 + 2]);
305
+ maxX = Math.max(maxX, p[v * 3]);
306
+ maxY = Math.max(maxY, p[v * 3 + 1]);
307
+ maxZ = Math.max(maxZ, p[v * 3 + 2]);
308
+ }
309
+ const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1;
310
+ const cell = size / cells;
311
+ const map = new Int32Array(count);
312
+ const byKey = /* @__PURE__ */ new Map();
313
+ const sums = [];
314
+ const members = [];
315
+ for (let v = 0; v < count; v++) {
316
+ const ix = Math.min(cells - 1, Math.floor((p[v * 3] - minX) / cell));
317
+ const iy = Math.min(cells - 1, Math.floor((p[v * 3 + 1] - minY) / cell));
318
+ const iz = Math.min(cells - 1, Math.floor((p[v * 3 + 2] - minZ) / cell));
319
+ const key = (ix * cells + iy) * cells + iz;
320
+ let c = byKey.get(key);
321
+ if (c === void 0) {
322
+ c = members.length;
323
+ byKey.set(key, c);
324
+ members.push(0);
325
+ sums.push(0, 0, 0);
326
+ }
327
+ map[v] = c;
328
+ members[c] = members[c] + 1;
329
+ sums[c * 3] = sums[c * 3] + p[v * 3];
330
+ sums[c * 3 + 1] = sums[c * 3 + 1] + p[v * 3 + 1];
331
+ sums[c * 3 + 2] = sums[c * 3 + 2] + p[v * 3 + 2];
332
+ }
333
+ const tri = triangles(mesh);
334
+ const kept = [];
335
+ for (let t = 0; t < tri.length; t += 3) {
336
+ const a = map[tri[t]];
337
+ const b = map[tri[t + 1]];
338
+ const c = map[tri[t + 2]];
339
+ if (a !== b && b !== c && c !== a) kept.push(a, b, c);
340
+ }
341
+ const n = members.length;
342
+ const positions = new Float32Array(n * 3);
343
+ for (let c = 0; c < n; c++) {
344
+ positions[c * 3] = sums[c * 3] / members[c];
345
+ positions[c * 3 + 1] = sums[c * 3 + 1] / members[c];
346
+ positions[c * 3 + 2] = sums[c * 3 + 2] / members[c];
347
+ }
348
+ const out = {
349
+ positions,
350
+ indices: n > 65535 ? new Uint32Array(kept) : new Uint16Array(kept)
351
+ };
352
+ for (const [name, width] of [
353
+ ["uvs", 2],
354
+ ["colors", 4]
355
+ ]) {
356
+ const src = mesh[name];
357
+ if (!src) continue;
358
+ const acc = new Float64Array(n * width);
359
+ for (let v = 0; v < count; v++)
360
+ for (let k = 0; k < width; k++)
361
+ acc[map[v] * width + k] = acc[map[v] * width + k] + src[v * width + k];
362
+ const avg = new Float32Array(n * width);
363
+ for (let c = 0; c < n; c++)
364
+ for (let k = 0; k < width; k++) avg[c * width + k] = acc[c * width + k] / members[c];
365
+ out[name] = avg;
366
+ }
367
+ return out;
368
+ }
369
+ function simplify(mesh, fraction) {
370
+ const target = Math.max(4, Math.floor(triangles(mesh).length / 3 * fraction));
371
+ let lo = 1;
372
+ let hi = 256;
373
+ let best = cluster(mesh, 2);
374
+ for (let step = 0; step < 8 && lo <= hi; step++) {
375
+ const cells = lo + hi >> 1;
376
+ const candidate = cluster(mesh, cells);
377
+ const tris = (candidate.indices?.length ?? 0) / 3;
378
+ if (tris <= target) {
379
+ best = candidate;
380
+ lo = cells + 1;
381
+ } else hi = cells - 1;
382
+ }
383
+ return best;
384
+ }
385
+ function finish(mesh, options = {}) {
386
+ const withNormals = (m) => options.normals === true || !m.normals ? computeNormals(m) : m;
387
+ const withTangents = (m) => options.tangents ? { ...m, tangents: computeTangents(m) } : m;
388
+ const base = withTangents(withNormals(mesh));
389
+ const lods = [];
390
+ for (const fraction of options.lods ?? []) {
391
+ if (!(fraction > 0 && fraction < 1)) {
392
+ throw new ShardError("procgen/bad-mesh", `LOD fractions are in (0, 1), got ${fraction}`, {
393
+ hint: "lods: [0.5, 0.2] keeps about half, then a fifth, of the triangles."
394
+ });
395
+ }
396
+ lods.push(withTangents(computeNormals(simplify(mesh, fraction))));
397
+ }
398
+ return { kind: "mesh", mesh: base, lods };
399
+ }
400
+ const meshBuilder = {
401
+ icosphere,
402
+ primitive(name, params = {}) {
403
+ const make = PRIMITIVES[name];
404
+ if (!make) {
405
+ throw new ShardError("procgen/bad-mesh", `No primitive "${name}"`, {
406
+ hint: `Primitives: ${Object.keys(PRIMITIVES).join(", ")}.`
407
+ });
408
+ }
409
+ const data = make(params).data();
410
+ const out = { positions: data.positions.slice() };
411
+ if (data.normals) out.normals = data.normals.slice();
412
+ if (data.uvs) out.uvs = data.uvs.slice();
413
+ if (data.indices) out.indices = data.indices.slice();
414
+ return out;
415
+ },
416
+ computeNormals,
417
+ finish
418
+ };
419
+ export {
420
+ meshBuilder
421
+ };
@@ -0,0 +1,44 @@
1
+ import { type JsonValue, type World } from '@aethervtt/shard-core';
2
+ import { type PreviewImage } from '@aethervtt/shard-protocol';
3
+ import type { AppMethod } from '@aethervtt/shard-runtime';
4
+ import { type Generator, type GenRequest } from './generator.js';
5
+ /**
6
+ * The generator a tool names: `star-explorer/Rock`, `scripts:Rock` or `Rock` (when one generator
7
+ * has that name), or a `*.gen.json` path (its seed and params become defaults).
8
+ */
9
+ export declare function resolveGenerator(world: World, target: string): Promise<{
10
+ gen: Generator;
11
+ seed?: number;
12
+ params: Record<string, JsonValue>;
13
+ file?: string;
14
+ }>;
15
+ export interface RunSummary {
16
+ generator: string;
17
+ output: string;
18
+ path: string;
19
+ guid: string;
20
+ key: string | undefined;
21
+ seed: number;
22
+ /** Whether the output came from a cache (memory, disk, or already loaded) instead of a run. */
23
+ cacheHit: boolean;
24
+ hit: string;
25
+ /** Generation time (the original run's, on a cache hit), ms. */
26
+ ms: number;
27
+ vertices?: number;
28
+ triangles?: number;
29
+ bounds?: JsonValue;
30
+ lods?: JsonValue;
31
+ texture?: JsonValue;
32
+ entities?: number;
33
+ components?: JsonValue;
34
+ generated?: number;
35
+ warnings: {
36
+ message: string;
37
+ path?: string;
38
+ }[];
39
+ }
40
+ /** Makes (or finds) an output and summarizes it. */
41
+ export declare function runGenerator(world: World, request: GenRequest): Promise<RunSummary>;
42
+ /** A picture of one output: rendered meshes and entities, textures as they are. */
43
+ export declare function previewOutput(world: World, request: GenRequest, size: number): Promise<PreviewImage>;
44
+ export declare const procgenMethods: AppMethod[];
@@ -0,0 +1,212 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import {
3
+ defineSchema,
4
+ isPlainObject,
5
+ ShardError,
6
+ t
7
+ } from "@aethervtt/shard-core";
8
+ import {
9
+ encodePng,
10
+ previewAsset,
11
+ previewKtx2,
12
+ toBase64
13
+ } from "@aethervtt/shard-protocol";
14
+ import {
15
+ allGenerators,
16
+ codeHashOf,
17
+ findGenerator,
18
+ requestOf
19
+ } from "./generator.js";
20
+ import { GeneratorBindings, procgen, procgenHost } from "./runtime.js";
21
+ import { contactSheet, parseSeeds } from "./sheet.js";
22
+ async function resolveGenerator(world, target2) {
23
+ if (target2.includes(".gen.json")) {
24
+ const file = target2.split("#")[0];
25
+ const server = assetServer(world);
26
+ const entry = server.entry(`${file}#Generator`);
27
+ if (!entry) {
28
+ throw new ShardError(
29
+ "procgen/unknown-generator",
30
+ `No generator file "${file}" in the catalog`,
31
+ {
32
+ hint: "Check the path, or run `shard import` for new files."
33
+ }
34
+ );
35
+ }
36
+ await server.load(entry.guid);
37
+ const binding = world.resource(GeneratorBindings).get(entry);
38
+ const gen = findGenerator(binding.generator);
39
+ if (!gen) {
40
+ throw new ShardError("procgen/unknown-generator", `No generator "${binding.generator}"`, {
41
+ path: `${file}/generator`
42
+ });
43
+ }
44
+ return { gen, seed: binding.seed, params: binding.params ?? {}, file };
45
+ }
46
+ const name = target2.startsWith("scripts:") ? target2.slice("scripts:".length) : target2;
47
+ const exact = findGenerator(name);
48
+ if (exact) return { gen: exact, params: {} };
49
+ const matches = allGenerators().filter((g) => g.name.endsWith(`/${name}`));
50
+ if (matches.length === 1) return { gen: matches[0], params: {} };
51
+ throw new ShardError("procgen/unknown-generator", `No generator "${target2}"`, {
52
+ hint: matches.length ? `Several match: ${matches.map((g) => g.name).join(", ")}.` : `Generators: ${allGenerators().map((g) => g.name).join(", ") || "(none)"}.`
53
+ });
54
+ }
55
+ function paramsOf(value, flag = "params") {
56
+ if (value === void 0 || value === null) return {};
57
+ if (!isPlainObject(value)) {
58
+ throw new ShardError("procgen/bad-params", `"${flag}" must be an object of param values`, {
59
+ path: `/${flag}`
60
+ });
61
+ }
62
+ return value;
63
+ }
64
+ async function runGenerator(world, request) {
65
+ const runtime = procgen(world);
66
+ const gen = findGenerator(request.generator);
67
+ const entry = runtime.entryFor(request);
68
+ const wasLoaded = entry.state === "loaded";
69
+ await assetServer(world).load(entry.guid);
70
+ const last = runtime.lastResult(entry.guid);
71
+ const record = runtime.recordOf(entry.guid);
72
+ const main = record?.assets.find((a) => a.label === "");
73
+ const info = main?.info ?? {};
74
+ const hit = wasLoaded ? "loaded" : last?.hit ?? "run";
75
+ const out = {
76
+ generator: gen.name,
77
+ output: gen.outputType,
78
+ path: entry.path,
79
+ guid: entry.guid,
80
+ key: last?.key,
81
+ seed: request.seed,
82
+ cacheHit: hit !== "run",
83
+ hit,
84
+ ms: Math.round((last?.ms ?? 0) * 100) / 100,
85
+ warnings: record?.warnings ?? []
86
+ };
87
+ if (gen.output === "mesh") {
88
+ out.vertices = info.vertices;
89
+ out.triangles = info.triangles;
90
+ out.bounds = info.bounds;
91
+ if (info.lods) out.lods = info.lods;
92
+ } else if (gen.output === "texture") {
93
+ out.texture = { width: info.width, height: info.height, usage: info.usage, mips: info.mips };
94
+ } else if (gen.output === "entities") {
95
+ out.entities = info.entities;
96
+ out.components = info.components;
97
+ }
98
+ if (record && record.children.length > 0) out.generated = record.children.length;
99
+ return out;
100
+ }
101
+ async function previewOutput(world, request, size) {
102
+ const runtime = procgen(world);
103
+ const gen = findGenerator(request.generator);
104
+ const entry = runtime.entryFor(request);
105
+ await assetServer(world).load(entry.guid);
106
+ if (gen.output === "texture") {
107
+ const bytes = runtime.recordOf(entry.guid)?.assets.find((a) => a.label === "")?.bytes;
108
+ if (bytes) return previewKtx2(bytes, size, size);
109
+ }
110
+ if (gen.output === "data") {
111
+ throw new ShardError("procgen/no-preview", `${gen.name} makes data; there's nothing to draw`, {
112
+ hint: "procgen.run returns the value summary; asset.get shows it."
113
+ });
114
+ }
115
+ return previewAsset(world, entry.path, size, size);
116
+ }
117
+ const target = t.string({
118
+ required: true,
119
+ description: 'A generator ("star-explorer/Rock", or "Rock" when unique) or a *.gen.json path, whose seed and params become defaults.'
120
+ });
121
+ const procgenMethods = [
122
+ {
123
+ name: "procgen.run",
124
+ description: "Runs a generator (or finds its cached output) and summarizes it: output type, cache key, time, whether it was a cache hit, vertex and triangle counts and bounds (meshes), size (textures), entities and components (entities), nested outputs, warnings. The output is an asset at the returned path (procedural:\u2026).",
125
+ params: defineSchema("procgen/RunParams", {
126
+ generator: target,
127
+ params: t.json({ description: "Param values; missing ones take the defaults." }),
128
+ seed: t.u32({ description: "The seed (default: the file's, or 0)." })
129
+ }),
130
+ handler: async ({ world }, p) => {
131
+ const found = await resolveGenerator(world, p.generator);
132
+ const params = { ...found.params, ...paramsOf(p.params) };
133
+ const seed = p.seed || found.seed || 0;
134
+ return runGenerator(world, requestOf(found.gen, params, seed));
135
+ }
136
+ },
137
+ {
138
+ name: "procgen.preview",
139
+ description: 'A PNG of a generator output: a mesh in a neutral studio light from a three-quarter view, a texture as is, entities as a scene framed on their bounds. With seeds ("1-9" or [1, 4, 9]) it is a labelled contact sheet, one cell per seed: change a param, preview nine seeds, compare.',
140
+ params: defineSchema("procgen/PreviewParams", {
141
+ generator: target,
142
+ params: t.json({ description: "Param values." }),
143
+ seed: t.u32({ description: "The seed, without seeds." }),
144
+ seeds: t.json({ description: 'Seeds for a contact sheet: "1-9", "1,5,9", or [1, 5, 9].' }),
145
+ size: t.u32({ default: 256, min: 32, max: 1024, description: "Pixels per cell." })
146
+ }),
147
+ handler: async ({ world }, p) => {
148
+ const found = await resolveGenerator(world, p.generator);
149
+ const params = { ...found.params, ...paramsOf(p.params) };
150
+ const size = p.size;
151
+ const seeds = p.seeds === void 0 || p.seeds === null ? void 0 : parseSeeds(p.seeds);
152
+ let image;
153
+ const keys = [];
154
+ if (seeds && seeds.length > 0) {
155
+ const images = [];
156
+ for (const seed of seeds) {
157
+ const request = requestOf(found.gen, params, seed);
158
+ images.push(await previewOutput(world, request, size));
159
+ const guid = procgen(world).entryFor(request).guid;
160
+ keys.push(procgen(world).lastResult(guid)?.key ?? "");
161
+ }
162
+ image = contactSheet(
163
+ images,
164
+ seeds.map((s) => `seed ${s}`),
165
+ size
166
+ );
167
+ } else {
168
+ const request = requestOf(found.gen, params, p.seed || found.seed || 0);
169
+ image = await previewOutput(world, request, size);
170
+ keys.push(procgen(world).lastResult(procgen(world).entryFor(request).guid)?.key ?? "");
171
+ }
172
+ const png = await encodePng(image.data, image.width, image.height);
173
+ return {
174
+ width: image.width,
175
+ height: image.height,
176
+ data: toBase64(png),
177
+ generator: found.gen.name,
178
+ ...seeds ? { seeds } : {},
179
+ keys
180
+ };
181
+ }
182
+ },
183
+ {
184
+ name: "procgen.describe",
185
+ description: "Every generator (params schema with units and ranges, output type, version, code hash), the output cache (records, bytes, limit), counts of runs and cache hits, jobs running now, and whether jobs run on workers.",
186
+ params: defineSchema("procgen/DescribeParams", {}),
187
+ handler: ({ world }) => {
188
+ const runtime = procgen(world);
189
+ const host = procgenHost();
190
+ return {
191
+ generators: allGenerators().map((g) => ({
192
+ name: g.name,
193
+ ...g.description ? { description: g.description } : {},
194
+ output: g.outputType,
195
+ version: g.version,
196
+ codeHash: codeHashOf(g),
197
+ params: g.params.jsonSchema()
198
+ })),
199
+ cache: runtime.cacheStats(),
200
+ stats: { ...runtime.stats, mainMs: Math.round(runtime.stats.mainMs * 100) / 100 },
201
+ inFlight: runtime.inFlight(),
202
+ workers: host.workers && host.workerModule ? host.workers.size : 0
203
+ };
204
+ }
205
+ }
206
+ ];
207
+ export {
208
+ previewOutput,
209
+ procgenMethods,
210
+ resolveGenerator,
211
+ runGenerator
212
+ };
@@ -0,0 +1,22 @@
1
+ import { type JsonValue } from '@aethervtt/shard-core';
2
+ import type { Generator } from './generator.js';
3
+ /** One asset a generator run made: the main output ('') or a sub-asset (`LOD1`). */
4
+ export interface GenOutputAsset {
5
+ label: string;
6
+ type: string;
7
+ bytes?: Uint8Array;
8
+ json?: JsonValue;
9
+ info?: Record<string, JsonValue>;
10
+ }
11
+ /** Plain JSON of a value: refs lose their `type`, typed arrays become arrays. */
12
+ export declare function toJson(value: unknown): JsonValue;
13
+ /**
14
+ * Turns what `run` returned into artifacts, checking it's the generator's output type. With
15
+ * `checkComponents: false`, entities' components aren't checked against their schemas (a worker
16
+ * whose bundle may predate the current schemas; the caller checks with `checkFragment`).
17
+ */
18
+ export declare function encodeOutput(gen: Generator, value: unknown, options?: {
19
+ checkComponents?: boolean;
20
+ }): GenOutputAsset[];
21
+ /** Checks an `entities` artifact's components against this thread's schemas; throws like `run`. */
22
+ export declare function checkFragment(gen: Generator, asset: GenOutputAsset): void;