@onimaxxing/worldgen
v0.31.3
Published
WASM entry point for ONI worldgen
Readme
@onimaxxing/worldgen
A WASM build of the Oxygen Not Included worldgen engine. Returns the same cluster the game would generate for a given coordinate.
What it covers
- Cluster layout and starmap (world positions, POIs, rocket destinations).
- Per-world element grid, biome polygons, and world traits.
- Geyser, building, pickupable, and other entity spawn positions.
- Per-cell mass, temperature, and disease (raw after
worldgen.generate; settled afterworldgen.advance). - Both basegame and Spaced Out clusters, including story traits, mixing codes, and DLC toggles.
- Partial generation of selected worlds (
worldgen.partials).
Output is verified cell-by-cell against snapshots captured from the
running game, with a slight modification to force determinism during
the physics pass. The physics pass itself (temperatures, gas/liquid
displacement, critter and plant placement) is exposed as
worldgen.advance.
Install
# Browsers + ESM bundlers
npm install @onimaxxing/worldgen
# Node scripts, CLIs, SSR
npm install @onimaxxing/worldgen-nodeSame API on both. The web package ships both a serial build and a
parallel (rayon + SharedArrayBuffer) build; init() feature-detects
the host and picks one. The Node package is serial-only.
Quick start
Browsers:
import init, { worldgen } from
'@onimaxxing/worldgen';
await init();
const map = worldgen.generate('V-SNDST-C-42-0-4A-MUWF1');
// map.worlds[0].element_idx element grid, width * height
// map.worlds[0].biome_cells overworld biome polygons
// map.worlds[0].geysers geyser spawn positions
// map.element_table element names by index
// map.starmap hex grid world locationsNode:
// ESM
import { worldgen } from
'@onimaxxing/worldgen-node';
const map = worldgen.generate('V-SNDST-C-42-0-4A-MUWF1');// CommonJS
const { worldgen } =
require('@onimaxxing/worldgen-node');
const map = worldgen.generate('V-SNDST-C-42-0-4A-MUWF1');init() is a no-op on Node (WASM is loaded synchronously at import
time), so the same code runs on both targets.
Types ship in the package .d.ts:
import type { MapData, SettleSnapshot } from
'@onimaxxing/worldgen';Parallel vs serial (web only)
The web package ships two complete wasm-bindgen builds under
serial/ and parallel/. init() picks one at runtime:
- Parallel: loaded when
globalThis.crossOriginIsolated === trueandSharedArrayBufferis available. Runs the settle sim across a rayon worker pool (navigator.hardwareConcurrencyworkers by default; override withinit({ threads: N })). Requires COOP/COEP response headers on the page. - Serial: loaded otherwise. No worker pool, no shared memory, no headers required.
Loading the WASM binary
await init() with no arguments resolves the .wasm via the
bundler-rewritten new URL('oni_wasm_bg.wasm', import.meta.url)
pattern inside the selected build. For hosts that don't rewrite
that pattern, pass the URL explicitly:
// Served from your own origin
await init({ module_or_path: '/wasm/oni_wasm_bg.wasm' });
// Pre-fetched bytes or a compiled WebAssembly.Module
await init({ module_or_path: await fetch(url) });
await init({ module_or_path: myWebAssemblyModule });The URL is interpreted by the selected build; to pin a specific
.wasm, also pass threads: 'serial'.
Coordinates
Standard game coords. Examples:
SNDST-A-42-0-0-0basegame Sandstone, seed 42, no stories or mixingV-SNDST-C-42-0-4A-MUWF1Spaced Out Vanilla Sandstone, all stories and mixingCER-C-100-0-4A-MUWF1Spaced Out Ceres, seed 100
API
The public surface is a default init export and a worldgen
singleton wrapping the one-slot cluster cache. worldgen.generate(coord)
populates the cache; every other method operates on whatever's
currently in it.
Generation
worldgen.generate(coord: string): MapDataGenerate a cluster from a game coordinate and return the decoded
map (see MapData shape). Caches the cluster
internally, replacing anything previously cached.
worldgen.partials(coord: string, opts: PartialsOptions): PartialsResultGenerate only the selected worlds of a cluster. Stateless; does not touch the cluster cache.
opts.worlds selects by placement index or world path (full path or
last segment, matched after mixing). Selectors that match nothing come
back in absent rather than erroring. Cell grids are pre-settle; for
settled state use worldgen.generate + worldgen.advance.
const r = worldgen.partials('V-SNDST-C-42-0-4A-MUWF1', {
worlds: ['VanillaSandstoneDefault'],
});
for (const g of r.worlds[0]?.geysers ?? []) console.log(g.type, g.x, g.y);Geyser output matches a full generate exactly. Story-trait templates and starmap locations are not generated in partial mode.
worldgen.entities(): EntitySpawnersRe-run ambient mob spawning against the cached cluster's current
cell state and return the refreshed entity lists. Call after
worldgen.advance chunks when settled liquid/gas flow may have
invalidated earlier mob placements. Requires a prior
worldgen.generate. Template-placed entities (geysers, oil wells,
props) persist across calls.
worldgen.version(): stringThe ONI game build this WASM was baselined against, e.g. "737195".
Settle simulation
Worldgen takes hundreds of milliseconds; settling takes several seconds. The API is split so you can show a preview first and fill in settled data progressively.
worldgen.advance(targetTick: number): SettleSnapshotAdvance the cached cluster's settle sim to targetTick (in
1..=499) and return a decoded snapshot at that tick. Call
repeatedly with increasing ticks to paint intermediate frames.
The settle runs 499 ticks total; the last call with
targetTick = 499 finalises the cluster.
const preview = worldgen.generate(coord);
renderPreview(preview);
for (let tick = 50; tick < 499; tick += 50) {
const snapshot = worldgen.advance(tick);
renderFrame(snapshot);
}
const final = worldgen.advance(499);
renderFrame(final);
worldgen.clear();Throws if the cache is empty or targetTick is out of range.
Editor
The active settings live in a single slot exposed as a { path → JSON-string }
files map. The path keys are Klei's config paths (e.g.
expansion1::worldgen/clusters/CGMCluster.yaml); each value is the
JSON-encoded file body.
const files = worldgen.snapshot.files();
const cluster = JSON.parse(files['expansion1::worldgen/clusters/CGMCluster.yaml']);
//...mutate cluster...
files['expansion1::worldgen/clusters/CGMCluster.yaml'] = JSON.stringify(cluster);
worldgen.snapshot.load(files);
// next worldgen.generate(...) uses the mutated settings| Method | Description |
|---|---|
| worldgen.snapshot.loadVanilla() | Reset SETTINGS to the embedded baseline. Strict mode. |
| worldgen.snapshot.loadVanillaExtensions() | Same, Extensions mode. Accepts mod-defined enum extensions on subsequent merges. |
| worldgen.snapshot.load(files) | Replace SETTINGS with files. Strict; atomic. Throws and leaves SETTINGS untouched on invalid input. |
| worldgen.snapshot.loadExtensions(files) | Extensions-mode counterpart of load. |
| worldgen.snapshot.merge(files) | Last-wins merge into the current SETTINGS. Auto-loads vanilla if empty. The typical mod-overlay entry point. |
| worldgen.snapshot.remove(paths) | Drop paths from the current SETTINGS. |
| worldgen.snapshot.files() | Fresh deep-copy of the current SETTINGS. load(files()) is a no-op round-trip. |
| worldgen.snapshot.checksum() | Stable hex hash of the current snapshot. Same content → same hash regardless of load history. |
| worldgen.snapshot.schemaVersion() | The ONI game build (e.g. "737195"). Persist alongside snapshot bytes for migration checks. |
For ingesting mod YAML, the package also exports parseKleiYaml:
import { parseKleiYaml } from '@onimaxxing/worldgen';
import { readFileSync } from 'node:fs';
const json = parseKleiYaml(readFileSync('mod/cluster.yaml', 'utf8'));
worldgen.snapshot.merge({
'expansion1::worldgen/clusters/MyMod.yaml': JSON.stringify(json),
});Any load* / merge / remove invalidates the cluster cache.
Cache lifecycle
worldgen.clear(): void
worldgen.reset(): voidclear()drops the cached generated cluster; snapshot edits persist.reset()drops both the cluster cache and the snapshot; nextgenerate()reloads the embedded stock settings from scratch.
Cache semantics:
- One slot for the cluster cache. New
generate()replaces it. - Per-worker. Both caches are thread-local in WASM, so each Web Worker (or Node worker thread) has its own independent state.
- Snapshot edits evict the cluster cache. Mutated settings make the cached cluster stale.
Running in a web worker
The WASM module is several megabytes and worldgen can block the thread for hundreds of milliseconds; settling several seconds. Run it in a worker to keep the main thread responsive.
// worker.ts
import init, { worldgen } from
'@onimaxxing/worldgen';
await init();
self.onmessage = async ({ data: { coord } }) => {
const preview = worldgen.generate(coord);
self.postMessage({ kind: 'preview', data: preview });
for (let tick = 50; tick < 499; tick += 50) {
const snapshot = worldgen.advance(tick);
self.postMessage({ kind: 'frame', data: snapshot });
}
self.postMessage({ kind: 'frame', data: worldgen.advance(499) });
worldgen.clear();
};// main thread
const worker = new Worker(new URL('./worker.ts', import.meta.url),
{ type: 'module' });
worker.onmessage = ({ data: { kind, data } }) => {
if (kind === 'preview') renderPreview(data);
if (kind === 'frame') renderFrame(data);
};
worker.postMessage({ coord: 'V-SNDST-C-42-0-4A-MUWF1' });MapData shape
interface MapData {
coordinate: string;
seed: number;
cluster_id: string;
coordinate_prefix: string; // short tag e.g. "SNDST-C", "V-BAD-C", "VOLCA"
element_table: string[]; // element names indexed by element_idx
starmap: StarmapEntry[]; // Spaced Out hex grid world locations
starmap_pois: StarmapPoi[]; // Spaced Out non-asteroid hex POIs
vanilla_starmap: VanillaStarmapEntry[]; // basegame rocket destinations
worlds: WorldMapData[];
failure: WorldgenFailure | null; // populated on fatal worldgen error
telemetry: WorldgenEvent[]; // fail-slow warnings (empty on clean runs)
}
interface WorldgenFailure {
stage: string; // pipeline stage that reported the error
world_index: number; // -1 for cluster-level failures
message: string;
}
interface WorldgenEvent {
category: string; // e.g. "layout", "mob_spawning", "template_rules"
message: string; // per-world entries prefixed with "world[N]:"
}
interface WorldMapData {
name: string; // world config path
width: number;
height: number;
is_starting: boolean;
world_traits: string[];
element_idx: Uint16Array; // u16 per cell, row-major (width * height)
mass: Float32Array; // f32 per cell
temperature: Float32Array; // f32 per cell
disease_idx: Uint8Array; // u8 per cell, 255 = none
disease_count: Int32Array; // i32 per cell
backwall_element_idx: Uint16Array; // U59 backwall layer; Vacuum-filled outside DLC5 aquatic biomes
backwall_mass: Float32Array; // f32 per cell, kg; 0 on Vacuum cells
backwall_temperature: Float32Array; // f32 per cell, K; 0 on Vacuum cells
zone_idx: Uint16Array; // u16 per cell -> zone_table; 0xFFFF = no biome polygon
zone_table: string[]; // distinct ZoneType strings (same vocab as biome_cells[].zone_type)
subworld_idx: Uint16Array; // u16 per cell -> subworld_table; 0xFFFF = no biome polygon
subworld_table: string[]; // distinct subworld type paths (same vocab as biome_cells[].type)
biome_cells: BiomeCell[];
geysers: GeyserSpawn[];
buildings: EntitySpawn[];
pickupables: EntitySpawn[];
other_entities: EntitySpawn[];
}
interface BiomeCell {
id: number;
type: string; // subworld type path
zone_type: ZoneType | null; // see ZoneType union exported from the package
x: number;
y: number;
poly: number[]; // flat [x0,y0,x1,y1,...]
tags: string[]; // layout tags ("AtSurface", "AtDepths", distance/zone tags, subworld tags)
}
interface EntitySpawn {
tag: string; // game prefab name
cell: number; // grid cell index
x: number; // cell % width
y: number; // cell / width
connections?: number; // TemplatePrefab.connections bitmask (conduit-aware buildings only)
rotationOrientation?: string; // TemplatePrefab.rotationOrientation, e.g. "R90", "R270", "FlipH"
}
interface GeyserSpawn extends EntitySpawn {
type: string; // resolved geyser template name
// Present when geyser stats were rolled:
scaled_rate?: number;
scaled_iter_len?: number;
scaled_iter_pct?: number;
scaled_year_len?: number;
scaled_year_pct?: number;
}
interface StarmapEntry {
world_index: number;
q: number; r: number; // hex grid coords
}
interface StarmapPoi {
poi_type: string; // e.g. "HarvestableSpacePOI_*", "ArtifactSpacePOI"
q: number; r: number;
// Only on harvestable POIs:
capacity_roll?: number;
recharge_roll?: number;
total_capacity?: number;
recharge_time?: number;
}
// Basegame rocket destinations. Empty on Spaced Out clusters.
interface VanillaStarmapEntry {
type: string; // destination type id
distance: number; // distance tier
}SettleSnapshot shape
worldgen.advance returns typed-array views over cell data, not a
JSON array. One snapshot covers every world in the cluster.
interface SettleSnapshot {
tick: number; // 1..=499
worlds: SettleWorld[];
}
interface SettleWorld {
width: number;
height: number;
element_idx: Uint16Array;
mass: Float32Array;
temperature: Float32Array;
disease_idx: Uint8Array; // 255 = none
disease_count: Int32Array;
backwall_element_idx: Uint16Array; // U59 backwall layer; Vacuum-filled outside DLC5 aquatic biomes
backwall_mass: Float32Array; // f32 per cell, kg; 0 on Vacuum cells
backwall_temperature: Float32Array; // f32 per cell, K; 0 on Vacuum cells
}Performance
Worldgen runtime, same machine both rows. Coordinate:
V-SNDST-C-42-0-4A-MUWF1, the Spaced Out Vanilla Sandstone cluster
at seed 42, with every story trait (4A) and every DLC mixing
option (MUWF1) enabled. 8 worlds, ~170,000 cells total.
Time per seed:
| Runtime | Time | |---|---| | In-game World Generation | 10.4 s | | This package (Node 24) | 0.41 s |
WASM worldgen runs roughly 25x faster than the game's C# worldgen.
Memory usage (one worldgen at a time, measured on the same cluster, resident memory only):
| Runtime | Working set | |---|---| | In-game World Generation | ~3.3 GB | | This package (Node 24) | ~0.2 GB |
WASM worldgen uses roughly 15x less memory than the game's C# worldgen.
Package size is dominated by the two WASM modules; only one loads at runtime. Load it in a web worker to keep download and instantiate off the main thread.
License
MIT.
