@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @aethervtt/shard-procgen
2
+
3
+ Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
@@ -0,0 +1,12 @@
1
+ /** A call a generator module makes that can't be deterministic. */
2
+ export interface NondeterministicCall {
3
+ file: string;
4
+ line: number;
5
+ column: number;
6
+ call: string;
7
+ }
8
+ /**
9
+ * Finds `Math.random()`, `Date.now()`, and `performance.now()` in modules that define generators
10
+ * (`project.generator(` or `defineGenerator(`), for `shard check`. Comments are skipped.
11
+ */
12
+ export declare function findNondeterminism(file: string, source: string): NondeterministicCall[];
package/dist/check.js ADDED
@@ -0,0 +1,18 @@
1
+ const DEFINES = /\.generator\s*\(|\bdefineGenerator\s*\(/;
2
+ const CALLS = /\b(Math\.random|Date\.now|performance\.now)\s*\(/g;
3
+ function findNondeterminism(file, source) {
4
+ if (!DEFINES.test(source)) return [];
5
+ const code = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, " "));
6
+ const out = [];
7
+ CALLS.lastIndex = 0;
8
+ for (let m = CALLS.exec(code); m; m = CALLS.exec(code)) {
9
+ const before = code.slice(0, m.index);
10
+ const line = before.split("\n").length;
11
+ const column = m.index - before.lastIndexOf("\n");
12
+ out.push({ file, line, column, call: `${m[1]}()` });
13
+ }
14
+ return out;
15
+ }
16
+ export {
17
+ findNondeterminism
18
+ };
@@ -0,0 +1,61 @@
1
+ import { type AssetRef, type Fields, Rng } from '@aethervtt/shard-core';
2
+ import { type NoiseGraph, sampleGrid2d, sampleNoiseGradient, sampleSpherePatch } from '@aethervtt/shard-noise';
3
+ import type { Generator, GenParams, GenRequest, OutputAsset, OutputSpec } from './generator.js';
4
+ import { type MeshBuilderApi } from './mesh-builder.js';
5
+ /** Noise sampling inside a generator: 0041's sync functions, on the generator's thread. */
6
+ export interface NoiseApi {
7
+ /** Values at absolute points (`xyz…`); see `sampleNoise`. */
8
+ sample(graph: NoiseGraph, seed: number, points: Float32Array, out: Float32Array, node?: string): void;
9
+ /** Values at `origin + local` (precise far from the origin); see `sampleOffset`. */
10
+ offset(graph: NoiseGraph, seed: number, origin: ArrayLike<number>, local: Float32Array, out: Float32Array, node?: string): void;
11
+ grid2d: typeof sampleGrid2d;
12
+ spherePatch: typeof sampleSpherePatch;
13
+ gradient: typeof sampleNoiseGradient;
14
+ }
15
+ /** What `run` gets besides its params. Everything here is deterministic. */
16
+ export interface GenContext {
17
+ readonly seed: number;
18
+ /** Seeded from `hashSeed(seed, generatorName)`: the only randomness a generator has. */
19
+ readonly rng: Rng;
20
+ /** `hashSeed(seed, label)`: a stable seed per label, for parts and nested generators. */
21
+ childSeed(label: string | number): number;
22
+ readonly noise: NoiseApi;
23
+ /**
24
+ * An asset a param points at (a NoiseGraph, a mesh, a data value), loaded before `run`. Only
25
+ * handles in the params can be loaded, so a generator never does I/O.
26
+ */
27
+ load<T = unknown>(ref: AssetRef | {
28
+ guid?: string;
29
+ path?: string;
30
+ } | null | undefined): T;
31
+ /**
32
+ * Another generator's output, as a reference to put in this one's output (a mesh handle in a
33
+ * fragment). It's cached on its own: changing it rebuilds only it.
34
+ */
35
+ generate<const P extends Fields, const O extends OutputSpec>(gen: Generator<P, O>, params: GenParams<P>, seed: number): Promise<AssetRef<OutputAsset<O>>>;
36
+ readonly mesh: MeshBuilderApi;
37
+ /** A GPU for compute, when one exists. Absent in workers and headless runs. */
38
+ readonly gpu?: undefined;
39
+ /** A non-fatal problem, shown by procgen.run and asset.get. */
40
+ warn(message: string, path?: string): void;
41
+ }
42
+ export interface ContextState {
43
+ children: GenRequest[];
44
+ warnings: {
45
+ message: string;
46
+ path?: string;
47
+ }[];
48
+ }
49
+ /** A dependency as the job carries it: the handle's guid and path, and the loaded object. */
50
+ export interface LoadedDependency {
51
+ guid: string | undefined;
52
+ path: string | undefined;
53
+ type: string;
54
+ object: unknown;
55
+ }
56
+ export declare function createContext(gen: Generator, request: GenRequest, deps: readonly LoadedDependency[], chain: readonly string[], state: ContextState): GenContext;
57
+ /**
58
+ * Replaces `Math.random`, `Date.now`, and `performance.now` with functions that throw
59
+ * `procgen/nondeterministic`. Returns a function that restores them.
60
+ */
61
+ export declare function guardNondeterminism(generator: string): () => void;
@@ -0,0 +1,96 @@
1
+ import { hashSeed, Rng, ShardError } from "@aethervtt/shard-core";
2
+ import {
3
+ sampleGrid2d,
4
+ sampleNoise,
5
+ sampleNoiseGradient,
6
+ sampleOffset,
7
+ sampleSpherePatch
8
+ } from "@aethervtt/shard-noise";
9
+ import { guidOf, identityOf, proceduralPath, requestOf } from "./generator.js";
10
+ import { meshBuilder } from "./mesh-builder.js";
11
+ const noiseApi = {
12
+ sample: sampleNoise,
13
+ offset: sampleOffset,
14
+ grid2d: sampleGrid2d,
15
+ spherePatch: sampleSpherePatch,
16
+ gradient: sampleNoiseGradient
17
+ };
18
+ function createContext(gen, request, deps, chain, state) {
19
+ const self = identityOf(request);
20
+ return {
21
+ seed: request.seed,
22
+ rng: new Rng(hashSeed(request.seed, gen.name)),
23
+ childSeed: (label) => hashSeed(request.seed, label),
24
+ noise: noiseApi,
25
+ load(ref) {
26
+ const dep = ref && deps.find(
27
+ (d) => ref.guid !== void 0 && d.guid === ref.guid || ref.path !== void 0 && d.path === ref.path
28
+ );
29
+ if (!dep) {
30
+ throw new ShardError(
31
+ "procgen/undeclared-dependency",
32
+ `${gen.name} loaded ${JSON.stringify(ref ?? null)}, which isn't one of its params`,
33
+ {
34
+ hint: "ctx.load takes the handles in the params (declare a t.handle param and pass the asset in)."
35
+ }
36
+ );
37
+ }
38
+ return dep.object;
39
+ },
40
+ generate: (child, params, seed) => {
41
+ const req = requestOf(child, params, seed);
42
+ const id = identityOf(req);
43
+ if (id === self || chain.includes(id)) {
44
+ return Promise.reject(
45
+ new ShardError(
46
+ "procgen/cycle",
47
+ `${gen.name} generates ${child.name} with the same inputs as one of its callers`,
48
+ {
49
+ hint: "A generator that calls itself needs different params or a different seed (ctx.childSeed) each level."
50
+ }
51
+ )
52
+ );
53
+ }
54
+ if (!state.children.some((c) => identityOf(c) === id)) state.children.push(req);
55
+ return Promise.resolve({
56
+ type: child.outputType,
57
+ guid: guidOf(req),
58
+ path: proceduralPath(req)
59
+ });
60
+ },
61
+ mesh: meshBuilder,
62
+ warn: (message, path) => state.warnings.push(path === void 0 ? { message } : { message, path })
63
+ };
64
+ }
65
+ function guardNondeterminism(generator) {
66
+ const fail = (what) => () => {
67
+ throw new ShardError("procgen/nondeterministic", `${generator} called ${what}`, {
68
+ hint: "Generators are pure: use ctx.rng for randomness and pass times in as params."
69
+ });
70
+ };
71
+ const random = Math.random;
72
+ const now = Date.now;
73
+ const perf = globalThis.performance;
74
+ const ownNow = perf ? Object.getOwnPropertyDescriptor(perf, "now") : void 0;
75
+ Math.random = fail("Math.random()");
76
+ Date.now = fail("Date.now()");
77
+ if (perf) {
78
+ Object.defineProperty(perf, "now", {
79
+ value: fail("performance.now()"),
80
+ configurable: true,
81
+ writable: true
82
+ });
83
+ }
84
+ return () => {
85
+ Math.random = random;
86
+ Date.now = now;
87
+ if (perf) {
88
+ if (ownNow) Object.defineProperty(perf, "now", ownNow);
89
+ else delete perf.now;
90
+ }
91
+ };
92
+ }
93
+ export {
94
+ createContext,
95
+ guardNondeterminism
96
+ };
@@ -0,0 +1,135 @@
1
+ import type { DataType } from '@aethervtt/shard-assets';
2
+ import { type AssetRef, type ComponentDef, type Fields, type FieldValue, type InferFields, type InitFields, type JsonValue } from '@aethervtt/shard-core';
3
+ import type { MeshData } from '@aethervtt/shard-mesh';
4
+ import type { TextureUsage } from '@aethervtt/shard-texture';
5
+ import type { GenContext } from './context.js';
6
+ /** What a generator makes: a kind, or a data type (0031) whose value it returns. */
7
+ export type OutputSpec = 'mesh' | 'texture' | 'data' | 'entities' | DataType;
8
+ /** A mesh with its levels of detail, as `ctx.mesh.finish` returns it. */
9
+ export interface MeshResult {
10
+ readonly kind: 'mesh';
11
+ mesh: MeshData;
12
+ /** Lower levels of detail, finest first (sub-assets `#LOD1`, `#LOD2`, …). */
13
+ lods: MeshData[];
14
+ }
15
+ /** A texture as `Texture.create` takes it: RGBA8 levels, or float RGBA for `usage: 'hdr'`. */
16
+ export interface TextureData {
17
+ width: number;
18
+ height: number;
19
+ /** Default `color` (sRGB). `data` and `normal` are linear; `hdr` levels are Float32Array. */
20
+ usage?: TextureUsage;
21
+ /** Level 0 first. With `mipmaps` and one level, the rest are generated. */
22
+ mips: (Uint8Array | Float32Array)[];
23
+ mipmaps?: boolean;
24
+ }
25
+ /**
26
+ * One entity of an `entities` output: a prefab file's entity (0030). Components are by name with
27
+ * JSON values; handles may be refs (`ctx.generate` results, loaded params) or `{ path }`.
28
+ */
29
+ export interface FragmentEntity {
30
+ name: string;
31
+ components?: Record<string, Record<string, unknown>>;
32
+ children?: FragmentEntity[];
33
+ }
34
+ /** An `entities` output: the root entity (its components merge into the instance), or its children. */
35
+ export type Fragment = FragmentEntity | FragmentEntity[];
36
+ /** What `run` returns for an output spec. */
37
+ export type RunResult<O extends OutputSpec> = O extends 'mesh' ? MeshResult | MeshData : O extends 'texture' ? TextureData : O extends 'entities' ? Fragment : O extends DataType<infer F> ? InferFields<F> : JsonValue;
38
+ /** The asset type an output spec makes. */
39
+ export type OutputAsset<O extends OutputSpec> = O extends 'mesh' ? 'Mesh' : O extends 'texture' ? 'Texture' : O extends 'entities' ? 'Prefab' : O extends DataType<Fields, infer N> ? N : 'Data';
40
+ export interface GeneratorOptions<P extends Fields, O extends OutputSpec> {
41
+ /** Parameters: a component schema (units, ranges, defaults, handles to what `run` loads). */
42
+ params: P;
43
+ output: O;
44
+ /** Bump for changes the code hash can't see (a vendored table, say). Default 1. */
45
+ version?: number;
46
+ description?: string;
47
+ /**
48
+ * Makes the output. Pure: only `ctx.rng`, `ctx.noise`, the params, and what `ctx.load` returns.
49
+ * Returns synchronously, or a promise when it awaits `ctx.generate`.
50
+ */
51
+ run(ctx: GenContext, params: InferFields<P>): RunResult<O> | Promise<RunResult<O>>;
52
+ }
53
+ /**
54
+ * A generator: `(seed, params) → output`, cached by the hash of its inputs. It's also a reference
55
+ * to itself (`type: 'Generator'`), so it can go in a `t.handle('Generator')` field
56
+ * (`GeneratorInstance({ generator: StarSystem })`).
57
+ */
58
+ export interface Generator<P extends Fields = Fields, O extends OutputSpec = OutputSpec> extends AssetRef<'Generator'> {
59
+ readonly name: string;
60
+ readonly description: string | undefined;
61
+ readonly params: ComponentDef<P>;
62
+ /** The output kind; `data` for data types too. */
63
+ readonly output: 'mesh' | 'texture' | 'data' | 'entities';
64
+ /** The asset type outputs load as: `Mesh`, `Texture`, `Prefab`, a data type's name, or `Data`. */
65
+ readonly outputType: OutputAsset<O>;
66
+ /** The data type a `data` output is validated against, if any. */
67
+ readonly dataType: ComponentDef | undefined;
68
+ readonly version: number;
69
+ readonly run: GeneratorOptions<P, O>['run'];
70
+ }
71
+ /** A small, stable string hash (two FNV-1a lanes, 64 bits as hex). Not for security. */
72
+ export declare function hashString(text: string): string;
73
+ /**
74
+ * Defines a generator named `name` (namespaced: `shard/Rock` for engine generators; projects use
75
+ * `project.generator`, which prefixes the project name). Defining a name again replaces it (hot
76
+ * reload).
77
+ */
78
+ export declare function defineGenerator<const P extends Fields, const O extends OutputSpec>(name: string, options: GeneratorOptions<P, O>): Generator<P, O>;
79
+ export declare function findGenerator(name: string): Generator | undefined;
80
+ /** The generator a name or reference names. Throws `procgen/unknown-generator`. */
81
+ export declare function requireGenerator(ref: string | {
82
+ name?: string;
83
+ path?: string;
84
+ }): Generator;
85
+ /** Every generator defined in this process, sorted by name. */
86
+ export declare function allGenerators(): Generator[];
87
+ /**
88
+ * Sets generators' code hashes: the hash of each generator's module and what it imports inside
89
+ * `scripts/`, from the bundler's module graph. Hosts call it before (re)loading project code; a
90
+ * generator without one uses the hash of its `run` source.
91
+ */
92
+ export declare function setGeneratorCodeHashes(hashes: Readonly<Record<string, string>>): void;
93
+ export declare function codeHashOf(gen: Generator | string): string;
94
+ /**
95
+ * The normalized params: validated, defaults filled in, keys sorted, f32 fields at f32 precision
96
+ * (so `1` and `1.0000000001` are the same inputs), handles as `{ path }` (or `{ guid }`). Throws
97
+ * `procgen/bad-params` with every error in `details`.
98
+ */
99
+ export declare function canonicalParams(gen: Generator, params?: unknown): Record<string, JsonValue>;
100
+ /**
101
+ * Param values given in code: any subset (the rest take defaults), with handles as refs or as
102
+ * `{ path }` / `{ guid }`.
103
+ */
104
+ export type GenParams<P extends Fields> = {
105
+ [K in keyof P]?: NonNullable<FieldValue<P[K]>> extends AssetRef ? {
106
+ guid?: string;
107
+ path?: string;
108
+ } | null : InitFields<P>[K];
109
+ };
110
+ /** One output: a generator, its canonical params, and a seed. */
111
+ export interface GenRequest {
112
+ readonly generator: string;
113
+ readonly params: Record<string, JsonValue>;
114
+ readonly seed: number;
115
+ }
116
+ /** Makes a request, validating the params. */
117
+ export declare function requestOf(gen: Generator, params: GenParams<Fields> | Record<string, unknown> | undefined, seed: number): GenRequest;
118
+ /**
119
+ * A request's identity: which output it names, whatever the code or loaded assets are now. Outputs
120
+ * are registered as `gen:<identity>`, and a code or dependency change swaps the content in place.
121
+ */
122
+ export declare function identityOf(request: GenRequest): string;
123
+ /** The asset guid of a request's output. */
124
+ export declare function guidOf(request: GenRequest): string;
125
+ /**
126
+ * The `procedural:` path of a request (`procedural:star-explorer/Rock?radius=2&seed=3`): params
127
+ * that differ from their defaults, sorted, then the seed. Resolves the same output anywhere.
128
+ */
129
+ export declare function proceduralPath(request: GenRequest): string;
130
+ /**
131
+ * Parses `star-explorer/Rock?seed=3&radius=2` (the part after `procedural:`): numbers, bools,
132
+ * asset paths for handles, JSON for structs and lists. Throws `procgen/unknown-generator` or
133
+ * `procgen/bad-params` (with `path` set to `at`).
134
+ */
135
+ export declare function parseProceduralRef(spec: string, at?: string): GenRequest;
@@ -0,0 +1,269 @@
1
+ import {
2
+ defineSchema,
3
+ isPlainObject,
4
+ ShardError
5
+ } from "@aethervtt/shard-core";
6
+ const generators = /* @__PURE__ */ new Map();
7
+ const codeHashes = /* @__PURE__ */ new Map();
8
+ const sourceHashes = /* @__PURE__ */ new Map();
9
+ function hashString(text) {
10
+ let a = 2166136261;
11
+ let b = 16777619 ^ 2654435769;
12
+ for (let i = 0; i < text.length; i++) {
13
+ const c = text.charCodeAt(i);
14
+ a = Math.imul(a ^ c, 16777619);
15
+ b = Math.imul(b ^ c, 1540483477);
16
+ b ^= b >>> 13;
17
+ }
18
+ a ^= a >>> 15;
19
+ b ^= b >>> 16;
20
+ return (a >>> 0).toString(16).padStart(8, "0") + (b >>> 0).toString(16).padStart(8, "0");
21
+ }
22
+ function outputOf(spec) {
23
+ if (spec === "mesh") return { output: "mesh", outputType: "Mesh", dataType: void 0 };
24
+ if (spec === "texture") return { output: "texture", outputType: "Texture", dataType: void 0 };
25
+ if (spec === "entities") return { output: "entities", outputType: "Prefab", dataType: void 0 };
26
+ if (spec === "data") return { output: "data", outputType: "Data", dataType: void 0 };
27
+ if (spec && typeof spec === "object" && typeof spec.name === "string") {
28
+ return { output: "data", outputType: spec.name, dataType: spec };
29
+ }
30
+ throw new ShardError("procgen/bad-output", `Unknown generator output ${JSON.stringify(spec)}`, {
31
+ hint: 'Use "mesh", "texture", "data", "entities", or a data type (project.dataAsset).'
32
+ });
33
+ }
34
+ function defineGenerator(name, options) {
35
+ if (!/^[a-z][a-z0-9-]*\/[A-Za-z][A-Za-z0-9]*$/.test(name)) {
36
+ throw new ShardError("procgen/bad-name", `"${name}" isn't a generator name`, {
37
+ hint: 'Generator names are "<namespace>/<Name>", e.g. "star-explorer/Rock".'
38
+ });
39
+ }
40
+ const params = defineSchema(`${name}Params`, options.params, {
41
+ description: `Parameters of the ${name} generator.`
42
+ });
43
+ const out = outputOf(options.output);
44
+ const version = options.version ?? 1;
45
+ const gen = {
46
+ type: "Generator",
47
+ guid: `generator:${name}`,
48
+ path: name,
49
+ name,
50
+ description: options.description,
51
+ params,
52
+ output: out.output,
53
+ outputType: out.outputType,
54
+ dataType: out.dataType,
55
+ version,
56
+ run: options.run
57
+ };
58
+ generators.set(name, gen);
59
+ sourceHashes.set(name, hashString(options.run.toString()));
60
+ return gen;
61
+ }
62
+ function findGenerator(name) {
63
+ return generators.get(name);
64
+ }
65
+ function requireGenerator(ref) {
66
+ const name = typeof ref === "string" ? ref : ref.name ?? ref.path ?? "";
67
+ const gen = generators.get(name);
68
+ if (!gen) {
69
+ throw new ShardError("procgen/unknown-generator", `No generator named "${name}"`, {
70
+ hint: generators.size ? `Generators: ${[...generators.keys()].sort().join(", ")}.` : "Define one with project.generator (or defineGenerator) in scripts/."
71
+ });
72
+ }
73
+ return gen;
74
+ }
75
+ function allGenerators() {
76
+ return [...generators.values()].sort((a, b) => a.name.localeCompare(b.name));
77
+ }
78
+ function setGeneratorCodeHashes(hashes) {
79
+ codeHashes.clear();
80
+ for (const [name, hash] of Object.entries(hashes)) codeHashes.set(name, hash);
81
+ }
82
+ function codeHashOf(gen) {
83
+ const name = typeof gen === "string" ? gen : gen.name;
84
+ return codeHashes.get(name) ?? sourceHashes.get(name) ?? "";
85
+ }
86
+ const F32_KINDS = /* @__PURE__ */ new Set([
87
+ "f32",
88
+ "vec2",
89
+ "vec3",
90
+ "vec4",
91
+ "quat",
92
+ "mat3",
93
+ "mat4",
94
+ "affine3x4",
95
+ "color"
96
+ ]);
97
+ function normalize(field, value) {
98
+ if (field?.kind === "handle" && isPlainObject(value))
99
+ return typeof value.path === "string" ? { path: value.path } : value;
100
+ if (field && F32_KINDS.has(field.kind)) {
101
+ if (typeof value === "number") return Math.fround(value);
102
+ if (Array.isArray(value)) return value.map((v) => typeof v === "number" ? Math.fround(v) : v);
103
+ return value;
104
+ }
105
+ if (field?.kind === "list" && Array.isArray(value))
106
+ return value.map((v) => normalize(field.item, v));
107
+ if (field?.kind === "struct" && isPlainObject(value)) return sortFields(field.fields, value);
108
+ if (Array.isArray(value)) return value.map((v) => normalize(void 0, v));
109
+ if (isPlainObject(value)) return sortFields(void 0, value);
110
+ return value;
111
+ }
112
+ function sortFields(fields, value) {
113
+ const out = {};
114
+ for (const key of Object.keys(value).sort()) out[key] = normalize(fields?.[key], value[key]);
115
+ return out;
116
+ }
117
+ function canonicalParams(gen, params = {}) {
118
+ const json = toParamJson(params ?? {});
119
+ const errors = gen.params.validate(json);
120
+ if (errors.length > 0) {
121
+ const first = errors[0];
122
+ throw new ShardError("procgen/bad-params", `${gen.name}: ${first.message}`, {
123
+ path: first.path,
124
+ hint: first.hint ?? `The ${gen.name} params are listed in .agents/generators.md.`,
125
+ details: errors
126
+ });
127
+ }
128
+ const value = gen.params.serialize(gen.params.deserialize(json));
129
+ return sortFields(gen.params.fields, value);
130
+ }
131
+ function toParamJson(value) {
132
+ if (ArrayBuffer.isView(value)) return Array.from(value);
133
+ if (Array.isArray(value)) return value.map(toParamJson);
134
+ if (value && typeof value === "object") {
135
+ const obj = value;
136
+ if (typeof obj.type === "string" && ("guid" in obj || "path" in obj) && isRef(obj)) {
137
+ const out2 = {};
138
+ if (typeof obj.guid === "string") out2.guid = obj.guid;
139
+ if (typeof obj.path === "string") out2.path = obj.path;
140
+ return out2;
141
+ }
142
+ const out = {};
143
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = toParamJson(v);
144
+ return out;
145
+ }
146
+ return value;
147
+ }
148
+ function isRef(obj) {
149
+ for (const key of Object.keys(obj)) {
150
+ if (key !== "type" && key !== "guid" && key !== "path" && typeof obj[key] !== "function")
151
+ return false;
152
+ }
153
+ return true;
154
+ }
155
+ function requestOf(gen, params, seed) {
156
+ if (!Number.isInteger(seed) || seed < 0 || seed > 4294967295) {
157
+ throw new ShardError("procgen/bad-params", `${gen.name}: seed must be a u32, got ${seed}`, {
158
+ path: "/seed",
159
+ hint: "Seeds are integers from 0 to 4294967295; derive child seeds with ctx.childSeed."
160
+ });
161
+ }
162
+ return { generator: gen.name, params: canonicalParams(gen, params), seed: seed >>> 0 };
163
+ }
164
+ function identityOf(request) {
165
+ return hashString(JSON.stringify([request.generator, request.params, request.seed]));
166
+ }
167
+ function guidOf(request) {
168
+ return `gen:${identityOf(request)}`;
169
+ }
170
+ function shortNumber(n) {
171
+ if (Math.fround(n) !== n || Number.isInteger(n)) return String(n);
172
+ for (let digits = 1; digits < 17; digits++) {
173
+ const text = String(Number(n.toPrecision(digits)));
174
+ if (Math.fround(Number(text)) === n) return text;
175
+ }
176
+ return String(n);
177
+ }
178
+ function formatValue(value) {
179
+ if (typeof value === "number") return shortNumber(value);
180
+ if (typeof value === "boolean") return String(value);
181
+ if (isPlainObject(value) && Object.keys(value).length === 1 && typeof value.path === "string")
182
+ return encodeURIComponent(value.path);
183
+ return encodeURIComponent(JSON.stringify(value));
184
+ }
185
+ function proceduralPath(request) {
186
+ const gen = requireGenerator(request.generator);
187
+ const defaults = canonicalParams(gen, {});
188
+ const parts = [];
189
+ for (const key of Object.keys(request.params)) {
190
+ const value = request.params[key];
191
+ if (JSON.stringify(value) === JSON.stringify(defaults[key])) continue;
192
+ const v = isPlainObject(value) && typeof value.path === "string" ? { path: value.path } : value;
193
+ parts.push(`${key}=${formatValue(v)}`);
194
+ }
195
+ parts.push(`seed=${request.seed}`);
196
+ return `procedural:${request.generator}?${parts.join("&")}`;
197
+ }
198
+ function parseValue(field, raw) {
199
+ const text = decodeURIComponent(raw);
200
+ if (field?.kind === "handle") return { path: text };
201
+ if (text === "true") return true;
202
+ if (text === "false") return false;
203
+ if (text !== "" && Number.isFinite(Number(text))) return Number(text);
204
+ if (/^[[{"]/.test(text)) {
205
+ try {
206
+ return JSON.parse(text);
207
+ } catch {
208
+ return text;
209
+ }
210
+ }
211
+ return text;
212
+ }
213
+ function parseProceduralRef(spec, at) {
214
+ const hash = spec.indexOf("#");
215
+ const body = hash === -1 ? spec : spec.slice(0, hash);
216
+ const q = body.indexOf("?");
217
+ const name = q === -1 ? body : body.slice(0, q);
218
+ let gen;
219
+ try {
220
+ gen = requireGenerator(name);
221
+ } catch (err) {
222
+ throw new ShardError(err.code, err.message, {
223
+ path: at,
224
+ hint: err.hint
225
+ });
226
+ }
227
+ const params = {};
228
+ let seed = 0;
229
+ if (q !== -1) {
230
+ for (const pair of body.slice(q + 1).split("&")) {
231
+ if (!pair) continue;
232
+ const eq = pair.indexOf("=");
233
+ const key = decodeURIComponent(eq === -1 ? pair : pair.slice(0, eq));
234
+ const raw = eq === -1 ? "" : pair.slice(eq + 1);
235
+ if (key === "seed") {
236
+ seed = Number(raw);
237
+ continue;
238
+ }
239
+ if (!(key in gen.params.fields)) {
240
+ throw new ShardError("procgen/bad-params", `${gen.name} has no parameter "${key}"`, {
241
+ path: at,
242
+ hint: `Parameters: ${Object.keys(gen.params.fields).join(", ") || "(none)"}, and seed.`
243
+ });
244
+ }
245
+ params[key] = parseValue(gen.params.fields[key], raw);
246
+ }
247
+ }
248
+ try {
249
+ return requestOf(gen, params, seed);
250
+ } catch (err) {
251
+ const e = err;
252
+ throw new ShardError(e.code, e.message, { path: at, hint: e.hint, details: e.details });
253
+ }
254
+ }
255
+ export {
256
+ allGenerators,
257
+ canonicalParams,
258
+ codeHashOf,
259
+ defineGenerator,
260
+ findGenerator,
261
+ guidOf,
262
+ hashString,
263
+ identityOf,
264
+ parseProceduralRef,
265
+ proceduralPath,
266
+ requestOf,
267
+ requireGenerator,
268
+ setGeneratorCodeHashes
269
+ };
@@ -0,0 +1,18 @@
1
+ import { type JsonValue } from '@aethervtt/shard-core';
2
+ export declare const generatorDependency: (id: string) => string | undefined;
3
+ export declare const generatorSchema: () => unknown;
4
+ /** The parsed body of a generator file. */
5
+ export interface GenFile {
6
+ generator: string;
7
+ seed: number;
8
+ params: Record<string, JsonValue>;
9
+ }
10
+ /** Parses and checks a `*.gen.json` file's shape (not its params). Errors point into the file. */
11
+ export declare function parseGenFile(text: string, path: string): GenFile;
12
+ /**
13
+ * `*.gen.json`: a generator, a seed, and params. Imports as the generator's output (a `Mesh` with
14
+ * `#LOD1…` sub-assets, a `Texture`, a data value, or a `Prefab`), cached like any asset, plus a
15
+ * `#Generator` sub-asset that `GeneratorInstance` can point at. Re-imports when the file, a loaded
16
+ * asset, or the generator's code changes.
17
+ */
18
+ export declare const GeneratorImporter: import("@aethervtt/shard-assets").ImporterDef;