@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/README.md ADDED
@@ -0,0 +1,442 @@
1
+ # @onimaxxing/worldgen
2
+
3
+ A WASM build of the Oxygen Not Included worldgen engine. Returns the
4
+ same cluster the game would generate for a given coordinate.
5
+
6
+ ## What it covers
7
+
8
+ - Cluster layout and starmap (world positions, POIs, rocket destinations).
9
+ - Per-world element grid, biome polygons, and world traits.
10
+ - Geyser, building, pickupable, and other entity spawn positions.
11
+ - Per-cell mass, temperature, and disease (raw after `worldgen.generate`; settled after `worldgen.advance`).
12
+ - Both basegame and Spaced Out clusters, including story traits, mixing codes, and DLC toggles.
13
+
14
+ Output is verified cell-by-cell against snapshots captured from the
15
+ running game, with a slight modification to force determinism during
16
+ the physics pass. The physics pass itself (temperatures, gas/liquid
17
+ displacement, critter and plant placement) is exposed as
18
+ `worldgen.advance`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ # Browsers + ESM bundlers
24
+ npm install @onimaxxing/worldgen
25
+
26
+ # Node scripts, CLIs, SSR
27
+ npm install @onimaxxing/worldgen-node
28
+ ```
29
+
30
+ Same API on both. The web package ships both a serial build and a
31
+ parallel (rayon + `SharedArrayBuffer`) build; `init()` feature-detects
32
+ the host and picks one. The Node package is serial-only.
33
+
34
+ ## Quick start
35
+
36
+ **Browsers:**
37
+
38
+ ```ts
39
+ import init, { worldgen } from
40
+ '@onimaxxing/worldgen';
41
+
42
+ await init();
43
+ const map = worldgen.generate('V-SNDST-C-42-0-4A-MUWF1');
44
+ // map.worlds[0].element_idx element grid, width * height
45
+ // map.worlds[0].biome_cells overworld biome polygons
46
+ // map.worlds[0].geysers geyser spawn positions
47
+ // map.element_table element names by index
48
+ // map.starmap hex grid world locations
49
+ ```
50
+
51
+ **Node:**
52
+
53
+ ```ts
54
+ // ESM
55
+ import { worldgen } from
56
+ '@onimaxxing/worldgen-node';
57
+
58
+ const map = worldgen.generate('V-SNDST-C-42-0-4A-MUWF1');
59
+ ```
60
+
61
+ ```js
62
+ // CommonJS
63
+ const { worldgen } =
64
+ require('@onimaxxing/worldgen-node');
65
+
66
+ const map = worldgen.generate('V-SNDST-C-42-0-4A-MUWF1');
67
+ ```
68
+
69
+ `init()` is a no-op on Node (WASM is loaded synchronously at import
70
+ time), so the same code runs on both targets.
71
+
72
+ Types ship in the package `.d.ts`:
73
+
74
+ ```ts
75
+ import type { MapData, SettleSnapshot } from
76
+ '@onimaxxing/worldgen';
77
+ ```
78
+
79
+ ### Parallel vs serial (web only)
80
+
81
+ The web package ships two complete wasm-bindgen builds under
82
+ `serial/` and `parallel/`. `init()` picks one at runtime:
83
+
84
+ - **Parallel:** loaded when `globalThis.crossOriginIsolated === true`
85
+ and `SharedArrayBuffer` is available. Runs the settle sim across a
86
+ rayon worker pool (`navigator.hardwareConcurrency` workers by
87
+ default; override with `init({ threads: N })`). Requires COOP/COEP
88
+ response headers on the page.
89
+ - **Serial:** loaded otherwise. No worker pool, no shared memory, no
90
+ headers required.
91
+
92
+ ### Loading the WASM binary
93
+
94
+ `await init()` with no arguments resolves the `.wasm` via the
95
+ bundler-rewritten `new URL('oni_wasm_bg.wasm', import.meta.url)`
96
+ pattern inside the selected build. For hosts that don't rewrite
97
+ that pattern, pass the URL explicitly:
98
+
99
+ ```ts
100
+ // Served from your own origin
101
+ await init({ module_or_path: '/wasm/oni_wasm_bg.wasm' });
102
+
103
+ // Pre-fetched bytes or a compiled WebAssembly.Module
104
+ await init({ module_or_path: await fetch(url) });
105
+ await init({ module_or_path: myWebAssemblyModule });
106
+ ```
107
+
108
+ The URL is interpreted by the selected build; to pin a specific
109
+ `.wasm`, also pass `threads: 'serial'`.
110
+
111
+ ### Coordinates
112
+
113
+ Standard game coords. Examples:
114
+
115
+ - `SNDST-A-42-0-0-0` basegame Sandstone, seed 42, no stories or mixing
116
+ - `V-SNDST-C-42-0-4A-MUWF1` Spaced Out Vanilla Sandstone, all stories and mixing
117
+ - `CER-C-100-0-4A-MUWF1` Spaced Out Ceres, seed 100
118
+
119
+ ## API
120
+
121
+ The public surface is a default `init` export and a `worldgen`
122
+ singleton wrapping the one-slot cluster cache. `worldgen.generate(coord)`
123
+ populates the cache; every other method operates on whatever's
124
+ currently in it.
125
+
126
+ ### Generation
127
+
128
+ ```ts
129
+ worldgen.generate(coord: string): MapData
130
+ ```
131
+
132
+ Generate a cluster from a game coordinate and return the decoded
133
+ map (see [`MapData` shape](#mapdata-shape)). Caches the cluster
134
+ internally, replacing anything previously cached.
135
+
136
+ ```ts
137
+ worldgen.entities(): EntitySpawners
138
+ ```
139
+
140
+ Re-run ambient mob spawning against the cached cluster's current
141
+ cell state and return the refreshed entity lists. Call after
142
+ `worldgen.advance` chunks when settled liquid/gas flow may have
143
+ invalidated earlier mob placements. Requires a prior
144
+ `worldgen.generate`. Template-placed entities (geysers, oil wells,
145
+ props) persist across calls.
146
+
147
+ ```ts
148
+ worldgen.version(): string
149
+ ```
150
+
151
+ The ONI game build this WASM was baselined against, e.g. `"737195"`.
152
+
153
+ ### Settle simulation
154
+
155
+ Worldgen takes hundreds of milliseconds; settling takes several
156
+ seconds. The API is split so you can show a preview first and fill
157
+ in settled data progressively.
158
+
159
+ ```ts
160
+ worldgen.advance(targetTick: number): SettleSnapshot
161
+ ```
162
+
163
+ Advance the cached cluster's settle sim to `targetTick` (in
164
+ `1..=499`) and return a decoded snapshot at that tick. Call
165
+ repeatedly with increasing ticks to paint intermediate frames.
166
+ The settle runs 499 ticks total; the last call with
167
+ `targetTick = 499` finalises the cluster.
168
+
169
+ ```ts
170
+ const preview = worldgen.generate(coord);
171
+ renderPreview(preview);
172
+
173
+ for (let tick = 50; tick < 499; tick += 50) {
174
+ const snapshot = worldgen.advance(tick);
175
+ renderFrame(snapshot);
176
+ }
177
+ const final = worldgen.advance(499);
178
+ renderFrame(final);
179
+
180
+ worldgen.clear();
181
+ ```
182
+
183
+ Throws if the cache is empty or `targetTick` is out of range.
184
+
185
+ ### Editor
186
+
187
+ The active settings live in a single slot exposed as a `{ path → JSON-string }`
188
+ files map. The path keys are Klei's config paths (e.g.
189
+ `expansion1::worldgen/clusters/CGMCluster.yaml`); each value is the
190
+ JSON-encoded file body.
191
+
192
+ ```ts
193
+ const files = worldgen.snapshot.files();
194
+ const cluster = JSON.parse(files['expansion1::worldgen/clusters/CGMCluster.yaml']);
195
+ //...mutate cluster...
196
+ files['expansion1::worldgen/clusters/CGMCluster.yaml'] = JSON.stringify(cluster);
197
+ worldgen.snapshot.load(files);
198
+ // next worldgen.generate(...) uses the mutated settings
199
+ ```
200
+
201
+ | Method | Description |
202
+ |---|---|
203
+ | `worldgen.snapshot.loadVanilla()` | Reset SETTINGS to the embedded baseline. Strict mode. |
204
+ | `worldgen.snapshot.loadVanillaExtensions()` | Same, Extensions mode. Accepts mod-defined enum extensions on subsequent `merge`s. |
205
+ | `worldgen.snapshot.load(files)` | Replace SETTINGS with `files`. Strict; atomic. Throws and leaves SETTINGS untouched on invalid input. |
206
+ | `worldgen.snapshot.loadExtensions(files)` | Extensions-mode counterpart of `load`. |
207
+ | `worldgen.snapshot.merge(files)` | Last-wins merge into the current SETTINGS. Auto-loads vanilla if empty. The typical mod-overlay entry point. |
208
+ | `worldgen.snapshot.remove(paths)` | Drop `paths` from the current SETTINGS. |
209
+ | `worldgen.snapshot.files()` | Fresh deep-copy of the current SETTINGS. `load(files())` is a no-op round-trip. |
210
+ | `worldgen.snapshot.checksum()` | Stable hex hash of the current snapshot. Same content → same hash regardless of load history. |
211
+ | `worldgen.snapshot.schemaVersion()` | The ONI game build (e.g. `"737195"`). Persist alongside snapshot bytes for migration checks. |
212
+
213
+ For ingesting mod YAML, the package also exports `parseKleiYaml`:
214
+
215
+ ```ts
216
+ import { parseKleiYaml } from '@onimaxxing/worldgen';
217
+ import { readFileSync } from 'node:fs';
218
+
219
+ const json = parseKleiYaml(readFileSync('mod/cluster.yaml', 'utf8'));
220
+ worldgen.snapshot.merge({
221
+ 'expansion1::worldgen/clusters/MyMod.yaml': JSON.stringify(json),
222
+ });
223
+ ```
224
+
225
+ Any `load*` / `merge` / `remove` invalidates the cluster cache.
226
+
227
+ ### Cache lifecycle
228
+
229
+ ```ts
230
+ worldgen.clear(): void
231
+ worldgen.reset(): void
232
+ ```
233
+
234
+ - `clear()` drops the cached generated cluster; snapshot edits persist.
235
+ - `reset()` drops both the cluster cache and the snapshot; next
236
+ `generate()` reloads the embedded stock settings from scratch.
237
+
238
+ Cache semantics:
239
+
240
+ - **One slot** for the cluster cache. New `generate()` replaces it.
241
+ - **Per-worker.** Both caches are thread-local in WASM, so each Web
242
+ Worker (or Node worker thread) has its own independent state.
243
+ - **Snapshot edits evict the cluster cache.** Mutated settings make
244
+ the cached cluster stale.
245
+
246
+ ### Running in a web worker
247
+
248
+ The WASM module is several megabytes and worldgen can block the
249
+ thread for hundreds of milliseconds; settling several seconds. Run
250
+ it in a worker to keep the main thread responsive.
251
+
252
+ ```ts
253
+ // worker.ts
254
+ import init, { worldgen } from
255
+ '@onimaxxing/worldgen';
256
+
257
+ await init();
258
+
259
+ self.onmessage = async ({ data: { coord } }) => {
260
+ const preview = worldgen.generate(coord);
261
+ self.postMessage({ kind: 'preview', data: preview });
262
+
263
+ for (let tick = 50; tick < 499; tick += 50) {
264
+ const snapshot = worldgen.advance(tick);
265
+ self.postMessage({ kind: 'frame', data: snapshot });
266
+ }
267
+ self.postMessage({ kind: 'frame', data: worldgen.advance(499) });
268
+
269
+ worldgen.clear();
270
+ };
271
+ ```
272
+
273
+ ```ts
274
+ // main thread
275
+ const worker = new Worker(new URL('./worker.ts', import.meta.url),
276
+ { type: 'module' });
277
+ worker.onmessage = ({ data: { kind, data } }) => {
278
+ if (kind === 'preview') renderPreview(data);
279
+ if (kind === 'frame') renderFrame(data);
280
+ };
281
+ worker.postMessage({ coord: 'V-SNDST-C-42-0-4A-MUWF1' });
282
+ ```
283
+
284
+ ### `MapData` shape
285
+
286
+ ```ts
287
+ interface MapData {
288
+ coordinate: string;
289
+ seed: number;
290
+ cluster_id: string;
291
+ coordinate_prefix: string; // short tag e.g. "SNDST-C", "V-BAD-C", "VOLCA"
292
+ element_table: string[]; // element names indexed by element_idx
293
+ starmap: StarmapEntry[]; // Spaced Out hex grid world locations
294
+ starmap_pois: StarmapPoi[]; // Spaced Out non-asteroid hex POIs
295
+ vanilla_starmap: VanillaStarmapEntry[]; // basegame rocket destinations
296
+ worlds: WorldMapData[];
297
+ failure: WorldgenFailure | null; // populated on fatal worldgen error
298
+ telemetry: WorldgenEvent[]; // fail-slow warnings (empty on clean runs)
299
+ }
300
+
301
+ interface WorldgenFailure {
302
+ stage: string; // pipeline stage that reported the error
303
+ world_index: number; // -1 for cluster-level failures
304
+ message: string;
305
+ }
306
+
307
+ interface WorldgenEvent {
308
+ category: string; // e.g. "layout", "mob_spawning", "template_rules"
309
+ message: string; // per-world entries prefixed with "world[N]:"
310
+ }
311
+
312
+ interface WorldMapData {
313
+ name: string; // world config path
314
+ width: number;
315
+ height: number;
316
+ is_starting: boolean;
317
+ world_traits: string[];
318
+ element_idx: Uint16Array; // u16 per cell, row-major (width * height)
319
+ mass: Float32Array; // f32 per cell
320
+ temperature: Float32Array; // f32 per cell
321
+ disease_idx: Uint8Array; // u8 per cell, 255 = none
322
+ disease_count: Int32Array; // i32 per cell
323
+ backwall_element_idx: Uint16Array; // U59 backwall layer; Vacuum-filled outside DLC5 aquatic biomes
324
+ backwall_mass: Float32Array; // f32 per cell, kg; 0 on Vacuum cells
325
+ backwall_temperature: Float32Array; // f32 per cell, K; 0 on Vacuum cells
326
+ biome_cells: BiomeCell[];
327
+ geysers: GeyserSpawn[];
328
+ buildings: EntitySpawn[];
329
+ pickupables: EntitySpawn[];
330
+ other_entities: EntitySpawn[];
331
+ }
332
+
333
+ interface BiomeCell {
334
+ id: number;
335
+ type: string; // subworld type path
336
+ zone_type: ZoneType | null; // see ZoneType union exported from the package
337
+ x: number;
338
+ y: number;
339
+ poly: number[]; // flat [x0,y0,x1,y1,...]
340
+ tags: string[]; // layout tags ("AtSurface", "AtDepths", distance/zone tags, subworld tags)
341
+ }
342
+
343
+ interface EntitySpawn {
344
+ tag: string; // game prefab name
345
+ cell: number; // grid cell index
346
+ x: number; // cell % width
347
+ y: number; // cell / width
348
+ connections?: number; // TemplatePrefab.connections bitmask (conduit-aware buildings only)
349
+ rotationOrientation?: string; // TemplatePrefab.rotationOrientation, e.g. "R90", "R270", "FlipH"
350
+ }
351
+
352
+ interface GeyserSpawn extends EntitySpawn {
353
+ type: string; // resolved geyser template name
354
+ // Present when geyser stats were rolled:
355
+ scaled_rate?: number;
356
+ scaled_iter_len?: number;
357
+ scaled_iter_pct?: number;
358
+ scaled_year_len?: number;
359
+ scaled_year_pct?: number;
360
+ }
361
+
362
+ interface StarmapEntry {
363
+ world_index: number;
364
+ q: number; r: number; // hex grid coords
365
+ }
366
+
367
+ interface StarmapPoi {
368
+ poi_type: string; // e.g. "HarvestableSpacePOI_*", "ArtifactSpacePOI"
369
+ q: number; r: number;
370
+ // Only on harvestable POIs:
371
+ capacity_roll?: number;
372
+ recharge_roll?: number;
373
+ total_capacity?: number;
374
+ recharge_time?: number;
375
+ }
376
+
377
+ // Basegame rocket destinations. Empty on Spaced Out clusters.
378
+ interface VanillaStarmapEntry {
379
+ type: string; // destination type id
380
+ distance: number; // distance tier
381
+ }
382
+ ```
383
+
384
+ ### `SettleSnapshot` shape
385
+
386
+ `worldgen.advance` returns typed-array views over cell data, not a
387
+ JSON array. One snapshot covers every world in the cluster.
388
+
389
+ ```ts
390
+ interface SettleSnapshot {
391
+ tick: number; // 1..=499
392
+ worlds: SettleWorld[];
393
+ }
394
+
395
+ interface SettleWorld {
396
+ width: number;
397
+ height: number;
398
+ element_idx: Uint16Array;
399
+ mass: Float32Array;
400
+ temperature: Float32Array;
401
+ disease_idx: Uint8Array; // 255 = none
402
+ disease_count: Int32Array;
403
+ backwall_element_idx: Uint16Array; // U59 backwall layer; Vacuum-filled outside DLC5 aquatic biomes
404
+ backwall_mass: Float32Array; // f32 per cell, kg; 0 on Vacuum cells
405
+ backwall_temperature: Float32Array; // f32 per cell, K; 0 on Vacuum cells
406
+ }
407
+ ```
408
+
409
+ ## Performance
410
+
411
+ Worldgen runtime, same machine both rows. Coordinate:
412
+ `V-SNDST-C-42-0-4A-MUWF1`, the Spaced Out Vanilla Sandstone cluster
413
+ at seed 42, with every story trait (`4A`) and every DLC mixing
414
+ option (`MUWF1`) enabled. 8 worlds, ~170,000 cells total.
415
+
416
+ **Time per seed:**
417
+
418
+ | Runtime | Time |
419
+ |---|---|
420
+ | In-game World Generation | 10.4 s |
421
+ | This package (Node 24) | 0.48 s |
422
+
423
+ WASM worldgen runs roughly 22x faster than the game's C# worldgen.
424
+
425
+ **Memory usage** (one worldgen at a time, measured on the same
426
+ cluster, resident memory only):
427
+
428
+ | Runtime | Working set |
429
+ |---|---|
430
+ | In-game World Generation | ~3.3 GB |
431
+ | This package (Node 24) | ~0.2 GB |
432
+
433
+ WASM worldgen uses roughly 15x less memory than the game's C# worldgen.
434
+
435
+ Package size is dominated by the two WASM modules; only one loads
436
+ at runtime. Load it in a web worker to keep download and instantiate
437
+ off the main thread.
438
+
439
+ ## License
440
+
441
+ **MIT.**
442
+