@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 +442 -0
- package/editor_settings.d.ts +1034 -0
- package/index.d.ts +682 -0
- package/index.js +286 -0
- package/package.json +35 -0
- package/parallel/LICENSE +21 -0
- package/parallel/index.js +13 -0
- package/parallel/oni_wasm.d.ts +852 -0
- package/parallel/oni_wasm.js +2368 -0
- package/parallel/oni_wasm_bg.wasm +0 -0
- package/parallel/oni_wasm_bg.wasm.d.ts +99 -0
- package/parallel/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.js +107 -0
- package/serial/LICENSE +21 -0
- package/serial/oni_wasm.d.ts +346 -0
- package/serial/oni_wasm.js +1109 -0
- package/serial/oni_wasm_bg.wasm +0 -0
- package/serial/oni_wasm_bg.wasm.d.ts +47 -0
- package/yaml.d.ts +19 -0
- package/yaml.js +97 -0
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
|
+
}
|
package/parallel/LICENSE
ADDED
|
@@ -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';
|