@onimaxxing/worldgen 0.15.0

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/index.js ADDED
@@ -0,0 +1,286 @@
1
+ // Façade for the dual-build web package: serial/ and parallel/ each
2
+ // hold a complete wasm-bindgen build. `init()` feature-detects
3
+ // `crossOriginIsolated && SharedArrayBuffer`, picks one, and memoizes
4
+ // it. Every `worldgen.*` call routes through the chosen instance, so
5
+ // the WASM module's thread-local / static state lives in exactly one
6
+ // place per page.
7
+
8
+ export { parseKleiYaml } from './yaml.js';
9
+
10
+ let _wasm = null;
11
+ let _initPromise = null;
12
+ let _isParallel = false;
13
+
14
+ // JS-side checksum cache. The resident snapshot lives WASM-side (in
15
+ // the SNAPSHOT_SLOT thread-local); WASM also caches per-version, so
16
+ // this JS cache exists purely to skip the JS↔WASM boundary cross on
17
+ // the hot path. Mutator methods set `_checksumDirty = true`; the
18
+ // cache is also rebuilt after `worldgen.reset()`.
19
+ //
20
+ // Caveat: this relies on the wrapper being the only path that
21
+ // mutates the slot. Direct calls to `_wasm.snapshot_state_*` from
22
+ // consumer code would bypass the dirty flag and produce a stale
23
+ // checksum. Those raw exports are intended as the wrapper's
24
+ // internals, not a public API.
25
+ let _lastChecksum = null;
26
+ let _checksumDirty = true;
27
+
28
+ function _requireInit(method) {
29
+ if (_wasm === null) {
30
+ throw new Error(
31
+ `worldgen.${method}: call \`await init()\` first to instantiate the WASM module.`
32
+ );
33
+ }
34
+ return _wasm;
35
+ }
36
+
37
+ /**
38
+ * Initialise the WASM module. Memoized.
39
+ *
40
+ * @param {Object} [opts]
41
+ * @param {string | URL | Request} [opts.module_or_path] Custom .wasm URL
42
+ * passed through to wasm-bindgen's init.
43
+ * @param {number | 'auto' | 'serial'} [opts.threads='auto'] `'serial'`
44
+ * forces the serial build; a number sets an explicit rayon worker
45
+ * count; `'auto'` uses `navigator.hardwareConcurrency`.
46
+ * @returns {Promise<void>}
47
+ */
48
+ async function init(opts) {
49
+ if (_initPromise) return _initPromise;
50
+ _initPromise = (async () => {
51
+ const wantParallel =
52
+ opts?.threads !== 'serial' &&
53
+ globalThis.crossOriginIsolated === true &&
54
+ typeof SharedArrayBuffer !== 'undefined';
55
+
56
+ const mod = wantParallel
57
+ ? await import('./parallel/oni_wasm.js')
58
+ : await import('./serial/oni_wasm.js');
59
+
60
+ if (typeof mod.default === 'function') {
61
+ await mod.default(opts?.module_or_path ? { module_or_path: opts.module_or_path } : undefined);
62
+ }
63
+
64
+ if (wantParallel && typeof mod.initThreadPool === 'function') {
65
+ const threads = typeof opts?.threads === 'number'
66
+ ? opts.threads
67
+ : (globalThis.navigator?.hardwareConcurrency ?? 4);
68
+ await mod.initThreadPool(threads);
69
+ }
70
+
71
+ _wasm = mod;
72
+ _isParallel = wantParallel;
73
+ })();
74
+ return _initPromise;
75
+ }
76
+
77
+ export default init;
78
+
79
+ /** `true` for parallel, `false` for serial, `null` before `init()` resolves. */
80
+ export function isParallel() {
81
+ return _wasm === null ? null : _isParallel;
82
+ }
83
+
84
+ // Decode a v5 `settle_cluster_advance` binary snapshot.
85
+ // v5 extends v4 with the U59 backwall layer (u16 elem + f32 mass +
86
+ // f32 temp = +10 bytes per cell, 25 bytes total). Worlds without
87
+ // `backwallNoise` in their subworld YAML emit Vacuum / 0 / 0.
88
+ function decodeSnapshot(buf) {
89
+ if (!buf || buf.length === 0) {
90
+ throw new Error(
91
+ 'settle_cluster_advance returned an empty buffer — no cached cluster, or tick out of range. Did you call worldgen.generate(coord) first?'
92
+ );
93
+ }
94
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
95
+ let offset = 0;
96
+ const version = view.getUint32(offset, true); offset += 4;
97
+ if (version !== 5) {
98
+ throw new Error(`decodeSnapshot: expected version 5, got ${version}`);
99
+ }
100
+ const tick = view.getUint32(offset, true); offset += 4;
101
+ const worldCount = view.getUint32(offset, true); offset += 4;
102
+
103
+ const worlds = [];
104
+ for (let w = 0; w < worldCount; w++) {
105
+ const width = view.getUint32(offset, true); offset += 4;
106
+ const height = view.getUint32(offset, true); offset += 4;
107
+ const cells = width * height;
108
+
109
+ const element_idx = new Uint16Array(cells);
110
+ const mass = new Float32Array(cells);
111
+ const temperature = new Float32Array(cells);
112
+ const disease_idx = new Uint8Array(cells);
113
+ const disease_count = new Int32Array(cells);
114
+ const backwall_element_idx = new Uint16Array(cells);
115
+ const backwall_mass = new Float32Array(cells);
116
+ const backwall_temperature = new Float32Array(cells);
117
+
118
+ for (let c = 0; c < cells; c++) {
119
+ element_idx[c] = view.getUint16(offset, true); offset += 2;
120
+ mass[c] = view.getFloat32(offset, true); offset += 4;
121
+ temperature[c] = view.getFloat32(offset, true); offset += 4;
122
+ disease_idx[c] = view.getUint8(offset); offset += 1;
123
+ disease_count[c] = view.getInt32(offset, true); offset += 4;
124
+ backwall_element_idx[c] = view.getUint16(offset, true); offset += 2;
125
+ backwall_mass[c] = view.getFloat32(offset, true); offset += 4;
126
+ backwall_temperature[c] = view.getFloat32(offset, true); offset += 4;
127
+ }
128
+
129
+ worlds.push({
130
+ width, height,
131
+ element_idx, mass, temperature,
132
+ disease_idx, disease_count,
133
+ backwall_element_idx, backwall_mass, backwall_temperature,
134
+ });
135
+ }
136
+
137
+ return { tick, worlds };
138
+ }
139
+
140
+ // Singleton that backs the one-slot CLUSTER_CACHE in WASM. All methods
141
+ // except `generate` operate on whatever's currently cached; call
142
+ // `generate(coord)` first to populate it.
143
+ export const worldgen = {
144
+ generate(coord) {
145
+ // Typed-array transport: per-cell grids arrive as Uint16Array /
146
+ // Float32Array / Uint8Array / Int32Array — no JSON.parse.
147
+ return _requireInit('generate').generate_map_data(coord);
148
+ },
149
+ advance(tick) {
150
+ return decodeSnapshot(_requireInit('advance').settle_cluster_advance(tick));
151
+ },
152
+ entities() {
153
+ return JSON.parse(_requireInit('entities').get_entity_spawners());
154
+ },
155
+ clear() {
156
+ _requireInit('clear').clear_cluster_cache();
157
+ },
158
+
159
+ // Snapshot-based settings I/O. Each entry of the `files` map keys a
160
+ // Klei config path (e.g. `expansion1::worldgen/clusters/CGMCluster.yaml`)
161
+ // to that file's JSON-encoded content. `load*`/`merge`/`remove` calls
162
+ // evict the cluster cache.
163
+ //
164
+ // The resident snapshot lives WASM-side in a thread-local slot —
165
+ // mutations operate in-place on the slot without round-tripping the
166
+ // ~10 MB snapshot JSON across the JS↔WASM boundary. Each mutation
167
+ // returns a new monotonic version number; JS uses it to invalidate
168
+ // the local checksum cache. `files()` and `checksum()` are
169
+ // additionally cached on the WASM side per version.
170
+ snapshot: {
171
+ /** Reset SETTINGS to the embedded vanilla baseline. Strict mode
172
+ * (closed parity surface — rejects mod-defined enum extensions
173
+ * like custom TemperatureRange values). */
174
+ loadVanilla() {
175
+ _requireInit('snapshot.loadVanilla').snapshot_state_vanilla();
176
+ _checksumDirty = true;
177
+ },
178
+ /** Extensions-mode `loadVanilla`. Identical bytes for the vanilla
179
+ * case; the difference surfaces on subsequent `merge(modFiles)`
180
+ * calls, where Extensions mode accepts `TemperatureRange::Custom(X)`
181
+ * etc. as long as the mod also registers them in
182
+ * `tables.temperatures.add`. Use this when bootstrapping for a
183
+ * mod overlay (Baator HellHot, custom-tag flows). */
184
+ loadVanillaExtensions() {
185
+ _requireInit('snapshot.loadVanillaExtensions').snapshot_state_vanilla_extensions();
186
+ _checksumDirty = true;
187
+ },
188
+ /** Replace SETTINGS with the `path → JSON-string` `files` map.
189
+ * Strict mode. Atomic — invalid input leaves SETTINGS untouched
190
+ * and throws. */
191
+ load(files) {
192
+ _requireInit('snapshot.load').snapshot_state_load(JSON.stringify(files));
193
+ _checksumDirty = true;
194
+ },
195
+ /** Extensions-mode counterpart of `load`. Accepts mod-defined
196
+ * enum extensions when the mod registers them. Subsequent
197
+ * `merge`s inherit the Extensions mode automatically. */
198
+ loadExtensions(files) {
199
+ _requireInit('snapshot.loadExtensions').snapshot_state_load_extensions(JSON.stringify(files));
200
+ _checksumDirty = true;
201
+ },
202
+ /** Merge `files` into the active SETTINGS (last-wins on duplicate
203
+ * paths). Auto-loads vanilla as the base if SETTINGS hasn't been
204
+ * initialised. Atomic — invalid input leaves SETTINGS untouched
205
+ * and throws. */
206
+ merge(files) {
207
+ _requireInit('snapshot.merge').snapshot_state_merge(JSON.stringify(files));
208
+ _checksumDirty = true;
209
+ },
210
+ /** Return the active SETTINGS as a `path → JSON-string` files map.
211
+ * Auto-loads vanilla if SETTINGS hasn't been initialised. Fresh
212
+ * deep copy on each call. */
213
+ files() {
214
+ // WASM caches the emitted JSON string per version; we JSON.parse
215
+ // each call to honor the "fresh deep copy" contract.
216
+ return JSON.parse(_requireInit('snapshot.files').snapshot_state_files());
217
+ },
218
+ /** Engine-side stable hex hash of the active SETTINGS, canonical
219
+ * across HashMap/IndexMap iteration noise. Auto-loads vanilla if
220
+ * SETTINGS hasn't been initialised. Two-tier cache: WASM-side
221
+ * (per snapshot version) + JS-side (no boundary cross). Hot path
222
+ * is a boolean check + cached string return. */
223
+ checksum() {
224
+ if (!_checksumDirty && _lastChecksum !== null) {
225
+ return _lastChecksum;
226
+ }
227
+ _lastChecksum = _requireInit('snapshot.checksum').snapshot_state_checksum();
228
+ _checksumDirty = false;
229
+ return _lastChecksum;
230
+ },
231
+ /** Drop `paths` from the active SETTINGS. Auto-loads vanilla as
232
+ * the base if SETTINGS hasn't been initialised. Paths not present
233
+ * are silently ignored. Closes the additive-only gap in `merge`. */
234
+ remove(paths) {
235
+ if (!paths || paths.length === 0) return;
236
+ _requireInit('snapshot.remove').snapshot_state_drop_paths(JSON.stringify(paths));
237
+ _checksumDirty = true;
238
+ },
239
+ /** Engine schema version stamp — the ONI game build this WASM was
240
+ * baselined against (e.g. `"737195"`). Same value as
241
+ * `worldgen.version()`. */
242
+ schemaVersion() {
243
+ return _requireInit('snapshot.schemaVersion').game_version();
244
+ },
245
+ },
246
+
247
+ // Drop both the SettingsCache and the cluster cache. Next generate()
248
+ // reloads the embedded defaults. Also clears the snapshot slot via
249
+ // settings_reset's cascade.
250
+ reset() {
251
+ _requireInit('reset').settings_reset();
252
+ _lastChecksum = null;
253
+ _checksumDirty = true;
254
+ },
255
+
256
+ // Noise editor preview. `tree` is either `{kind:'named', name:'noise/Heat'}`
257
+ // (read from the live SettingsCache the editor mutates) or
258
+ // `{kind:'inline', yaml:'<yaml document>'}` (parsed in isolation). The
259
+ // returned Float32Array is row-major `y * width + x`, unnormalised.
260
+ evaluateNoise(tree, width, height, seed) {
261
+ if (!tree || typeof tree !== 'object') {
262
+ throw new TypeError("evaluateNoise: tree must be { kind: 'named'|'inline', ... }");
263
+ }
264
+ const kind = tree.kind;
265
+ let value;
266
+ if (kind === 'named') {
267
+ value = tree.name;
268
+ } else if (kind === 'inline') {
269
+ value = tree.yaml;
270
+ } else {
271
+ throw new TypeError(`evaluateNoise: unknown tree.kind '${kind}'`);
272
+ }
273
+ return _requireInit('evaluateNoise').evaluate_noise(kind, value, width, height, seed);
274
+ },
275
+
276
+ // Enumerate every noise-node variant the engine recognises plus its
277
+ // param schema. Drives the editor's "Add node" palette + per-node
278
+ // form generation. Static across the lifetime of the build.
279
+ noiseNodeKinds() {
280
+ return JSON.parse(_requireInit('noiseNodeKinds').noise_node_kinds());
281
+ },
282
+
283
+ version() {
284
+ return _requireInit('version').game_version();
285
+ },
286
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@onimaxxing/worldgen",
3
+ "type": "module",
4
+ "description": "WASM entry point for ONI worldgen",
5
+ "version": "0.15.0",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/tigin-backwards/help-i-want-to-customize-my-oxygen-not-included-worldgen.git"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "yaml.js",
14
+ "index.d.ts",
15
+ "yaml.d.ts",
16
+ "editor_settings.d.ts",
17
+ "README.md",
18
+ "serial",
19
+ "serial/**",
20
+ "parallel",
21
+ "parallel/**"
22
+ ],
23
+ "main": "index.js",
24
+ "types": "index.d.ts",
25
+ "sideEffects": [
26
+ "./parallel/snippets/**"
27
+ ],
28
+ "dependencies": {
29
+ "js-yaml": "^4.1.0"
30
+ },
31
+ "publishConfig": {
32
+ "registry": "https://registry.npmjs.org/",
33
+ "access": "public"
34
+ }
35
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tigin-backwards
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.
@@ -0,0 +1,13 @@
1
+ // Worker-bootstrap shim. wasm-bindgen-rayon's workerHelpers.js does
2
+ // `await import('../../..')` from
3
+ // `parallel/snippets/wasm-bindgen-rayon-<hash>/src/workerHelpers.js`,
4
+ // which resolves to `parallel/`, not the package root. Without an
5
+ // index here, worker spawn fails with a module resolution error.
6
+ //
7
+ // `default` MUST be re-exported alongside `wbg_rayon_start_worker`:
8
+ // workerHelpers calls `pkg.default(init)` to initialise wasm in the
9
+ // worker thread. Omitting `default` here makes that property reach
10
+ // for an undefined export, which under rsbuild/webpack tree-shaking
11
+ // surfaces as `o.default is not a function` at worker start.
12
+
13
+ export { default, wbg_rayon_start_worker } from './oni_wasm.js';