@aethervtt/shard-terrain 0.1.0 → 0.2.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/dist/biomes.d.ts +1 -0
- package/dist/biomes.js +3 -0
- package/dist/chunk.d.ts +0 -44
- package/dist/chunk.js +2 -127
- package/dist/colliders.d.ts +16 -1
- package/dist/colliders.js +39 -24
- package/dist/components.d.ts +5 -0
- package/dist/components.js +7 -2
- package/dist/cube-sphere.d.ts +50 -0
- package/dist/cube-sphere.js +224 -0
- package/dist/grid-mesh.d.ts +49 -0
- package/dist/grid-mesh.js +208 -0
- package/dist/heightfield/bake.d.ts +120 -0
- package/dist/heightfield/bake.js +494 -0
- package/dist/heightfield/colliders.d.ts +39 -0
- package/dist/heightfield/colliders.js +152 -0
- package/dist/heightfield/component.d.ts +14 -0
- package/dist/heightfield/component.js +59 -0
- package/dist/heightfield/deflate.d.ts +11 -0
- package/dist/heightfield/deflate.js +484 -0
- package/dist/heightfield/heightmap.d.ts +27 -0
- package/dist/heightfield/heightmap.js +160 -0
- package/dist/heightfield/kernel.d.ts +277 -0
- package/dist/heightfield/kernel.js +1033 -0
- package/dist/heightfield/material.d.ts +13 -0
- package/dist/heightfield/material.js +394 -0
- package/dist/heightfield/methods.d.ts +104 -0
- package/dist/heightfield/methods.js +218 -0
- package/dist/heightfield/pack.d.ts +59 -0
- package/dist/heightfield/pack.js +127 -0
- package/dist/heightfield/pages.d.ts +91 -0
- package/dist/heightfield/pages.js +244 -0
- package/dist/heightfield/project.d.ts +40 -0
- package/dist/heightfield/project.js +55 -0
- package/dist/heightfield/queries.d.ts +48 -0
- package/dist/heightfield/queries.js +150 -0
- package/dist/heightfield/render.d.ts +108 -0
- package/dist/heightfield/render.js +727 -0
- package/dist/heightfield/root-grid.d.ts +38 -0
- package/dist/heightfield/root-grid.js +109 -0
- package/dist/heightfield/runtime.d.ts +105 -0
- package/dist/heightfield/runtime.js +312 -0
- package/dist/heightfield/source-asset.d.ts +39 -0
- package/dist/heightfield/source-asset.js +372 -0
- package/dist/heightfield/source.d.ts +151 -0
- package/dist/heightfield/source.js +402 -0
- package/dist/heightfield/stack.d.ts +29 -0
- package/dist/heightfield/stack.js +158 -0
- package/dist/heightfield/system.d.ts +22 -0
- package/dist/heightfield/system.js +165 -0
- package/dist/heightfield/testing.d.ts +317 -0
- package/dist/heightfield/testing.js +441 -0
- package/dist/heightfield/worker.js +47 -0
- package/dist/heights.d.ts +8 -3
- package/dist/heights.js +8 -4
- package/dist/index.d.ts +24 -7
- package/dist/index.js +158 -8
- package/dist/lod.d.ts +27 -0
- package/dist/lod.js +23 -1
- package/dist/material.js +2 -1
- package/dist/methods.js +64 -16
- package/dist/overlays.d.ts +2 -0
- package/dist/overlays.js +48 -6
- package/dist/planet.d.ts +2 -7
- package/dist/planet.js +8 -18
- package/dist/plugin.d.ts +10 -3
- package/dist/plugin.js +48 -8
- package/dist/quadtree.d.ts +61 -39
- package/dist/quadtree.js +44 -208
- package/dist/render.d.ts +7 -1
- package/dist/render.js +8 -6
- package/dist/test-planet.d.ts +39 -0
- package/dist/test-planet.js +40 -4
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +42 -2
- package/package.json +16 -16
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { defineComponent, t } from "@aethervtt/shard-core";
|
|
2
|
+
import { Visibility } from "@aethervtt/shard-render";
|
|
3
|
+
import { Grid, Transform } from "@aethervtt/shard-transform";
|
|
4
|
+
const Terrain = defineComponent(
|
|
5
|
+
"terrain/Terrain",
|
|
6
|
+
{
|
|
7
|
+
source: t.handle("terrain/TerrainSource", {
|
|
8
|
+
description: "The layer stack (*.terrain.json): noise, heightmap images, splines that flatten, raise or carve, and paint layers. Baked into page packs in .shard/cache/terrain by `shard import` (or on first use)."
|
|
9
|
+
}),
|
|
10
|
+
errorPixels: t.f32({
|
|
11
|
+
default: 2,
|
|
12
|
+
min: 0.1,
|
|
13
|
+
description: "A node splits when its geometric error covers more pixels than this."
|
|
14
|
+
}),
|
|
15
|
+
vertexPixels: t.f32({
|
|
16
|
+
default: 4,
|
|
17
|
+
min: 0,
|
|
18
|
+
description: "Finest on-screen vertex spacing: however rough the ground, nodes stop splitting once their vertices are this many pixels apart (normals keep the finer relief in the shading). 0: no limit."
|
|
19
|
+
}),
|
|
20
|
+
colliderRadius: t.f32({
|
|
21
|
+
default: 96,
|
|
22
|
+
min: 0,
|
|
23
|
+
unit: "m",
|
|
24
|
+
description: "Heightfield collider tiles (and leaf-detail rendering) within this distance of every TerrainAnchor, character, dynamic body and NavAgent."
|
|
25
|
+
}),
|
|
26
|
+
skirts: t.bool({
|
|
27
|
+
default: true,
|
|
28
|
+
description: "Walls under chunk edges that hide cracks while neighbors change level."
|
|
29
|
+
}),
|
|
30
|
+
residentDepth: t.i8({
|
|
31
|
+
default: -1,
|
|
32
|
+
min: -1,
|
|
33
|
+
description: "Depths up to this stay loaded for the terrain\u2019s life, so streaming that falls behind shows coarse ground, never a hole. \u22121: the deepest level whose pages fit in 8 MB."
|
|
34
|
+
}),
|
|
35
|
+
scatter: t.handle("scatter/ScatterSet", {
|
|
36
|
+
description: "Props and foliage on the terrain (*.scatter.json; needs the scatter plugin), placed per block from its heights and paint."
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
description: "A heightfield terrain (spec 0071): a bounded landscape from 256 m to 64 km a side, streamed from its baked pages through a quadtree with no cracks or popping, with Rapier heightfield colliders near bodies. Needs a Grid (its chunks are grid children); its local origin is the terrain\u2019s corner, x and z along its sides.",
|
|
41
|
+
requires: [Grid, Transform, Visibility]
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
const TerrainChunk = defineComponent(
|
|
45
|
+
"terrain/TerrainChunk",
|
|
46
|
+
{
|
|
47
|
+
terrain: t.entity({ readonly: true }),
|
|
48
|
+
key: t.string({ readonly: true, description: "depth/x/z" }),
|
|
49
|
+
kind: t.enum(["render", "collider"], { readonly: true })
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
description: "A heightfield chunk or collider tile the terrain spawned. Rebuilt from it; never saved.",
|
|
53
|
+
serialize: false
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
export {
|
|
57
|
+
Terrain,
|
|
58
|
+
TerrainChunk
|
|
59
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Types for deflate.js (plain JavaScript so worker threads can import it without a loader).
|
|
2
|
+
|
|
3
|
+
/** Compresses `input` to a raw DEFLATE stream (one final block). Same bytes on every host. */
|
|
4
|
+
export declare function deflate(input: Uint8Array): Uint8Array
|
|
5
|
+
/**
|
|
6
|
+
* Decompresses a raw DEFLATE stream into exactly `size` bytes. Throws an error with code
|
|
7
|
+
* `terrain/corrupt-pack` when the stream is damaged or doesn't hold `size` bytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function inflate(input: Uint8Array, size: number): Uint8Array
|
|
10
|
+
/** Huffman code lengths for `freqs`, at most `limit` bits, the same on every host. */
|
|
11
|
+
export declare function codeLengths(freqs: ArrayLike<number>, limit: number): Uint8Array
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// Raw DEFLATE (RFC 1951) for terrain packs (spec 0071). Plain JavaScript with no imports, so pool
|
|
2
|
+
// workers load it directly (no bundler, no TypeScript loader); types are in deflate.d.ts. Written
|
|
3
|
+
// here so a bake writes the same pack bytes on every host: Node's zlib and Chrome's
|
|
4
|
+
// CompressionStream are free to emit different streams for the same input, and pages are compared
|
|
5
|
+
// byte for byte. One block per call: LZ77 over a 32 KB window with hash chains of fixed length and
|
|
6
|
+
// one step of lazy matching, then dynamic Huffman codes limited to 15 bits. Every choice is fixed,
|
|
7
|
+
// so the output depends on the input alone.
|
|
8
|
+
|
|
9
|
+
const WINDOW = 32768
|
|
10
|
+
const MIN_MATCH = 3
|
|
11
|
+
const MAX_MATCH = 258
|
|
12
|
+
const HASH_BITS = 15
|
|
13
|
+
const HASH_SIZE = 1 << HASH_BITS
|
|
14
|
+
/** Candidates tried per position: deterministic, and enough for terrain pages. */
|
|
15
|
+
const CHAIN = 48
|
|
16
|
+
/** A match this long is taken without looking for a better one at the next byte. */
|
|
17
|
+
const GOOD = 32
|
|
18
|
+
|
|
19
|
+
const LENGTH_BASE = [
|
|
20
|
+
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131,
|
|
21
|
+
163, 195, 227, 258,
|
|
22
|
+
]
|
|
23
|
+
const LENGTH_EXTRA = [
|
|
24
|
+
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
|
|
25
|
+
]
|
|
26
|
+
const DIST_BASE = [
|
|
27
|
+
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049,
|
|
28
|
+
3073, 4097, 6145, 8193, 12289, 16385, 24577,
|
|
29
|
+
]
|
|
30
|
+
const DIST_EXTRA = [
|
|
31
|
+
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13,
|
|
32
|
+
]
|
|
33
|
+
/** Order code length code lengths are sent in. */
|
|
34
|
+
const CL_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]
|
|
35
|
+
|
|
36
|
+
/** Length code (257–285) for a match length. */
|
|
37
|
+
const lengthCode = new Uint16Array(MAX_MATCH + 1)
|
|
38
|
+
for (let c = 0; c < LENGTH_BASE.length; c++) {
|
|
39
|
+
const end = c + 1 < LENGTH_BASE.length ? LENGTH_BASE[c + 1] : MAX_MATCH + 1
|
|
40
|
+
for (let l = LENGTH_BASE[c]; l < end; l++) lengthCode[l] = c
|
|
41
|
+
}
|
|
42
|
+
lengthCode[MAX_MATCH] = 28
|
|
43
|
+
|
|
44
|
+
/** Distance code (0–29) per distance (1–32768). */
|
|
45
|
+
const distCodes = new Uint8Array(WINDOW + 1)
|
|
46
|
+
for (let c = 0; c < DIST_BASE.length; c++) {
|
|
47
|
+
const end = c + 1 < DIST_BASE.length ? DIST_BASE[c + 1] : WINDOW + 1
|
|
48
|
+
for (let d = DIST_BASE[c]; d < end; d++) distCodes[d] = c
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function distCode(d) {
|
|
52
|
+
return distCodes[d]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Huffman codes go out most significant bit first: reversed into the LSB-first stream. */
|
|
56
|
+
function reverse(code, length) {
|
|
57
|
+
let rev = 0
|
|
58
|
+
for (let i = 0; i < length; i++) rev |= ((code >>> i) & 1) << (length - 1 - i)
|
|
59
|
+
return rev
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class BitWriter {
|
|
63
|
+
buf = new Uint8Array(1024)
|
|
64
|
+
len = 0
|
|
65
|
+
acc = 0
|
|
66
|
+
bits = 0
|
|
67
|
+
|
|
68
|
+
write(value, count) {
|
|
69
|
+
this.acc |= value << this.bits
|
|
70
|
+
this.bits += count
|
|
71
|
+
while (this.bits >= 8) {
|
|
72
|
+
this.byte(this.acc & 0xff)
|
|
73
|
+
this.acc >>>= 8
|
|
74
|
+
this.bits -= 8
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
byte(b) {
|
|
79
|
+
if (this.len === this.buf.length) {
|
|
80
|
+
const next = new Uint8Array(this.buf.length * 2)
|
|
81
|
+
next.set(this.buf)
|
|
82
|
+
this.buf = next
|
|
83
|
+
}
|
|
84
|
+
this.buf[this.len++] = b
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
finish() {
|
|
88
|
+
if (this.bits > 0) this.byte(this.acc & 0xff)
|
|
89
|
+
this.acc = 0
|
|
90
|
+
this.bits = 0
|
|
91
|
+
return this.buf.slice(0, this.len)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Code lengths for `freqs`, at most `limit` bits: Huffman's by a sort with ties broken by symbol,
|
|
97
|
+
* then lengths over the limit moved up the tree, so the result is the same everywhere.
|
|
98
|
+
*/
|
|
99
|
+
export function codeLengths(freqs, limit) {
|
|
100
|
+
const n = freqs.length
|
|
101
|
+
const lengths = new Uint8Array(n)
|
|
102
|
+
const symbols = []
|
|
103
|
+
for (let s = 0; s < n; s++) if (freqs[s] > 0) symbols.push(s)
|
|
104
|
+
if (symbols.length === 0) return lengths
|
|
105
|
+
if (symbols.length === 1) {
|
|
106
|
+
lengths[symbols[0]] = 1
|
|
107
|
+
return lengths
|
|
108
|
+
}
|
|
109
|
+
// Nodes: leaves first (weight, symbol order), then internal ones made in order.
|
|
110
|
+
const weight = []
|
|
111
|
+
const parent = []
|
|
112
|
+
const order = symbols.slice().sort((a, b) => freqs[a] - freqs[b] || a - b)
|
|
113
|
+
for (const s of order) {
|
|
114
|
+
weight.push(freqs[s])
|
|
115
|
+
parent.push(-1)
|
|
116
|
+
}
|
|
117
|
+
// Two queues: leaves sorted, internal nodes made in nondecreasing weight.
|
|
118
|
+
let leaf = 0
|
|
119
|
+
let inner = order.length
|
|
120
|
+
const pick = () => {
|
|
121
|
+
if (leaf < order.length && (inner >= weight.length || weight[leaf] <= weight[inner]))
|
|
122
|
+
return leaf++
|
|
123
|
+
return inner++
|
|
124
|
+
}
|
|
125
|
+
// Until one node is left: join the two lightest.
|
|
126
|
+
while (order.length - leaf + (weight.length - inner) >= 2) {
|
|
127
|
+
const a = pick()
|
|
128
|
+
const b = pick()
|
|
129
|
+
weight.push(weight[a] + weight[b])
|
|
130
|
+
parent.push(-1)
|
|
131
|
+
parent[a] = weight.length - 1
|
|
132
|
+
parent[b] = weight.length - 1
|
|
133
|
+
}
|
|
134
|
+
const depth = new Array(weight.length).fill(0)
|
|
135
|
+
for (let i = weight.length - 2; i >= 0; i--) depth[i] = depth[parent[i]] + 1
|
|
136
|
+
let deepest = 0
|
|
137
|
+
for (let i = 0; i < order.length; i++) deepest = Math.max(deepest, depth[i])
|
|
138
|
+
const count = new Array(Math.max(limit, deepest) + 1).fill(0)
|
|
139
|
+
for (let i = 0; i < order.length; i++) count[depth[i]]++
|
|
140
|
+
// Lengths over the limit move up the tree two leaves at a time (JPEG's Annex K.3): a pair at
|
|
141
|
+
// depth i becomes one leaf at i − 1, and a leaf at a shallower j splits to make room. The Kraft
|
|
142
|
+
// sum stays 1.
|
|
143
|
+
for (let i = count.length - 1; i > limit; i--) {
|
|
144
|
+
while (count[i] > 0) {
|
|
145
|
+
let j = i - 2
|
|
146
|
+
while (count[j] === 0) j--
|
|
147
|
+
count[i] -= 2
|
|
148
|
+
count[i - 1] += 1
|
|
149
|
+
count[j + 1] += 2
|
|
150
|
+
count[j] -= 1
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Longest codes to the least frequent symbols (the sorted order), as Huffman would.
|
|
154
|
+
let i = 0
|
|
155
|
+
for (let l = limit; l >= 1; l--) {
|
|
156
|
+
for (let k = 0; k < count[l]; k++) lengths[order[i++]] = l
|
|
157
|
+
}
|
|
158
|
+
return lengths
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Canonical codes for code lengths (RFC 1951 3.2.2), bit-reversed for the stream. */
|
|
162
|
+
function canonical(lengths) {
|
|
163
|
+
const maxLen = 16
|
|
164
|
+
const blCount = new Uint16Array(maxLen)
|
|
165
|
+
for (const l of lengths) if (l > 0) blCount[l]++
|
|
166
|
+
const next = new Uint16Array(maxLen)
|
|
167
|
+
let code = 0
|
|
168
|
+
for (let bits = 1; bits < maxLen; bits++) {
|
|
169
|
+
code = (code + blCount[bits - 1]) << 1
|
|
170
|
+
next[bits] = code
|
|
171
|
+
}
|
|
172
|
+
const codes = new Uint16Array(lengths.length)
|
|
173
|
+
for (let s = 0; s < lengths.length; s++) {
|
|
174
|
+
const l = lengths[s]
|
|
175
|
+
if (l > 0) codes[s] = reverse(next[l]++, l)
|
|
176
|
+
}
|
|
177
|
+
return codes
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** LZ77 tokens: a literal is `byte`, a match is `(256 + length) | distance << 16`. */
|
|
181
|
+
function tokenize(input) {
|
|
182
|
+
const n = input.length
|
|
183
|
+
const out = new Uint32Array(n + 1)
|
|
184
|
+
let count = 0
|
|
185
|
+
const head = new Int32Array(HASH_SIZE).fill(-1)
|
|
186
|
+
const prev = new Int32Array(WINDOW).fill(-1)
|
|
187
|
+
const hash = (i) =>
|
|
188
|
+
(((input[i] << 10) ^ (input[i + 1] << 5) ^ input[i + 2]) * 2654435761) >>> (32 - HASH_BITS)
|
|
189
|
+
const insert = (i) => {
|
|
190
|
+
if (i + MIN_MATCH > n) return
|
|
191
|
+
const h = hash(i)
|
|
192
|
+
prev[i & (WINDOW - 1)] = head[h]
|
|
193
|
+
head[h] = i
|
|
194
|
+
}
|
|
195
|
+
/** Longest match at `i` among earlier positions: [length, distance]. */
|
|
196
|
+
let bestLen = 0
|
|
197
|
+
let bestDist = 0
|
|
198
|
+
const find = (i) => {
|
|
199
|
+
bestLen = 0
|
|
200
|
+
bestDist = 0
|
|
201
|
+
if (i + MIN_MATCH > n) return
|
|
202
|
+
let candidate = head[hash(i)]
|
|
203
|
+
const limit = Math.min(MAX_MATCH, n - i)
|
|
204
|
+
for (let chain = 0; chain < CHAIN && candidate >= 0 && i - candidate <= WINDOW; chain++) {
|
|
205
|
+
if (input[candidate + bestLen] === input[i + bestLen]) {
|
|
206
|
+
let l = 0
|
|
207
|
+
while (l < limit && input[candidate + l] === input[i + l]) l++
|
|
208
|
+
if (l > bestLen) {
|
|
209
|
+
bestLen = l
|
|
210
|
+
bestDist = i - candidate
|
|
211
|
+
if (l === limit) break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const p = prev[candidate & (WINDOW - 1)]
|
|
215
|
+
if (p >= candidate) break
|
|
216
|
+
candidate = p
|
|
217
|
+
}
|
|
218
|
+
if (bestLen < MIN_MATCH) bestLen = 0
|
|
219
|
+
}
|
|
220
|
+
let i = 0
|
|
221
|
+
while (i < n) {
|
|
222
|
+
find(i)
|
|
223
|
+
let len = bestLen
|
|
224
|
+
let dist = bestDist
|
|
225
|
+
if (len > 0 && len < GOOD && i + 1 < n) {
|
|
226
|
+
// Lazy: if the next byte starts a longer match, this one goes out as a literal.
|
|
227
|
+
insert(i)
|
|
228
|
+
find(i + 1)
|
|
229
|
+
if (bestLen > len) {
|
|
230
|
+
out[count++] = input[i]
|
|
231
|
+
i++
|
|
232
|
+
len = bestLen
|
|
233
|
+
dist = bestDist
|
|
234
|
+
for (let k = 0; k < len; k++) insert(i + k)
|
|
235
|
+
} else for (let k = 1; k < len; k++) insert(i + k)
|
|
236
|
+
out[count++] = (256 + len) | (dist << 16)
|
|
237
|
+
i += len
|
|
238
|
+
} else if (len > 0) {
|
|
239
|
+
for (let k = 0; k < len; k++) insert(i + k)
|
|
240
|
+
out[count++] = (256 + len) | (dist << 16)
|
|
241
|
+
i += len
|
|
242
|
+
} else {
|
|
243
|
+
insert(i)
|
|
244
|
+
out[count++] = input[i]
|
|
245
|
+
i++
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out.subarray(0, count)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Compresses `input` to a raw DEFLATE stream (one final block). Same bytes on every host. */
|
|
252
|
+
export function deflate(input) {
|
|
253
|
+
const tokens = tokenize(input)
|
|
254
|
+
const litFreq = new Uint32Array(286)
|
|
255
|
+
const distFreq = new Uint32Array(30)
|
|
256
|
+
for (const t of tokens) {
|
|
257
|
+
if (t < 256) litFreq[t]++
|
|
258
|
+
else {
|
|
259
|
+
const len = (t & 0xffff) - 256
|
|
260
|
+
litFreq[257 + lengthCode[len]]++
|
|
261
|
+
distFreq[distCode(t >>> 16)]++
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
litFreq[256] = 1
|
|
265
|
+
// At least one distance code (inflaters want a distance tree even with no matches).
|
|
266
|
+
if (distFreq.every((f) => f === 0)) distFreq[0] = 1
|
|
267
|
+
const litLen = codeLengths(litFreq, 15)
|
|
268
|
+
const distLen = codeLengths(distFreq, 15)
|
|
269
|
+
let hlit = 286
|
|
270
|
+
while (hlit > 257 && litLen[hlit - 1] === 0) hlit--
|
|
271
|
+
let hdist = 30
|
|
272
|
+
while (hdist > 1 && distLen[hdist - 1] === 0) hdist--
|
|
273
|
+
// Run-length code the two length tables together.
|
|
274
|
+
const all = new Uint8Array(hlit + hdist)
|
|
275
|
+
all.set(litLen.subarray(0, hlit))
|
|
276
|
+
all.set(distLen.subarray(0, hdist), hlit)
|
|
277
|
+
const cl = []
|
|
278
|
+
const clExtra = []
|
|
279
|
+
for (let i = 0; i < all.length; ) {
|
|
280
|
+
const v = all[i]
|
|
281
|
+
let run = 1
|
|
282
|
+
while (i + run < all.length && all[i + run] === v) run++
|
|
283
|
+
if (v === 0 && run >= 3) {
|
|
284
|
+
const r = Math.min(run, 138)
|
|
285
|
+
if (r >= 11) {
|
|
286
|
+
cl.push(18)
|
|
287
|
+
clExtra.push(r - 11)
|
|
288
|
+
} else {
|
|
289
|
+
cl.push(17)
|
|
290
|
+
clExtra.push(r - 3)
|
|
291
|
+
}
|
|
292
|
+
i += r
|
|
293
|
+
} else if (v !== 0 && run >= 4) {
|
|
294
|
+
cl.push(v)
|
|
295
|
+
clExtra.push(0)
|
|
296
|
+
const r = Math.min(run - 1, 6)
|
|
297
|
+
cl.push(16)
|
|
298
|
+
clExtra.push(r - 3)
|
|
299
|
+
i += 1 + r
|
|
300
|
+
} else {
|
|
301
|
+
cl.push(v)
|
|
302
|
+
clExtra.push(0)
|
|
303
|
+
i++
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const clFreq = new Uint32Array(19)
|
|
307
|
+
for (const c of cl) clFreq[c]++
|
|
308
|
+
const clLen = codeLengths(clFreq, 7)
|
|
309
|
+
const clCodes = canonical(clLen)
|
|
310
|
+
let hclen = 19
|
|
311
|
+
while (hclen > 4 && clLen[CL_ORDER[hclen - 1]] === 0) hclen--
|
|
312
|
+
const litCodes = canonical(litLen)
|
|
313
|
+
const distCodes = canonical(distLen)
|
|
314
|
+
const w = new BitWriter()
|
|
315
|
+
w.write(1, 1) // final block
|
|
316
|
+
w.write(2, 2) // dynamic Huffman
|
|
317
|
+
w.write(hlit - 257, 5)
|
|
318
|
+
w.write(hdist - 1, 5)
|
|
319
|
+
w.write(hclen - 4, 4)
|
|
320
|
+
for (let i = 0; i < hclen; i++) w.write(clLen[CL_ORDER[i]], 3)
|
|
321
|
+
for (let i = 0; i < cl.length; i++) {
|
|
322
|
+
const c = cl[i]
|
|
323
|
+
w.write(clCodes[c], clLen[c])
|
|
324
|
+
if (c === 16) w.write(clExtra[i], 2)
|
|
325
|
+
else if (c === 17) w.write(clExtra[i], 3)
|
|
326
|
+
else if (c === 18) w.write(clExtra[i], 7)
|
|
327
|
+
}
|
|
328
|
+
for (const t of tokens) {
|
|
329
|
+
if (t < 256) {
|
|
330
|
+
w.write(litCodes[t], litLen[t])
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
const len = (t & 0xffff) - 256
|
|
334
|
+
const lc = lengthCode[len]
|
|
335
|
+
w.write(litCodes[257 + lc], litLen[257 + lc])
|
|
336
|
+
if (LENGTH_EXTRA[lc] > 0) w.write(len - LENGTH_BASE[lc], LENGTH_EXTRA[lc])
|
|
337
|
+
const dist = t >>> 16
|
|
338
|
+
const dc = distCode(dist)
|
|
339
|
+
w.write(distCodes[dc], distLen[dc])
|
|
340
|
+
if (DIST_EXTRA[dc] > 0) w.write(dist - DIST_BASE[dc], DIST_EXTRA[dc])
|
|
341
|
+
}
|
|
342
|
+
w.write(litCodes[256], litLen[256])
|
|
343
|
+
return w.finish()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- inflate -----------------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
/** A Huffman decoding table: counts per length and symbols in canonical order. */
|
|
349
|
+
function decoder(lengths, n) {
|
|
350
|
+
const counts = new Uint16Array(16)
|
|
351
|
+
for (let i = 0; i < n; i++) counts[lengths[i]]++
|
|
352
|
+
counts[0] = 0
|
|
353
|
+
const offs = new Uint16Array(16)
|
|
354
|
+
for (let l = 1; l < 16; l++) offs[l] = offs[l - 1] + counts[l - 1]
|
|
355
|
+
const symbols = new Uint16Array(n)
|
|
356
|
+
for (let i = 0; i < n; i++) if (lengths[i] > 0) symbols[offs[lengths[i]]++] = i
|
|
357
|
+
return { counts, symbols }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const fixedLit = (() => {
|
|
361
|
+
const l = new Uint8Array(288)
|
|
362
|
+
l.fill(8, 0, 144)
|
|
363
|
+
l.fill(9, 144, 256)
|
|
364
|
+
l.fill(7, 256, 280)
|
|
365
|
+
l.fill(8, 280, 288)
|
|
366
|
+
return decoder(l, 288)
|
|
367
|
+
})()
|
|
368
|
+
const fixedDist = decoder(new Uint8Array(30).fill(5), 30)
|
|
369
|
+
|
|
370
|
+
function corrupt(why) {
|
|
371
|
+
return Object.assign(new Error(`A terrain page doesn't inflate: ${why}`), {
|
|
372
|
+
code: 'terrain/corrupt-pack',
|
|
373
|
+
hint: 'The pack is damaged or from another bake: `shard terrain bake --force` rewrites it.',
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Decompresses a raw DEFLATE stream into exactly `size` bytes (a page's known length). Any
|
|
379
|
+
* conforming stream works; terrain packs hold `deflate`'s.
|
|
380
|
+
*/
|
|
381
|
+
export function inflate(input, size) {
|
|
382
|
+
const out = new Uint8Array(size)
|
|
383
|
+
let o = 0
|
|
384
|
+
let pos = 0
|
|
385
|
+
let acc = 0
|
|
386
|
+
let bits = 0
|
|
387
|
+
const need = (n) => {
|
|
388
|
+
while (bits < n) {
|
|
389
|
+
if (pos >= input.length) throw corrupt('it ends early')
|
|
390
|
+
acc |= input[pos++] << bits
|
|
391
|
+
bits += 8
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const take = (n) => {
|
|
395
|
+
if (n === 0) return 0
|
|
396
|
+
need(n)
|
|
397
|
+
const v = acc & ((1 << n) - 1)
|
|
398
|
+
acc >>>= n
|
|
399
|
+
bits -= n
|
|
400
|
+
return v
|
|
401
|
+
}
|
|
402
|
+
const decode = (d) => {
|
|
403
|
+
let code = 0
|
|
404
|
+
let first = 0
|
|
405
|
+
let index = 0
|
|
406
|
+
for (let len = 1; len < 16; len++) {
|
|
407
|
+
code |= take(1)
|
|
408
|
+
const count = d.counts[len]
|
|
409
|
+
if (code - count < first) return d.symbols[index + (code - first)]
|
|
410
|
+
index += count
|
|
411
|
+
first += count
|
|
412
|
+
first <<= 1
|
|
413
|
+
code <<= 1
|
|
414
|
+
}
|
|
415
|
+
throw corrupt('a code is invalid')
|
|
416
|
+
}
|
|
417
|
+
let final = 0
|
|
418
|
+
while (!final) {
|
|
419
|
+
final = take(1)
|
|
420
|
+
const type = take(2)
|
|
421
|
+
if (type === 0) {
|
|
422
|
+
acc = 0
|
|
423
|
+
bits = 0
|
|
424
|
+
if (pos + 4 > input.length) throw corrupt('it ends early')
|
|
425
|
+
const len = input[pos] | (input[pos + 1] << 8)
|
|
426
|
+
pos += 4
|
|
427
|
+
if (o + len > size || pos + len > input.length) throw corrupt('a stored block overruns')
|
|
428
|
+
out.set(input.subarray(pos, pos + len), o)
|
|
429
|
+
o += len
|
|
430
|
+
pos += len
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
let lit = fixedLit
|
|
434
|
+
let dist = fixedDist
|
|
435
|
+
if (type === 2) {
|
|
436
|
+
const hlit = take(5) + 257
|
|
437
|
+
const hdist = take(5) + 1
|
|
438
|
+
const hclen = take(4) + 4
|
|
439
|
+
const clLen = new Uint8Array(19)
|
|
440
|
+
for (let i = 0; i < hclen; i++) clLen[CL_ORDER[i]] = take(3)
|
|
441
|
+
const cl = decoder(clLen, 19)
|
|
442
|
+
const lengths = new Uint8Array(hlit + hdist)
|
|
443
|
+
for (let i = 0; i < hlit + hdist; ) {
|
|
444
|
+
const sym = decode(cl)
|
|
445
|
+
if (sym < 16) lengths[i++] = sym
|
|
446
|
+
else {
|
|
447
|
+
let rep = 0
|
|
448
|
+
let v = 0
|
|
449
|
+
if (sym === 16) {
|
|
450
|
+
if (i === 0) throw corrupt('a repeat has nothing before it')
|
|
451
|
+
v = lengths[i - 1]
|
|
452
|
+
rep = 3 + take(2)
|
|
453
|
+
} else if (sym === 17) rep = 3 + take(3)
|
|
454
|
+
else rep = 11 + take(7)
|
|
455
|
+
if (i + rep > lengths.length) throw corrupt('code lengths overrun')
|
|
456
|
+
lengths.fill(v, i, i + rep)
|
|
457
|
+
i += rep
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
lit = decoder(lengths.subarray(0, hlit), hlit)
|
|
461
|
+
dist = decoder(lengths.subarray(hlit), hdist)
|
|
462
|
+
} else if (type !== 1) throw corrupt('a block has an unknown type')
|
|
463
|
+
for (;;) {
|
|
464
|
+
const sym = decode(lit)
|
|
465
|
+
if (sym < 256) {
|
|
466
|
+
if (o >= size) throw corrupt('it is longer than the page')
|
|
467
|
+
out[o++] = sym
|
|
468
|
+
} else if (sym === 256) break
|
|
469
|
+
else {
|
|
470
|
+
const lc = sym - 257
|
|
471
|
+
if (lc >= 29) throw corrupt('a length code is invalid')
|
|
472
|
+
const len = LENGTH_BASE[lc] + take(LENGTH_EXTRA[lc])
|
|
473
|
+
const dc = decode(dist)
|
|
474
|
+
if (dc >= 30) throw corrupt('a distance code is invalid')
|
|
475
|
+
const d = DIST_BASE[dc] + take(DIST_EXTRA[dc])
|
|
476
|
+
if (d > o || o + len > size) throw corrupt('a match reaches outside the page')
|
|
477
|
+
for (let k = 0; k < len; k++) out[o + k] = out[o + k - d]
|
|
478
|
+
o += len
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (o !== size) throw corrupt(`it holds ${o} bytes, not ${size}`)
|
|
483
|
+
return out
|
|
484
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AssetStore } from '@aethervtt/shard-assets';
|
|
2
|
+
/** A heightmap image (spec 0071): samples 0–1, row by row, top row first. */
|
|
3
|
+
export interface Heightmap {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
readonly data: Float32Array;
|
|
7
|
+
/** Bumps on hot reload. */
|
|
8
|
+
version: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const Heightmaps: import("@aethervtt/shard-core").ResourceDef<AssetStore<Heightmap, "Heightmap">>;
|
|
11
|
+
/** What the importer stores: a small header and the samples (u16 or f32, little-endian). */
|
|
12
|
+
interface HeightmapArtifact {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
bits: 16 | 32;
|
|
16
|
+
}
|
|
17
|
+
export declare const HeightmapAssetType: import("@aethervtt/shard-assets").AssetTypeDef<Heightmap>;
|
|
18
|
+
/** Decodes a 16-bit grayscale, non-interlaced PNG to samples 0–65535 (row by row). */
|
|
19
|
+
export declare function decodeHeightPng(path: string, png: Uint8Array): HeightmapArtifact & {
|
|
20
|
+
samples: Uint16Array;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* `*.r16`, `*.r32` and `*.height.png` (or any 16-bit grayscale PNG whose `.meta` names this
|
|
24
|
+
* importer): a Heightmap for terrain image layers and paint masks (spec 0071).
|
|
25
|
+
*/
|
|
26
|
+
export declare const HeightmapImporter: import("@aethervtt/shard-assets").ImporterDef;
|
|
27
|
+
export {};
|