@aethervtt/shard-terrain 0.1.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 (76) hide show
  1. package/dist/biomes.d.ts +1 -0
  2. package/dist/biomes.js +3 -0
  3. package/dist/chunk.d.ts +0 -44
  4. package/dist/chunk.js +2 -127
  5. package/dist/colliders.d.ts +16 -1
  6. package/dist/colliders.js +39 -24
  7. package/dist/components.d.ts +5 -0
  8. package/dist/components.js +7 -2
  9. package/dist/cube-sphere.d.ts +50 -0
  10. package/dist/cube-sphere.js +224 -0
  11. package/dist/grid-mesh.d.ts +49 -0
  12. package/dist/grid-mesh.js +208 -0
  13. package/dist/heightfield/bake.d.ts +120 -0
  14. package/dist/heightfield/bake.js +494 -0
  15. package/dist/heightfield/colliders.d.ts +39 -0
  16. package/dist/heightfield/colliders.js +152 -0
  17. package/dist/heightfield/component.d.ts +14 -0
  18. package/dist/heightfield/component.js +59 -0
  19. package/dist/heightfield/deflate.d.ts +11 -0
  20. package/dist/heightfield/deflate.js +484 -0
  21. package/dist/heightfield/heightmap.d.ts +27 -0
  22. package/dist/heightfield/heightmap.js +160 -0
  23. package/dist/heightfield/kernel.d.ts +277 -0
  24. package/dist/heightfield/kernel.js +1033 -0
  25. package/dist/heightfield/material.d.ts +13 -0
  26. package/dist/heightfield/material.js +394 -0
  27. package/dist/heightfield/methods.d.ts +104 -0
  28. package/dist/heightfield/methods.js +218 -0
  29. package/dist/heightfield/pack.d.ts +59 -0
  30. package/dist/heightfield/pack.js +127 -0
  31. package/dist/heightfield/pages.d.ts +91 -0
  32. package/dist/heightfield/pages.js +244 -0
  33. package/dist/heightfield/project.d.ts +40 -0
  34. package/dist/heightfield/project.js +55 -0
  35. package/dist/heightfield/queries.d.ts +48 -0
  36. package/dist/heightfield/queries.js +150 -0
  37. package/dist/heightfield/render.d.ts +108 -0
  38. package/dist/heightfield/render.js +727 -0
  39. package/dist/heightfield/root-grid.d.ts +38 -0
  40. package/dist/heightfield/root-grid.js +109 -0
  41. package/dist/heightfield/runtime.d.ts +105 -0
  42. package/dist/heightfield/runtime.js +312 -0
  43. package/dist/heightfield/source-asset.d.ts +39 -0
  44. package/dist/heightfield/source-asset.js +372 -0
  45. package/dist/heightfield/source.d.ts +151 -0
  46. package/dist/heightfield/source.js +402 -0
  47. package/dist/heightfield/stack.d.ts +29 -0
  48. package/dist/heightfield/stack.js +158 -0
  49. package/dist/heightfield/system.d.ts +22 -0
  50. package/dist/heightfield/system.js +165 -0
  51. package/dist/heightfield/testing.d.ts +317 -0
  52. package/dist/heightfield/testing.js +441 -0
  53. package/dist/heightfield/worker.js +47 -0
  54. package/dist/heights.d.ts +8 -3
  55. package/dist/heights.js +8 -4
  56. package/dist/index.d.ts +24 -7
  57. package/dist/index.js +158 -8
  58. package/dist/lod.d.ts +27 -0
  59. package/dist/lod.js +23 -1
  60. package/dist/material.js +2 -1
  61. package/dist/methods.js +64 -16
  62. package/dist/overlays.d.ts +2 -0
  63. package/dist/overlays.js +48 -6
  64. package/dist/planet.d.ts +2 -7
  65. package/dist/planet.js +8 -18
  66. package/dist/plugin.d.ts +10 -3
  67. package/dist/plugin.js +48 -8
  68. package/dist/quadtree.d.ts +61 -39
  69. package/dist/quadtree.js +44 -208
  70. package/dist/render.d.ts +7 -1
  71. package/dist/render.js +8 -6
  72. package/dist/test-planet.d.ts +39 -0
  73. package/dist/test-planet.js +40 -4
  74. package/dist/testing.d.ts +2 -0
  75. package/dist/testing.js +42 -2
  76. package/package.json +16 -16
@@ -0,0 +1,218 @@
1
+ import { findComponent, ShardError } from "@aethervtt/shard-core";
2
+ import { TerrainWorld } from "../heights.js";
3
+ import { tilesOf } from "./colliders.js";
4
+ import { finestPage, heightfieldSample, loadTerrainRegion, pageHeight } from "./queries.js";
5
+ const round = (v, digits = 3) => {
6
+ const k = 10 ** digits;
7
+ return Math.round(v * k) / k;
8
+ };
9
+ function scenePath(world, entity) {
10
+ const member = findComponent("scene/SceneMember");
11
+ if (!member || !world.isAlive(entity)) return null;
12
+ const v = world.tryGet(entity, member);
13
+ return v?.path || null;
14
+ }
15
+ function resolveHeightfield(world, ref) {
16
+ const all = world.tryResource(TerrainWorld)?.heightfields;
17
+ if (!all || all.size === 0) {
18
+ throw new ShardError("terrain/no-terrain", "There is no terrain/Terrain in the world", {
19
+ hint: "Add terrain/Terrain (with a Grid and a *.terrain.json source) to an entity."
20
+ });
21
+ }
22
+ if (ref === void 0 || ref === null || ref === "") {
23
+ if (all.size > 1) {
24
+ throw new ShardError("terrain/which-terrain", `There are ${all.size} terrains; name one`, {
25
+ hint: "Pass terrain: an entity id or a scene path (terrain.describe lists them)."
26
+ });
27
+ }
28
+ return [...all.values()][0];
29
+ }
30
+ for (const rt of all.values()) {
31
+ if (rt.entity === ref) return rt;
32
+ const path = scenePath(world, rt.entity);
33
+ if (typeof ref === "string" && path && (path === ref || path.endsWith(`/${ref}`))) return rt;
34
+ }
35
+ throw new ShardError("terrain/not-a-terrain", `No terrain matches ${JSON.stringify(ref)}`, {
36
+ hint: "terrain.describe lists every terrain with its entity and scene path."
37
+ });
38
+ }
39
+ function usable(rt) {
40
+ if (rt.problem) throw rt.problem;
41
+ if (!rt.ready) {
42
+ throw new ShardError(
43
+ "terrain/not-ready",
44
+ `The terrain is waiting for ${rt.waiting ?? "its source"}`,
45
+ {
46
+ hint: "Its source and assets load asynchronously; step a frame and ask again."
47
+ }
48
+ );
49
+ }
50
+ }
51
+ function describeHeightfield(world, rt) {
52
+ const layout = rt.layout;
53
+ const r = rt.parts.get("render");
54
+ const byDepth = {};
55
+ let partial = 0;
56
+ for (let i = 0; i < rt.selection.renderedCount; i++) {
57
+ const n = rt.selection.rendered[i];
58
+ const d = rt.tree.depth[n];
59
+ byDepth[d] = (byDepth[d] ?? 0) + 1;
60
+ if (rt.tree.mask[n] !== 15) partial++;
61
+ }
62
+ const blocks = layout ? layout.blocksX * layout.blocksZ : 0;
63
+ const records = rt.manifest?.blocks ?? {};
64
+ const current = rt.bake === "current" ? Object.keys(records).length : 0;
65
+ const tiles = tilesOf(rt);
66
+ let resident = 0;
67
+ if (rt.pages && layout) {
68
+ for (const page of rt.pages.pages()) if (page.depth <= rt.residentDepth) resident++;
69
+ }
70
+ return {
71
+ entity: rt.entity,
72
+ path: scenePath(world, rt.entity),
73
+ source: rt.settings?.source?.path ?? rt.settings?.source?.guid ?? null,
74
+ ready: rt.ready,
75
+ streaming: rt.streaming,
76
+ waitingFor: rt.waiting,
77
+ problem: rt.problem ? rt.problem.toJSON() : null,
78
+ size: layout ? [layout.sizeX, layout.sizeZ] : null,
79
+ spacing: layout?.spacing ?? null,
80
+ heightRange: rt.asset ? rt.asset.source.heightRange : null,
81
+ roots: layout ? { x: layout.rootsX, z: layout.rootsZ, size: layout.rootSize } : null,
82
+ depths: layout ? layout.depth + 1 : 0,
83
+ version: rt.version,
84
+ bake: {
85
+ state: rt.bake,
86
+ blocks,
87
+ current,
88
+ stale: rt.bake === "current" ? blocks - current : blocks,
89
+ baking: rt.bake === "baking" ? blocks : 0,
90
+ lastBake: rt.lastBake ? {
91
+ rebaked: rt.lastBake.rebaked,
92
+ pages: rt.lastBake.pages,
93
+ ms: Math.round(rt.lastBake.ms),
94
+ msPerBlock: Math.round(rt.lastBake.msPerBlock),
95
+ outOfRange: rt.lastBake.outOfRange
96
+ } : null
97
+ },
98
+ detail: {
99
+ errorPixels: rt.settings?.errorPixels ?? null,
100
+ vertexPixels: rt.settings?.vertexPixels ?? null,
101
+ lodBias: round(rt.lodBias),
102
+ errors: Array.from(rt.errors, (e) => round(e))
103
+ },
104
+ selected: {
105
+ chunks: rt.selection.renderedCount,
106
+ byDepth,
107
+ partial,
108
+ requested: rt.selection.requestedCount,
109
+ waiting: rt.selection.waiting
110
+ },
111
+ pages: {
112
+ residentDepth: rt.residentDepth,
113
+ resident,
114
+ cached: rt.pages?.cached ?? 0,
115
+ readsInFlight: rt.pages?.pendingReads ?? 0,
116
+ reads: rt.pages?.stats.reads ?? 0,
117
+ bytesRead: rt.pages?.stats.bytesRead ?? 0,
118
+ bakedInline: rt.pages?.stats.bakedInline ?? 0
119
+ },
120
+ pool: r ? {
121
+ slots: r.capacity,
122
+ used: r.used,
123
+ uploaded: r.stats.uploaded,
124
+ uploadedLastFrame: r.stats.uploadedLastFrame,
125
+ waitingToUpload: r.arrived.length,
126
+ evicted: r.stats.evicted
127
+ } : null,
128
+ colliders: {
129
+ tiles: tiles.tiles.size,
130
+ active: [...tiles.tiles.values()].filter((t) => t.entity >= 0).length,
131
+ pending: tiles.due.size,
132
+ fromPages: tiles.fromPages,
133
+ bakedInline: tiles.bakedInline,
134
+ anchors: rt.anchors
135
+ }
136
+ };
137
+ }
138
+ async function sampleHeightfield(world, rt, points) {
139
+ usable(rt);
140
+ for (const [x, z] of points) {
141
+ await loadTerrainRegion(world, rt.entity, [x - 1, z - 1, x + 1, z + 1]);
142
+ }
143
+ return points.map(([x, z]) => {
144
+ const s = heightfieldSample(rt, x, z);
145
+ return {
146
+ x,
147
+ z,
148
+ height: round(s.height),
149
+ normal: s.normal.map((v) => round(v, 4)),
150
+ slope: round(s.slope, 2),
151
+ layers: s.layers,
152
+ share: round(s.share, 3),
153
+ depth: s.depth
154
+ };
155
+ });
156
+ }
157
+ function heightfieldMap(rt, width, mode) {
158
+ usable(rt);
159
+ const layout = rt.layout;
160
+ const height = Math.max(1, Math.round(width * layout.sizeZ / layout.sizeX));
161
+ const data = new Uint8Array(width * height * 4);
162
+ const lo = rt.lo;
163
+ const hi = rt.hi;
164
+ const tints = rt.asset.source.layers.map((l) => l.tint);
165
+ const dirtied = new Set(rt.dirtied);
166
+ const blockSize = layout.block * layout.leafSize;
167
+ for (let py = 0; py < height; py++) {
168
+ const z = (py + 0.5) / height * layout.sizeZ;
169
+ for (let px = 0; px < width; px++) {
170
+ const x = (px + 0.5) / width * layout.sizeX;
171
+ const o = (py * width + px) * 4;
172
+ let r = 0;
173
+ let g = 0;
174
+ let b = 0;
175
+ if (mode === "bake") {
176
+ const id = `${Math.floor(x / blockSize)},${Math.floor(z / blockSize)}`;
177
+ const current = rt.manifest?.blocks[id] !== void 0 && rt.bake === "current";
178
+ [r, g, b] = dirtied.has(id) ? [60, 200, 230] : current ? [70, 170, 80] : [220, 160, 40];
179
+ const fx = x / blockSize % 1;
180
+ const fz = z / blockSize % 1;
181
+ if (fx < 0.02 || fz < 0.02) [r, g, b] = [r * 0.5, g * 0.5, b * 0.5];
182
+ } else {
183
+ const page = finestPage(rt, x, z);
184
+ if (page) {
185
+ if (mode === "slope" || mode === "layers") {
186
+ const s = heightfieldSample(rt, x, z);
187
+ if (mode === "slope") r = g = b = Math.min(255, s.slope / 60 * 255);
188
+ else {
189
+ const names = rt.asset.source.layers.map((l) => l.name);
190
+ const ta = tints[names.indexOf(s.layers[0])] ?? [1, 0, 1];
191
+ const tb = tints[names.indexOf(s.layers[1])] ?? ta;
192
+ const t = s.share;
193
+ r = (ta[0] * (1 - t) + tb[0] * t) ** (1 / 2.2) * 255;
194
+ g = (ta[1] * (1 - t) + tb[1] * t) ** (1 / 2.2) * 255;
195
+ b = (ta[2] * (1 - t) + tb[2] * t) ** (1 / 2.2) * 255;
196
+ }
197
+ } else {
198
+ const k = Math.min(1, Math.max(0, (pageHeight(rt, page, x, z) - lo) / (hi - lo)));
199
+ r = 30 + 225 * k;
200
+ g = 50 + 205 * k;
201
+ b = 30 + 225 * k;
202
+ }
203
+ }
204
+ }
205
+ data[o] = r;
206
+ data[o + 1] = g;
207
+ data[o + 2] = b;
208
+ data[o + 3] = 255;
209
+ }
210
+ }
211
+ return { width, height, data };
212
+ }
213
+ export {
214
+ describeHeightfield,
215
+ heightfieldMap,
216
+ resolveHeightfield,
217
+ sampleHeightfield
218
+ };
@@ -0,0 +1,59 @@
1
+ import type { PlatformFileSystem } from '@aethervtt/shard-platform';
2
+ /** Nodes per pack side. */
3
+ export declare const PACK = 16;
4
+ export declare const PACK_PAGES: number;
5
+ /** Bytes before the first page: header and index. */
6
+ export declare const PACK_INDEX_BYTES: number;
7
+ /** A page's index entry. Absent pages (past the terrain's edge) have length 0. */
8
+ export interface PackEntry {
9
+ offset: number;
10
+ length: number;
11
+ /** Quantized height range over the node's subtree. */
12
+ min: number;
13
+ max: number;
14
+ /** Geometric error (m). */
15
+ error: number;
16
+ }
17
+ export interface PackIndex {
18
+ depth: number;
19
+ gx: number;
20
+ gz: number;
21
+ entries: PackEntry[];
22
+ }
23
+ /** Where a node's page lives: its pack file and slot in it. */
24
+ export declare function packOf(depth: number, x: number, z: number): {
25
+ path: string;
26
+ slot: number;
27
+ };
28
+ export declare function readIndex(path: string, bytes: Uint8Array): PackIndex;
29
+ /** A pack file from its pages (null where absent), in slot order. */
30
+ export declare function writePack(depth: number, gx: number, gz: number, pages: readonly ({
31
+ data: Uint8Array;
32
+ min: number;
33
+ max: number;
34
+ error: number;
35
+ } | null)[]): Uint8Array;
36
+ /**
37
+ * Where a terrain's packs and manifest live: the project's `.shard/cache/terrain/<id>/` through
38
+ * the platform's file service, or memory (tests, read-only hosts baking on first use).
39
+ */
40
+ export interface PackStore {
41
+ readonly writable: boolean;
42
+ /** A whole file, or undefined if it doesn't exist. */
43
+ read(path: string): Promise<Uint8Array | undefined>;
44
+ /** `length` bytes at `offset` (one ranged read where the host has them). */
45
+ readRange(path: string, offset: number, length: number): Promise<Uint8Array>;
46
+ write(path: string, bytes: Uint8Array): Promise<void>;
47
+ /** Bytes of every file, for stats. */
48
+ size(): Promise<number>;
49
+ }
50
+ /** Packs in memory. */
51
+ export declare function memoryPackStore(): PackStore & {
52
+ files: Map<string, Uint8Array>;
53
+ };
54
+ /**
55
+ * Packs under `dir` on a platform file service: ranged reads through `readRange` (a whole read
56
+ * and a slice on hosts without one), writes to a temporary file moved into place, so a running
57
+ * game reading the old pack never sees half a new one.
58
+ */
59
+ export declare function fsPackStore(fs: PlatformFileSystem, dir: string): PackStore;
@@ -0,0 +1,127 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ const PACK = 16;
3
+ const PACK_PAGES = PACK * PACK;
4
+ const MAGIC = 827348051;
5
+ const HEADER = 16;
6
+ const ENTRY = 20;
7
+ const PACK_INDEX_BYTES = HEADER + ENTRY * PACK_PAGES;
8
+ function packOf(depth, x, z) {
9
+ const gx = Math.floor(x / PACK);
10
+ const gz = Math.floor(z / PACK);
11
+ return { path: `d${depth}/${gx}_${gz}.pack`, slot: (z - gz * PACK) * PACK + (x - gx * PACK) };
12
+ }
13
+ function corrupt(path, why) {
14
+ return new ShardError("terrain/corrupt-pack", `${path}: ${why}`, {
15
+ path,
16
+ hint: "The pack is damaged or from another bake: `shard terrain bake --force` rewrites it."
17
+ });
18
+ }
19
+ function readIndex(path, bytes) {
20
+ if (bytes.length < PACK_INDEX_BYTES) throw corrupt(path, "too short for an index");
21
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
22
+ if (v.getUint32(0, true) !== MAGIC) throw corrupt(path, "not a terrain pack");
23
+ const entries = [];
24
+ for (let i = 0; i < PACK_PAGES; i++) {
25
+ const o = HEADER + i * ENTRY;
26
+ entries.push({
27
+ offset: v.getUint32(o, true),
28
+ length: v.getUint32(o + 4, true),
29
+ min: v.getUint16(o + 8, true),
30
+ max: v.getUint16(o + 10, true),
31
+ error: v.getFloat32(o + 12, true)
32
+ });
33
+ }
34
+ return { depth: v.getUint8(6), gx: v.getUint16(8, true), gz: v.getUint16(10, true), entries };
35
+ }
36
+ function writePack(depth, gx, gz, pages) {
37
+ let size = PACK_INDEX_BYTES;
38
+ for (const p of pages) if (p) size += p.data.length;
39
+ const out = new Uint8Array(size);
40
+ const v = new DataView(out.buffer);
41
+ v.setUint32(0, MAGIC, true);
42
+ v.setUint16(4, 1, true);
43
+ v.setUint8(6, depth);
44
+ v.setUint16(8, gx, true);
45
+ v.setUint16(10, gz, true);
46
+ v.setUint16(12, PACK_PAGES, true);
47
+ let offset = PACK_INDEX_BYTES;
48
+ for (let i = 0; i < PACK_PAGES; i++) {
49
+ const p = pages[i];
50
+ if (!p) continue;
51
+ const o = HEADER + i * ENTRY;
52
+ v.setUint32(o, offset, true);
53
+ v.setUint32(o + 4, p.data.length, true);
54
+ v.setUint16(o + 8, p.min, true);
55
+ v.setUint16(o + 10, p.max, true);
56
+ v.setFloat32(o + 12, p.error, true);
57
+ out.set(p.data, offset);
58
+ offset += p.data.length;
59
+ }
60
+ return out;
61
+ }
62
+ function memoryPackStore() {
63
+ const files = /* @__PURE__ */ new Map();
64
+ return {
65
+ files,
66
+ writable: true,
67
+ read: async (path) => files.get(path),
68
+ readRange: async (path, offset, length) => {
69
+ const f = files.get(path);
70
+ if (!f) {
71
+ throw new ShardError("terrain/missing-pack", `No pack ${path}`, {
72
+ path,
73
+ hint: "The terrain isn\u2019t baked (or was baked for another grid): `shard terrain bake`."
74
+ });
75
+ }
76
+ return f.subarray(offset, offset + length);
77
+ },
78
+ write: async (path, bytes) => void files.set(path, bytes),
79
+ size: async () => {
80
+ let n = 0;
81
+ for (const f of files.values()) n += f.length;
82
+ return n;
83
+ }
84
+ };
85
+ }
86
+ function fsPackStore(fs, dir) {
87
+ const full = (p) => `${dir}/${p}`;
88
+ return {
89
+ writable: fs.writable,
90
+ read: async (path) => await fs.exists(full(path)) ? fs.readBytes(full(path)) : void 0,
91
+ readRange: async (path, offset, length) => {
92
+ if (fs.readRange) return fs.readRange(full(path), offset, length);
93
+ const all = await fs.readBytes(full(path));
94
+ return all.subarray(offset, offset + length);
95
+ },
96
+ write: async (path, bytes) => {
97
+ if (fs.move) {
98
+ const temp = `${full(path)}.tmp`;
99
+ await fs.writeBytes(temp, bytes);
100
+ await fs.move(temp, full(path));
101
+ } else await fs.writeBytes(full(path), bytes);
102
+ },
103
+ size: async () => {
104
+ if (!fs.list || !fs.stat) return 0;
105
+ let n = 0;
106
+ const walk = async (d) => {
107
+ for (const e of await fs.list(d)) {
108
+ const p = `${d}/${e.name}`;
109
+ if (e.kind === "dir") await walk(p);
110
+ else n += (await fs.stat(p))?.size ?? 0;
111
+ }
112
+ };
113
+ await walk(dir);
114
+ return n;
115
+ }
116
+ };
117
+ }
118
+ export {
119
+ PACK,
120
+ PACK_INDEX_BYTES,
121
+ PACK_PAGES,
122
+ fsPackStore,
123
+ memoryPackStore,
124
+ packOf,
125
+ readIndex,
126
+ writePack
127
+ };
@@ -0,0 +1,91 @@
1
+ import type { Workers } from '@aethervtt/shard-platform';
2
+ import { type NoiseAccess, type Stack } from './kernel.js';
3
+ import { type PackEntry, type PackIndex, type PackStore } from './pack.js';
4
+ import type { TerrainLayout } from './source.js';
5
+ /** A node's number: depth, then z, then x (exact in an f64 up to 2^20 nodes a side). */
6
+ export declare function nodeKey(depth: number, x: number, z: number): number;
7
+ export declare function keyDepth(key: number): number;
8
+ export declare function keyX(key: number): number;
9
+ export declare function keyZ(key: number): number;
10
+ /** A page as streaming and queries use it. */
11
+ export interface LoadedPage {
12
+ key: number;
13
+ depth: number;
14
+ x: number;
15
+ z: number;
16
+ leaf: boolean;
17
+ /** u16 heights: LEAF_SIDE² with a border for leaves, SIDE² for parents. */
18
+ heights: Uint16Array;
19
+ /** Normals, x then z planes (SIDE² each). */
20
+ normals: Uint8Array;
21
+ control: Uint8Array;
22
+ /** RGBA8 texels for the GPU pool (LEAF_SIDE²), until uploaded. */
23
+ texels: Uint8Array | undefined;
24
+ /** Quantized height range of the node's subtree, and its geometric error. */
25
+ min: number;
26
+ max: number;
27
+ error: number;
28
+ }
29
+ /**
30
+ * A terrain's pages at runtime (spec 0071): pack indexes, reads through the store's ranged reads,
31
+ * inflate on the worker pool, and a CPU cache of decoded pages (least recently used out, coarse
32
+ * levels pinned). Colliders, height queries and the GPU pool all read through it.
33
+ */
34
+ export declare class PageStore {
35
+ store: PackStore;
36
+ readonly layout: TerrainLayout;
37
+ readonly lo: number;
38
+ readonly hi: number;
39
+ workers: Workers | undefined;
40
+ /** Extra milliseconds before every read resolves (tests: a slow disk). */
41
+ delayMs: number;
42
+ /** Pages kept decoded on the CPU, besides pinned ones. */
43
+ capacity: number;
44
+ private readonly indexes;
45
+ private readonly pendingIndexes;
46
+ private readonly inflight;
47
+ /** Decoded pages by key, in use order (Map insertion order: oldest first). */
48
+ private readonly cache;
49
+ private readonly pinned;
50
+ stats: {
51
+ reads: number;
52
+ bytesRead: number;
53
+ inflated: number;
54
+ bakedInline: number;
55
+ };
56
+ constructor(store: PackStore, layout: TerrainLayout, lo: number, hi: number, workers?: Workers);
57
+ /** Nodes per side at a depth. */
58
+ nodesAt(depth: number): [number, number];
59
+ /** A node's index entry, if its pack's index is loaded. */
60
+ entry(depth: number, x: number, z: number): PackEntry | undefined;
61
+ /** Loads the index of the pack holding a node. */
62
+ index(depth: number, x: number, z: number): Promise<PackIndex | null>;
63
+ /** Whether a node's pack index is loaded (or known missing). */
64
+ hasIndex(depth: number, x: number, z: number): boolean;
65
+ /** A decoded page in the CPU cache (marked used), or undefined. */
66
+ get(key: number): LoadedPage | undefined;
67
+ /** Whether a read for this page is under way. */
68
+ loading(key: number): boolean;
69
+ /** Keeps a page decoded for the store's life (coarse levels). */
70
+ pin(key: number): void;
71
+ /**
72
+ * Reads, inflates and decodes a page (once, however many ask), into the CPU cache. Undefined
73
+ * past the terrain's edge or when the pack doesn't have it.
74
+ */
75
+ load(depth: number, x: number, z: number): Promise<LoadedPage | undefined>;
76
+ private read;
77
+ /** Adds a page to the CPU cache, dropping the least recently used unpinned ones over capacity. */
78
+ put(page: LoadedPage): void;
79
+ /**
80
+ * A leaf page now: from the cache, else baked on this thread from the stack (exactly the pack's
81
+ * bytes, spec 0071). Colliders use it on the frame a tile is due, so when a read lands never
82
+ * changes the world.
83
+ */
84
+ leafNow(noise: NoiseAccess, stack: Stack, x: number, z: number): LoadedPage;
85
+ /** Every decoded page (pinned and cached). */
86
+ pages(): IterableIterator<LoadedPage>;
87
+ get cached(): number;
88
+ get pendingReads(): number;
89
+ /** Forgets every index and page (after a rebake). Reads under way finish into the new cache. */
90
+ clear(): void;
91
+ }