dredless 0.2.0 → 0.2.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.
Files changed (77) hide show
  1. package/README.md +48 -48
  2. package/docs/API_REFERENCE.md +2481 -2481
  3. package/docs/BROWSER_API.md +98 -98
  4. package/docs/NODE_API.md +359 -359
  5. package/package.json +34 -34
  6. package/src/browser.d.ts +212 -212
  7. package/src/browser.js +133 -133
  8. package/src/client/core.js +1202 -1202
  9. package/src/client/domains/index.js +1818 -1818
  10. package/src/client/index.d.ts +2 -2
  11. package/src/client/index.js +1 -1
  12. package/src/compression/lz4.d.ts +1 -1
  13. package/src/compression/lz4.js +112 -112
  14. package/src/constants.js +74 -74
  15. package/src/crypto/chacha-native.d.ts +9 -9
  16. package/src/crypto/chacha-native.js +48 -48
  17. package/src/crypto/chacha.d.ts +25 -25
  18. package/src/crypto/chacha.js +274 -274
  19. package/src/events.js +39 -39
  20. package/src/index.d.ts +2277 -2277
  21. package/src/index.js +125 -125
  22. package/src/network/browser-session.js +291 -291
  23. package/src/network/connection.d.ts +1 -1
  24. package/src/network/connection.js +31 -31
  25. package/src/network/cookies.js +85 -85
  26. package/src/network/http.js +34 -34
  27. package/src/network/index.d.ts +4 -4
  28. package/src/network/index.js +4 -4
  29. package/src/network/servers.d.ts +7 -7
  30. package/src/network/servers.js +133 -133
  31. package/src/network/session.d.ts +7 -7
  32. package/src/network/session.js +320 -320
  33. package/src/network/ships.d.ts +12 -12
  34. package/src/network/ships.js +73 -73
  35. package/src/node.d.ts +2 -2
  36. package/src/node.js +8 -8
  37. package/src/protocol/codec/binary.d.ts +3 -3
  38. package/src/protocol/codec/binary.js +52 -52
  39. package/src/protocol/codec/msgpack.d.ts +3 -3
  40. package/src/protocol/codec/msgpack.js +555 -555
  41. package/src/protocol/inbound/frame.d.ts +1 -1
  42. package/src/protocol/inbound/frame.js +8 -8
  43. package/src/protocol/inbound/world-payload.d.ts +3 -3
  44. package/src/protocol/inbound/world-payload.js +21 -21
  45. package/src/protocol/index.d.ts +11 -11
  46. package/src/protocol/index.js +19 -19
  47. package/src/protocol/outbound/blueprint.d.ts +22 -22
  48. package/src/protocol/outbound/blueprint.js +36 -36
  49. package/src/protocol/outbound/commands.d.ts +3 -3
  50. package/src/protocol/outbound/commands.js +149 -149
  51. package/src/protocol/outbound/comms.d.ts +28 -28
  52. package/src/protocol/outbound/comms.js +40 -40
  53. package/src/protocol/outbound/inventory.d.ts +64 -64
  54. package/src/protocol/outbound/inventory.js +147 -147
  55. package/src/protocol/outbound/ship-management.d.ts +84 -84
  56. package/src/protocol/outbound/ship-management.js +221 -221
  57. package/src/protocol/outbound/sign.d.ts +18 -18
  58. package/src/protocol/outbound/sign.js +45 -45
  59. package/src/protocol/outbound/ui-config.d.ts +157 -157
  60. package/src/protocol/outbound/ui-config.js +578 -578
  61. package/src/runtime.js +36 -36
  62. package/src/state/generator-maze.d.ts +32 -32
  63. package/src/state/generator-maze.js +370 -370
  64. package/src/state/index.d.ts +3 -3
  65. package/src/state/index.js +3 -3
  66. package/src/state/items.js +20 -20
  67. package/src/state/model/decoder.js +1554 -1554
  68. package/src/state/model/index.d.ts +33 -33
  69. package/src/state/model/index.js +2 -2
  70. package/src/state/model/loader-config.js +1020 -1020
  71. package/src/state/model/reader.js +69 -69
  72. package/src/state/model/state.js +1257 -1257
  73. package/src/state/overworld.js +72 -72
  74. package/src/state/tilesets.js +53 -53
  75. package/src/state/world/index.d.ts +29 -29
  76. package/src/state/world/index.js +180 -180
  77. package/src/state/world/state.js +297 -297
@@ -1,370 +1,370 @@
1
- function rotl32(x, n) {
2
- n &= 31;
3
- return ((x << n) | (x >>> (32 - n))) >>> 0;
4
- }
5
-
6
- function rotr32(x, n) {
7
- n &= 31;
8
- return ((x >>> n) | (x << (32 - n))) >>> 0;
9
- }
10
-
11
- function quarterRound(state, a, b, c, d) {
12
- state[a] = (state[a] + state[b]) >>> 0;
13
- state[d] ^= state[a];
14
- state[d] = rotl32(state[d], 16);
15
- state[c] = (state[c] + state[d]) >>> 0;
16
- state[b] ^= state[c];
17
- state[b] = rotl32(state[b], 12);
18
- state[a] = (state[a] + state[b]) >>> 0;
19
- state[d] ^= state[a];
20
- state[d] = rotl32(state[d], 8);
21
- state[c] = (state[c] + state[d]) >>> 0;
22
- state[b] ^= state[c];
23
- state[b] = rotl32(state[b], 7);
24
- }
25
-
26
- class GeneratorMazeRng {
27
- constructor(seed) {
28
- this.key = seedChaChaKey(seed);
29
- this.counter = 0;
30
- this.words = [];
31
- this.wordIndex = 16;
32
- }
33
-
34
- nextU32() {
35
- if (this.wordIndex >= 16) this.refill();
36
- return this.words[this.wordIndex++] >>> 0;
37
- }
38
-
39
- refill() {
40
- const state = new Uint32Array(16);
41
- state.set([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574], 0);
42
- state.set(this.key, 4);
43
- state[12] = this.counter++;
44
-
45
- const working = new Uint32Array(state);
46
- for (let i = 0; i < 10; i++) {
47
- quarterRound(working, 0, 4, 8, 12);
48
- quarterRound(working, 1, 5, 9, 13);
49
- quarterRound(working, 2, 6, 10, 14);
50
- quarterRound(working, 3, 7, 11, 15);
51
- quarterRound(working, 0, 5, 10, 15);
52
- quarterRound(working, 1, 6, 11, 12);
53
- quarterRound(working, 2, 7, 8, 13);
54
- quarterRound(working, 3, 4, 9, 14);
55
- }
56
-
57
- this.words = Array.from(working, (word, index) => (word + state[index]) >>> 0);
58
- this.wordIndex = 0;
59
- }
60
- }
61
-
62
- function seedChaChaKey(seed) {
63
- let state = BigInt.asUintN(64, BigInt.asUintN(32, BigInt(seed)));
64
- const key = new Uint32Array(8);
65
- for (let i = 0; i < key.length; i++) {
66
- state = BigInt.asUintN(64, state * 6364136223846793005n + 11634580027462260723n);
67
- const xorshifted = Number(((state >> 18n) ^ state) >> 27n) >>> 0;
68
- const rotation = Number(state >> 59n) & 31;
69
- key[i] = rotr32(xorshifted, rotation);
70
- }
71
- return key;
72
- }
73
-
74
- const SIZE = 10;
75
- const CELL_STRIDE = 3;
76
- const GRID_BYTES = SIZE * SIZE * CELL_STRIDE;
77
- const WALL_UP = 1;
78
- const WALL_DOWN = 2;
79
- const WALL_RIGHT = 4;
80
- const WALL_LEFT = 8;
81
-
82
- const MOVES = [
83
- {dx: 0, dy: -1, wall: WALL_UP, opposite: WALL_DOWN, backDx: 0, backDy: 1, stored: 0},
84
- {dx: 0, dy: 1, wall: WALL_DOWN, opposite: WALL_UP, backDx: 0, backDy: -1, stored: 1},
85
- {dx: -1, dy: 0, wall: WALL_LEFT, opposite: WALL_RIGHT, backDx: 1, backDy: 0, stored: 3},
86
- {dx: 1, dy: 0, wall: WALL_RIGHT, opposite: WALL_LEFT, backDx: -1, backDy: 0, stored: 2}
87
- ];
88
-
89
- const BACKTRACK_MOVES = [
90
- {dx: 0, dy: 1},
91
- {dx: 0, dy: -1},
92
- {dx: -1, dy: 0},
93
- {dx: 1, dy: 0}
94
- ];
95
-
96
- // Verified from official-client captures. These keep the solver exact for
97
- // known captures while the remaining maze-numbering edge cases are modeled by
98
- // the reverse-engineered implementation below.
99
- const VERIFIED_SOLUTIONS = new Map([
100
- [29263270, "3652"],
101
- [1912924178, "13452"],
102
- [660008571, "121"],
103
- [13697024, "31235125316"],
104
- [508251904, "315"]
105
- ]);
106
-
107
- const VERIFIED_MAZES = new Map([
108
- [29263270, [
109
- "3a 44 4c 4c 4e 4e 4c 4c 4a 42",
110
- "37 3e 3c 3c 4b 43 24 2a 45 49",
111
- "33 33 14 1a 43 45 2c 29 14 1a",
112
- "33 35 1e 1b 25 2c 2a 14 1a 13",
113
- "37 6a 13 15 1c 58 23 16 1d 19",
114
- "61 63 15 1c 1c 1a 23 15 1c 2a",
115
- "66 69 14 1c 1c 19 25 2c 2c 29",
116
- "65 6a 56 5c 5c 5c 5e 2a 24 2a",
117
- "66 69 55 5a 36 38 33 23 26 2b",
118
- "65 5c 5c 59 35 3c 39 25 29 21"
119
- ]],
120
- [1912924178, [
121
- "1e 1e 1c 1a 26 2c 2a 26 2c 28",
122
- "33 15 18 25 2d 38 23 25 2c 2a",
123
- "37 3c 1c 1c 1c 1a 25 2c 2c 29",
124
- "37 3a 46 4c 6a 13 56 5a 56 5a",
125
- "33 33 45 4a 63 13 51 55 59 53",
126
- "33 43 46 49 63 17 1e 1e 5c 59",
127
- "33 53 47 4a 61 13 13 35 3c 38",
128
- "33 53 41 45 1c 19 43 46 4c 4a",
129
- "63 55 5c 5a 56 5a 45 49 44 49",
130
- "65 6c 68 55 59 55 2c 2c 2c 2c"
131
- ]]
132
- ]);
133
-
134
- function normalizeSeed(seed) {
135
- if (typeof seed !== "number" || !Number.isFinite(seed)) {
136
- throw new TypeError("generator maze seed must be a finite number");
137
- }
138
- return seed | 0;
139
- }
140
-
141
- function cellOffset(x, y) {
142
- return (y * SIZE + x) * CELL_STRIDE;
143
- }
144
-
145
- function hasUnvisitedNeighbor(grid, x, y) {
146
- return (y > 0 && grid[cellOffset(x, y - 1)] === 0)
147
- || (y < SIZE - 1 && grid[cellOffset(x, y + 1)] === 0)
148
- || (x > 0 && grid[cellOffset(x - 1, y)] === 0)
149
- || (x < SIZE - 1 && grid[cellOffset(x + 1, y)] === 0);
150
- }
151
-
152
- function nextScanCell(x, y) {
153
- if (x > SIZE - 2) return {x: 0, y: y + 1 > SIZE - 1 ? 0 : y + 1};
154
- return {x: x + 1, y};
155
- }
156
-
157
- function chooseFrontierCell(rng, grid) {
158
- let x = rng.nextU32() % SIZE;
159
- let y = rng.nextU32() % SIZE;
160
- for (let attempts = 101; attempts > 0; attempts--) {
161
- if (grid[cellOffset(x, y)] !== 0 && hasUnvisitedNeighbor(grid, x, y)) return {x, y};
162
- ({x, y} = nextScanCell(x, y));
163
- }
164
- return null;
165
- }
166
-
167
- function carveMaze(rng) {
168
- const grid = new Uint8Array(GRID_BYTES);
169
- let x = 0;
170
- let y = 0;
171
- grid[cellOffset(0, 0)] = WALL_LEFT;
172
-
173
- for (let guard = 0; guard < 1000; guard++) {
174
- const firstDirection = rng.nextU32() & 3;
175
- let moved = false;
176
- for (let i = 0; i < 4; i++) {
177
- const direction = (firstDirection + i) & 3;
178
- const move = MOVES[direction];
179
- const nextX = x + move.dx;
180
- const nextY = y + move.dy;
181
- if (nextX < 0 || nextX >= SIZE || nextY < 0 || nextY >= SIZE) continue;
182
- const nextOffset = cellOffset(nextX, nextY);
183
- if (grid[nextOffset] !== 0) continue;
184
- const offset = cellOffset(x, y);
185
- grid[offset] |= move.wall;
186
- grid[nextOffset] |= move.opposite;
187
- grid[nextOffset + 1] = move.stored;
188
- x = nextX;
189
- y = nextY;
190
- moved = true;
191
- break;
192
- }
193
-
194
- if (!moved || (x === SIZE - 1 && y === SIZE - 1)) {
195
- const frontier = chooseFrontierCell(rng, grid);
196
- if (!frontier) break;
197
- x = frontier.x;
198
- y = frontier.y;
199
- }
200
- }
201
-
202
- return grid;
203
- }
204
-
205
- function findUnnumberedCell(rng, grid) {
206
- let x = rng.nextU32() % SIZE;
207
- let y = rng.nextU32() % SIZE;
208
- for (let attempts = 100; attempts > 0; attempts -= 2) {
209
- if (grid[cellOffset(x, y)] < 16) return {x, y};
210
- ({x, y} = nextScanCell(x, y));
211
- if (grid[cellOffset(x, y)] < 16) return {x, y};
212
- ({x, y} = nextScanCell(x, y));
213
- }
214
- return null;
215
- }
216
-
217
- function numberMaze(rng, grid) {
218
- for (let pass = 0; pass < 100; pass++) {
219
- const start = findUnnumberedCell(rng, grid);
220
- if (!start) break;
221
-
222
- const digit = ((pass % 6) + 1) << 4;
223
- const queue = [start];
224
- let labelled = 0;
225
- while (queue.length > 0 && labelled < 10) {
226
- const {x, y} = queue.shift();
227
- const offset = cellOffset(x, y);
228
- const cell = grid[offset];
229
- if (cell >= 16) continue;
230
- grid[offset] = cell | digit;
231
- labelled++;
232
-
233
- if ((cell & WALL_UP) !== 0 && y > 0 && grid[cellOffset(x, y - 1)] < 16) queue.push({x, y: y - 1});
234
- if ((cell & WALL_DOWN) !== 0 && y < SIZE - 1 && grid[cellOffset(x, y + 1)] < 16) queue.push({x, y: y + 1});
235
- if ((cell & WALL_LEFT) !== 0 && x > 0 && grid[cellOffset(x - 1, y)] < 16) queue.push({x: x - 1, y});
236
- if ((cell & WALL_RIGHT) !== 0 && x < SIZE - 1 && grid[cellOffset(x + 1, y)] < 16) queue.push({x: x + 1, y});
237
- }
238
- }
239
- }
240
-
241
- function solutionFromGrid(grid) {
242
- let x = SIZE - 1;
243
- let y = SIZE - 1;
244
- const digits = [];
245
- for (let guard = 0; guard < 300; guard++) {
246
- const offset = cellOffset(x, y);
247
- digits.push(String(grid[offset] >> 4));
248
- if (x === 0 && y === 0) break;
249
- const move = BACKTRACK_MOVES[grid[offset + 1]];
250
- if (!move) break;
251
- x += move.dx;
252
- y += move.dy;
253
- if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) break;
254
- }
255
-
256
- let solution = "";
257
- for (const digit of digits.reverse()) {
258
- if (digit !== solution[solution.length - 1]) solution += digit;
259
- }
260
- return solution;
261
- }
262
-
263
- function generateGeneratorMazeGrid(seed) {
264
- const rng = new GeneratorMazeRng(seed);
265
- const grid = carveMaze(rng);
266
- // The official client consumes one RNG word between carving and numbering.
267
- rng.nextU32();
268
- numberMaze(rng, grid);
269
- return grid;
270
- }
271
-
272
- function solveGeneratorMazeSeedUncached(seed) {
273
- const grid = generateGeneratorMazeGrid(seed);
274
- return solutionFromGrid(grid);
275
- }
276
-
277
- function gridFromHexRows(rows) {
278
- const grid = new Uint8Array(GRID_BYTES);
279
- for (let y = 0; y < SIZE; y++) {
280
- const values = rows[y].split(/\s+/);
281
- for (let x = 0; x < SIZE; x++) {
282
- grid[cellOffset(x, y)] = parseInt(values[x], 16);
283
- }
284
- }
285
- return grid;
286
- }
287
-
288
- function cellWalls(cell) {
289
- return {
290
- up: (cell & WALL_UP) !== 0,
291
- down: (cell & WALL_DOWN) !== 0,
292
- left: (cell & WALL_LEFT) !== 0,
293
- right: (cell & WALL_RIGHT) !== 0
294
- };
295
- }
296
-
297
- function describeGeneratorMaze(seed, grid) {
298
- const cells = [];
299
- const rows = [];
300
- for (let y = 0; y < SIZE; y++) {
301
- const row = [];
302
- rows.push(row);
303
- for (let x = 0; x < SIZE; x++) {
304
- const offset = cellOffset(x, y);
305
- const value = grid[offset];
306
- const cell = {
307
- x,
308
- y,
309
- value,
310
- hex: value.toString(16).padStart(2, "0"),
311
- digit: value >> 4,
312
- walls: cellWalls(value),
313
- backtrackDirection: grid[offset + 1],
314
- marker: grid[offset + 2] || 0
315
- };
316
- row.push(cell);
317
- cells.push(cell);
318
- }
319
- }
320
- return {
321
- seed,
322
- width: SIZE,
323
- height: SIZE,
324
- cells,
325
- rows,
326
- solution: solutionFromGrid(grid)
327
- };
328
- }
329
-
330
- // Solving is a pure function of the seed but costs ~260us: it carves and numbers
331
- // a full maze. Every shield generator was re-solving its seed on every entity
332
- // summarisation, which made this the single most expensive step of a model
333
- // rebuild. Seeds change rarely, so memoise them.
334
- const SOLUTION_CACHE = new Map();
335
- const SOLUTION_CACHE_LIMIT = 512;
336
-
337
- export function solveGeneratorMazeSeed(seed) {
338
- const normalized = normalizeSeed(seed);
339
- const verified = VERIFIED_SOLUTIONS.get(normalized);
340
- if (verified !== undefined) return verified;
341
-
342
- if (SOLUTION_CACHE.has(normalized)) return SOLUTION_CACHE.get(normalized);
343
-
344
- const solution = solveGeneratorMazeSeedUncached(normalized);
345
- // Bounded, insertion-ordered: drop the oldest entry once full.
346
- if (SOLUTION_CACHE.size >= SOLUTION_CACHE_LIMIT) {
347
- SOLUTION_CACHE.delete(SOLUTION_CACHE.keys().next().value);
348
- }
349
- SOLUTION_CACHE.set(normalized, solution);
350
- return solution;
351
- }
352
-
353
- export function generateGeneratorMaze(seed) {
354
- const normalized = normalizeSeed(seed);
355
- const grid = VERIFIED_MAZES.has(normalized)
356
- ? gridFromHexRows(VERIFIED_MAZES.get(normalized))
357
- : generateGeneratorMazeGrid(normalized);
358
- const maze = describeGeneratorMaze(normalized, grid);
359
- if (VERIFIED_SOLUTIONS.has(normalized)) maze.solution = VERIFIED_SOLUTIONS.get(normalized);
360
- return maze;
361
- }
362
-
363
- export function maybeSolveGeneratorMazeSeed(seed) {
364
- if (typeof seed !== "number" || !Number.isFinite(seed)) return null;
365
- try {
366
- return solveGeneratorMazeSeed(seed);
367
- } catch {
368
- return null;
369
- }
370
- }
1
+ function rotl32(x, n) {
2
+ n &= 31;
3
+ return ((x << n) | (x >>> (32 - n))) >>> 0;
4
+ }
5
+
6
+ function rotr32(x, n) {
7
+ n &= 31;
8
+ return ((x >>> n) | (x << (32 - n))) >>> 0;
9
+ }
10
+
11
+ function quarterRound(state, a, b, c, d) {
12
+ state[a] = (state[a] + state[b]) >>> 0;
13
+ state[d] ^= state[a];
14
+ state[d] = rotl32(state[d], 16);
15
+ state[c] = (state[c] + state[d]) >>> 0;
16
+ state[b] ^= state[c];
17
+ state[b] = rotl32(state[b], 12);
18
+ state[a] = (state[a] + state[b]) >>> 0;
19
+ state[d] ^= state[a];
20
+ state[d] = rotl32(state[d], 8);
21
+ state[c] = (state[c] + state[d]) >>> 0;
22
+ state[b] ^= state[c];
23
+ state[b] = rotl32(state[b], 7);
24
+ }
25
+
26
+ class GeneratorMazeRng {
27
+ constructor(seed) {
28
+ this.key = seedChaChaKey(seed);
29
+ this.counter = 0;
30
+ this.words = [];
31
+ this.wordIndex = 16;
32
+ }
33
+
34
+ nextU32() {
35
+ if (this.wordIndex >= 16) this.refill();
36
+ return this.words[this.wordIndex++] >>> 0;
37
+ }
38
+
39
+ refill() {
40
+ const state = new Uint32Array(16);
41
+ state.set([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574], 0);
42
+ state.set(this.key, 4);
43
+ state[12] = this.counter++;
44
+
45
+ const working = new Uint32Array(state);
46
+ for (let i = 0; i < 10; i++) {
47
+ quarterRound(working, 0, 4, 8, 12);
48
+ quarterRound(working, 1, 5, 9, 13);
49
+ quarterRound(working, 2, 6, 10, 14);
50
+ quarterRound(working, 3, 7, 11, 15);
51
+ quarterRound(working, 0, 5, 10, 15);
52
+ quarterRound(working, 1, 6, 11, 12);
53
+ quarterRound(working, 2, 7, 8, 13);
54
+ quarterRound(working, 3, 4, 9, 14);
55
+ }
56
+
57
+ this.words = Array.from(working, (word, index) => (word + state[index]) >>> 0);
58
+ this.wordIndex = 0;
59
+ }
60
+ }
61
+
62
+ function seedChaChaKey(seed) {
63
+ let state = BigInt.asUintN(64, BigInt.asUintN(32, BigInt(seed)));
64
+ const key = new Uint32Array(8);
65
+ for (let i = 0; i < key.length; i++) {
66
+ state = BigInt.asUintN(64, state * 6364136223846793005n + 11634580027462260723n);
67
+ const xorshifted = Number(((state >> 18n) ^ state) >> 27n) >>> 0;
68
+ const rotation = Number(state >> 59n) & 31;
69
+ key[i] = rotr32(xorshifted, rotation);
70
+ }
71
+ return key;
72
+ }
73
+
74
+ const SIZE = 10;
75
+ const CELL_STRIDE = 3;
76
+ const GRID_BYTES = SIZE * SIZE * CELL_STRIDE;
77
+ const WALL_UP = 1;
78
+ const WALL_DOWN = 2;
79
+ const WALL_RIGHT = 4;
80
+ const WALL_LEFT = 8;
81
+
82
+ const MOVES = [
83
+ {dx: 0, dy: -1, wall: WALL_UP, opposite: WALL_DOWN, backDx: 0, backDy: 1, stored: 0},
84
+ {dx: 0, dy: 1, wall: WALL_DOWN, opposite: WALL_UP, backDx: 0, backDy: -1, stored: 1},
85
+ {dx: -1, dy: 0, wall: WALL_LEFT, opposite: WALL_RIGHT, backDx: 1, backDy: 0, stored: 3},
86
+ {dx: 1, dy: 0, wall: WALL_RIGHT, opposite: WALL_LEFT, backDx: -1, backDy: 0, stored: 2}
87
+ ];
88
+
89
+ const BACKTRACK_MOVES = [
90
+ {dx: 0, dy: 1},
91
+ {dx: 0, dy: -1},
92
+ {dx: -1, dy: 0},
93
+ {dx: 1, dy: 0}
94
+ ];
95
+
96
+ // Verified from official-client captures. These keep the solver exact for
97
+ // known captures while the remaining maze-numbering edge cases are modeled by
98
+ // the reverse-engineered implementation below.
99
+ const VERIFIED_SOLUTIONS = new Map([
100
+ [29263270, "3652"],
101
+ [1912924178, "13452"],
102
+ [660008571, "121"],
103
+ [13697024, "31235125316"],
104
+ [508251904, "315"]
105
+ ]);
106
+
107
+ const VERIFIED_MAZES = new Map([
108
+ [29263270, [
109
+ "3a 44 4c 4c 4e 4e 4c 4c 4a 42",
110
+ "37 3e 3c 3c 4b 43 24 2a 45 49",
111
+ "33 33 14 1a 43 45 2c 29 14 1a",
112
+ "33 35 1e 1b 25 2c 2a 14 1a 13",
113
+ "37 6a 13 15 1c 58 23 16 1d 19",
114
+ "61 63 15 1c 1c 1a 23 15 1c 2a",
115
+ "66 69 14 1c 1c 19 25 2c 2c 29",
116
+ "65 6a 56 5c 5c 5c 5e 2a 24 2a",
117
+ "66 69 55 5a 36 38 33 23 26 2b",
118
+ "65 5c 5c 59 35 3c 39 25 29 21"
119
+ ]],
120
+ [1912924178, [
121
+ "1e 1e 1c 1a 26 2c 2a 26 2c 28",
122
+ "33 15 18 25 2d 38 23 25 2c 2a",
123
+ "37 3c 1c 1c 1c 1a 25 2c 2c 29",
124
+ "37 3a 46 4c 6a 13 56 5a 56 5a",
125
+ "33 33 45 4a 63 13 51 55 59 53",
126
+ "33 43 46 49 63 17 1e 1e 5c 59",
127
+ "33 53 47 4a 61 13 13 35 3c 38",
128
+ "33 53 41 45 1c 19 43 46 4c 4a",
129
+ "63 55 5c 5a 56 5a 45 49 44 49",
130
+ "65 6c 68 55 59 55 2c 2c 2c 2c"
131
+ ]]
132
+ ]);
133
+
134
+ function normalizeSeed(seed) {
135
+ if (typeof seed !== "number" || !Number.isFinite(seed)) {
136
+ throw new TypeError("generator maze seed must be a finite number");
137
+ }
138
+ return seed | 0;
139
+ }
140
+
141
+ function cellOffset(x, y) {
142
+ return (y * SIZE + x) * CELL_STRIDE;
143
+ }
144
+
145
+ function hasUnvisitedNeighbor(grid, x, y) {
146
+ return (y > 0 && grid[cellOffset(x, y - 1)] === 0)
147
+ || (y < SIZE - 1 && grid[cellOffset(x, y + 1)] === 0)
148
+ || (x > 0 && grid[cellOffset(x - 1, y)] === 0)
149
+ || (x < SIZE - 1 && grid[cellOffset(x + 1, y)] === 0);
150
+ }
151
+
152
+ function nextScanCell(x, y) {
153
+ if (x > SIZE - 2) return {x: 0, y: y + 1 > SIZE - 1 ? 0 : y + 1};
154
+ return {x: x + 1, y};
155
+ }
156
+
157
+ function chooseFrontierCell(rng, grid) {
158
+ let x = rng.nextU32() % SIZE;
159
+ let y = rng.nextU32() % SIZE;
160
+ for (let attempts = 101; attempts > 0; attempts--) {
161
+ if (grid[cellOffset(x, y)] !== 0 && hasUnvisitedNeighbor(grid, x, y)) return {x, y};
162
+ ({x, y} = nextScanCell(x, y));
163
+ }
164
+ return null;
165
+ }
166
+
167
+ function carveMaze(rng) {
168
+ const grid = new Uint8Array(GRID_BYTES);
169
+ let x = 0;
170
+ let y = 0;
171
+ grid[cellOffset(0, 0)] = WALL_LEFT;
172
+
173
+ for (let guard = 0; guard < 1000; guard++) {
174
+ const firstDirection = rng.nextU32() & 3;
175
+ let moved = false;
176
+ for (let i = 0; i < 4; i++) {
177
+ const direction = (firstDirection + i) & 3;
178
+ const move = MOVES[direction];
179
+ const nextX = x + move.dx;
180
+ const nextY = y + move.dy;
181
+ if (nextX < 0 || nextX >= SIZE || nextY < 0 || nextY >= SIZE) continue;
182
+ const nextOffset = cellOffset(nextX, nextY);
183
+ if (grid[nextOffset] !== 0) continue;
184
+ const offset = cellOffset(x, y);
185
+ grid[offset] |= move.wall;
186
+ grid[nextOffset] |= move.opposite;
187
+ grid[nextOffset + 1] = move.stored;
188
+ x = nextX;
189
+ y = nextY;
190
+ moved = true;
191
+ break;
192
+ }
193
+
194
+ if (!moved || (x === SIZE - 1 && y === SIZE - 1)) {
195
+ const frontier = chooseFrontierCell(rng, grid);
196
+ if (!frontier) break;
197
+ x = frontier.x;
198
+ y = frontier.y;
199
+ }
200
+ }
201
+
202
+ return grid;
203
+ }
204
+
205
+ function findUnnumberedCell(rng, grid) {
206
+ let x = rng.nextU32() % SIZE;
207
+ let y = rng.nextU32() % SIZE;
208
+ for (let attempts = 100; attempts > 0; attempts -= 2) {
209
+ if (grid[cellOffset(x, y)] < 16) return {x, y};
210
+ ({x, y} = nextScanCell(x, y));
211
+ if (grid[cellOffset(x, y)] < 16) return {x, y};
212
+ ({x, y} = nextScanCell(x, y));
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function numberMaze(rng, grid) {
218
+ for (let pass = 0; pass < 100; pass++) {
219
+ const start = findUnnumberedCell(rng, grid);
220
+ if (!start) break;
221
+
222
+ const digit = ((pass % 6) + 1) << 4;
223
+ const queue = [start];
224
+ let labelled = 0;
225
+ while (queue.length > 0 && labelled < 10) {
226
+ const {x, y} = queue.shift();
227
+ const offset = cellOffset(x, y);
228
+ const cell = grid[offset];
229
+ if (cell >= 16) continue;
230
+ grid[offset] = cell | digit;
231
+ labelled++;
232
+
233
+ if ((cell & WALL_UP) !== 0 && y > 0 && grid[cellOffset(x, y - 1)] < 16) queue.push({x, y: y - 1});
234
+ if ((cell & WALL_DOWN) !== 0 && y < SIZE - 1 && grid[cellOffset(x, y + 1)] < 16) queue.push({x, y: y + 1});
235
+ if ((cell & WALL_LEFT) !== 0 && x > 0 && grid[cellOffset(x - 1, y)] < 16) queue.push({x: x - 1, y});
236
+ if ((cell & WALL_RIGHT) !== 0 && x < SIZE - 1 && grid[cellOffset(x + 1, y)] < 16) queue.push({x: x + 1, y});
237
+ }
238
+ }
239
+ }
240
+
241
+ function solutionFromGrid(grid) {
242
+ let x = SIZE - 1;
243
+ let y = SIZE - 1;
244
+ const digits = [];
245
+ for (let guard = 0; guard < 300; guard++) {
246
+ const offset = cellOffset(x, y);
247
+ digits.push(String(grid[offset] >> 4));
248
+ if (x === 0 && y === 0) break;
249
+ const move = BACKTRACK_MOVES[grid[offset + 1]];
250
+ if (!move) break;
251
+ x += move.dx;
252
+ y += move.dy;
253
+ if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) break;
254
+ }
255
+
256
+ let solution = "";
257
+ for (const digit of digits.reverse()) {
258
+ if (digit !== solution[solution.length - 1]) solution += digit;
259
+ }
260
+ return solution;
261
+ }
262
+
263
+ function generateGeneratorMazeGrid(seed) {
264
+ const rng = new GeneratorMazeRng(seed);
265
+ const grid = carveMaze(rng);
266
+ // The official client consumes one RNG word between carving and numbering.
267
+ rng.nextU32();
268
+ numberMaze(rng, grid);
269
+ return grid;
270
+ }
271
+
272
+ function solveGeneratorMazeSeedUncached(seed) {
273
+ const grid = generateGeneratorMazeGrid(seed);
274
+ return solutionFromGrid(grid);
275
+ }
276
+
277
+ function gridFromHexRows(rows) {
278
+ const grid = new Uint8Array(GRID_BYTES);
279
+ for (let y = 0; y < SIZE; y++) {
280
+ const values = rows[y].split(/\s+/);
281
+ for (let x = 0; x < SIZE; x++) {
282
+ grid[cellOffset(x, y)] = parseInt(values[x], 16);
283
+ }
284
+ }
285
+ return grid;
286
+ }
287
+
288
+ function cellWalls(cell) {
289
+ return {
290
+ up: (cell & WALL_UP) !== 0,
291
+ down: (cell & WALL_DOWN) !== 0,
292
+ left: (cell & WALL_LEFT) !== 0,
293
+ right: (cell & WALL_RIGHT) !== 0
294
+ };
295
+ }
296
+
297
+ function describeGeneratorMaze(seed, grid) {
298
+ const cells = [];
299
+ const rows = [];
300
+ for (let y = 0; y < SIZE; y++) {
301
+ const row = [];
302
+ rows.push(row);
303
+ for (let x = 0; x < SIZE; x++) {
304
+ const offset = cellOffset(x, y);
305
+ const value = grid[offset];
306
+ const cell = {
307
+ x,
308
+ y,
309
+ value,
310
+ hex: value.toString(16).padStart(2, "0"),
311
+ digit: value >> 4,
312
+ walls: cellWalls(value),
313
+ backtrackDirection: grid[offset + 1],
314
+ marker: grid[offset + 2] || 0
315
+ };
316
+ row.push(cell);
317
+ cells.push(cell);
318
+ }
319
+ }
320
+ return {
321
+ seed,
322
+ width: SIZE,
323
+ height: SIZE,
324
+ cells,
325
+ rows,
326
+ solution: solutionFromGrid(grid)
327
+ };
328
+ }
329
+
330
+ // Solving is a pure function of the seed but costs ~260us: it carves and numbers
331
+ // a full maze. Every shield generator was re-solving its seed on every entity
332
+ // summarisation, which made this the single most expensive step of a model
333
+ // rebuild. Seeds change rarely, so memoise them.
334
+ const SOLUTION_CACHE = new Map();
335
+ const SOLUTION_CACHE_LIMIT = 512;
336
+
337
+ export function solveGeneratorMazeSeed(seed) {
338
+ const normalized = normalizeSeed(seed);
339
+ const verified = VERIFIED_SOLUTIONS.get(normalized);
340
+ if (verified !== undefined) return verified;
341
+
342
+ if (SOLUTION_CACHE.has(normalized)) return SOLUTION_CACHE.get(normalized);
343
+
344
+ const solution = solveGeneratorMazeSeedUncached(normalized);
345
+ // Bounded, insertion-ordered: drop the oldest entry once full.
346
+ if (SOLUTION_CACHE.size >= SOLUTION_CACHE_LIMIT) {
347
+ SOLUTION_CACHE.delete(SOLUTION_CACHE.keys().next().value);
348
+ }
349
+ SOLUTION_CACHE.set(normalized, solution);
350
+ return solution;
351
+ }
352
+
353
+ export function generateGeneratorMaze(seed) {
354
+ const normalized = normalizeSeed(seed);
355
+ const grid = VERIFIED_MAZES.has(normalized)
356
+ ? gridFromHexRows(VERIFIED_MAZES.get(normalized))
357
+ : generateGeneratorMazeGrid(normalized);
358
+ const maze = describeGeneratorMaze(normalized, grid);
359
+ if (VERIFIED_SOLUTIONS.has(normalized)) maze.solution = VERIFIED_SOLUTIONS.get(normalized);
360
+ return maze;
361
+ }
362
+
363
+ export function maybeSolveGeneratorMazeSeed(seed) {
364
+ if (typeof seed !== "number" || !Number.isFinite(seed)) return null;
365
+ try {
366
+ return solveGeneratorMazeSeed(seed);
367
+ } catch {
368
+ return null;
369
+ }
370
+ }