minecraft-renderer 0.1.84 → 0.1.86
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/mesher.js.map +2 -2
- package/dist/mesherWasm.js +214 -214
- package/dist/minecraft-renderer.js +15 -19
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +4 -8
- package/package.json +1 -1
- package/src/lib/worldrendererCommon.ts +7 -0
- package/src/mesher-shared/shared.ts +5 -0
- package/src/three/modules/starfield.ts +3 -16
- package/src/wasm-mesher/bridge/render-from-wasm.ts +51 -21
- package/src/wasm-mesher/tests/blendEmissionOrder.test.ts +123 -0
- package/src/wasm-mesher/tests/crossColumnCulling.test.ts +205 -0
- package/src/wasm-mesher/tests/mesherWasmChunkBuffer.test.ts +24 -0
- package/src/wasm-mesher/tests/mesherWasmNeighborhood.test.ts +91 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +148 -69
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +28 -0
- package/src/wasm-mesher/worker/mesherWasmNeighborhood.ts +125 -0
|
@@ -10,6 +10,16 @@ import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMe
|
|
|
10
10
|
import { SectionRequestTracker } from './mesherWasmRequestTracker'
|
|
11
11
|
import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
|
|
12
12
|
import { CONVERSION_CACHE_LIMIT, clearConversionCache, getOrConvertColumn, invalidateConversion, setConversionCacheLimit } from './mesherWasmConversionCache'
|
|
13
|
+
import { PendingChunkBuffer } from './mesherWasmChunkBuffer'
|
|
14
|
+
import {
|
|
15
|
+
PendingNeighborHealTracker,
|
|
16
|
+
SIDE_NEIGHBOR_OFFSETS,
|
|
17
|
+
collectChunksForColumnUnion,
|
|
18
|
+
columnDataAvailable,
|
|
19
|
+
countParsedCache3x3,
|
|
20
|
+
countWorldColumns3x3,
|
|
21
|
+
type PacketCaches
|
|
22
|
+
} from './mesherWasmNeighborhood'
|
|
13
23
|
|
|
14
24
|
let wasm: typeof import('../runtime-build/wasm_mesher.js') | null = null
|
|
15
25
|
let wasmInitialized = false
|
|
@@ -304,6 +314,77 @@ interface ParsedV16Entry {
|
|
|
304
314
|
}
|
|
305
315
|
const parsedV16Cache = new Map<string, ParsedV16Entry>()
|
|
306
316
|
|
|
317
|
+
const pendingChunks = new PendingChunkBuffer()
|
|
318
|
+
const pendingNeighborHeal = new PendingNeighborHealTracker()
|
|
319
|
+
let blindMeshWarnCount = 0
|
|
320
|
+
const BLIND_MESH_WARN_LIMIT = 5
|
|
321
|
+
|
|
322
|
+
const packetCaches = (): PacketCaches => ({
|
|
323
|
+
raw: rawMapChunkCache,
|
|
324
|
+
v17: parsedV17Cache,
|
|
325
|
+
v16: parsedV16Cache
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
function notifyColumnsAwaitingNeighbor(neighborX: number, neighborZ: number) {
|
|
329
|
+
for (const col of pendingNeighborHeal.takeColumnsAwaitingNeighbor(neighborX, neighborZ)) {
|
|
330
|
+
postMessage({ type: 'neighborDataArrived', x: col.x, z: col.z, workerIndex })
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function onColumnDataArrived(x: number, z: number) {
|
|
335
|
+
notifyColumnsAwaitingNeighbor(x, z)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function warnGenuinelyBlindMesh(columnX: number, columnZ: number, missingSides: number) {
|
|
339
|
+
if (missingSides <= 0) return
|
|
340
|
+
blindMeshWarnCount++
|
|
341
|
+
if (blindMeshWarnCount <= BLIND_MESH_WARN_LIMIT) {
|
|
342
|
+
console.warn(`[mesher] column ${columnX},${columnZ} meshed with ${missingSides}/4 side neighbors missing from both pipelines — genuinely blind mesh`)
|
|
343
|
+
} else if (blindMeshWarnCount === BLIND_MESH_WARN_LIMIT + 1) {
|
|
344
|
+
console.warn(`[mesher] genuinely blind mesh warnings suppressed after ${BLIND_MESH_WARN_LIMIT} occurrences`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function processChunkMessage(data: { x: number; z: number; chunk: any; customBlockModels?: any }) {
|
|
349
|
+
world.addColumn(data.x, data.z, data.chunk)
|
|
350
|
+
// --- Light sync (folded in from main): apply any light that arrived before
|
|
351
|
+
// this column's data. Doing it here (not inline in the handler) means
|
|
352
|
+
// chunks replayed from pendingChunks via drainPendingChunks() also get
|
|
353
|
+
// their buffered light applied.
|
|
354
|
+
syncV17LightToColumn(data.x, data.z)
|
|
355
|
+
const lightKey = rawCacheKey(data.x, data.z)
|
|
356
|
+
if (pendingLightDirtyColumns.delete(lightKey) || updateLightV17Cache.has(lightKey)) {
|
|
357
|
+
dirtyColumnSectionsForLightUpdate(data.x, data.z)
|
|
358
|
+
}
|
|
359
|
+
if (data.customBlockModels) {
|
|
360
|
+
const chunkKey = `${data.x},${data.z}`
|
|
361
|
+
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
362
|
+
}
|
|
363
|
+
const sectionH = SECTION_HEIGHT
|
|
364
|
+
const minY = config?.worldMinY ?? 0
|
|
365
|
+
const maxY = config?.worldMaxY ?? 256
|
|
366
|
+
const column = world.getColumn(data.x, data.z)
|
|
367
|
+
let hasAnySection = false
|
|
368
|
+
for (let y = minY; y < maxY; y += sectionH) {
|
|
369
|
+
if (column?.getSection?.(new Vec3(0, y, 0))) {
|
|
370
|
+
hasAnySection = true
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!hasAnySection) {
|
|
375
|
+
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
376
|
+
postMessage({ type: 'heightmap', key: `${data.x >> 4},${data.z >> 4}`, heightmap: emptyHeightmap }, [emptyHeightmap.buffer])
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function drainPendingChunks() {
|
|
381
|
+
if (!world) return
|
|
382
|
+
pendingChunks.drain(msg => {
|
|
383
|
+
processChunkMessage(msg)
|
|
384
|
+
onColumnDataArrived(msg.x, msg.z)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
307
388
|
// 1.16 sky/block light cache — same shape as the v17 entry, populated by
|
|
308
389
|
// `processUpdateLightV16` (which calls the shared `parseUpdateLightV17`
|
|
309
390
|
// WASM export). Separate map for the same isolation reasons as
|
|
@@ -748,6 +829,7 @@ const handleMessage = async (data: any) => {
|
|
|
748
829
|
await initWasm()
|
|
749
830
|
allDataReady = true
|
|
750
831
|
workerIndex = data.workerIndex
|
|
832
|
+
drainPendingChunks()
|
|
751
833
|
break
|
|
752
834
|
}
|
|
753
835
|
case 'dirty': {
|
|
@@ -759,44 +841,17 @@ const handleMessage = async (data: any) => {
|
|
|
759
841
|
// Invalidate BEFORE replacing the column reference so a stale entry
|
|
760
842
|
// can never outlive the old chunk object.
|
|
761
843
|
invalidateConversion(data.x, data.z)
|
|
762
|
-
if (!world)
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const chunkKey = `${data.x},${data.z}`
|
|
771
|
-
world.customBlockModels.set(chunkKey, data.customBlockModels)
|
|
772
|
-
}
|
|
773
|
-
// Safety-net heightmap push for fully empty columns. With WASM
|
|
774
|
-
// mesher as the sole path, the main thread no longer requests
|
|
775
|
-
// `getHeightmap` on chunk load — heightmaps come from
|
|
776
|
-
// `processColumnTick`. But a fully empty column (no sections, or
|
|
777
|
-
// all sections missing) never enters that path because
|
|
778
|
-
// `setSectionDirty` short-circuits when `chunk.getSection(pos)` is
|
|
779
|
-
// falsy, so `processColumnTick` never sees it. Without this push
|
|
780
|
-
// downstream consumers (e.g. `rain.ts`) would have no heightmap
|
|
781
|
-
// entry for such columns. We send a cheap sentinel-filled
|
|
782
|
-
// `Int16Array(256).fill(-32768)` — no JS heightmap scan — only when
|
|
783
|
-
// we detect zero sections; non-empty columns get their real
|
|
784
|
-
// heightmap from the next `processColumnTick`.
|
|
785
|
-
const sectionH = SECTION_HEIGHT
|
|
786
|
-
const minY = config?.worldMinY ?? 0
|
|
787
|
-
const maxY = config?.worldMaxY ?? 256
|
|
788
|
-
const column = world.getColumn(data.x, data.z)
|
|
789
|
-
let hasAnySection = false
|
|
790
|
-
for (let y = minY; y < maxY; y += sectionH) {
|
|
791
|
-
if (column?.getSection?.(new Vec3(0, y, 0))) {
|
|
792
|
-
hasAnySection = true
|
|
793
|
-
break
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (!hasAnySection) {
|
|
797
|
-
const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
|
|
798
|
-
postMessage({ type: 'heightmap', key: `${data.x >> 4},${data.z >> 4}`, heightmap: emptyHeightmap }, [emptyHeightmap.buffer])
|
|
844
|
+
if (!world) {
|
|
845
|
+
pendingChunks.enqueue({
|
|
846
|
+
x: data.x,
|
|
847
|
+
z: data.z,
|
|
848
|
+
chunk: data.chunk,
|
|
849
|
+
customBlockModels: data.customBlockModels
|
|
850
|
+
})
|
|
851
|
+
break
|
|
799
852
|
}
|
|
853
|
+
processChunkMessage(data)
|
|
854
|
+
onColumnDataArrived(data.x, data.z)
|
|
800
855
|
break
|
|
801
856
|
}
|
|
802
857
|
case 'unloadChunk': {
|
|
@@ -810,6 +865,7 @@ const handleMessage = async (data: any) => {
|
|
|
810
865
|
if (!world) break
|
|
811
866
|
world.removeColumn(data.x, data.z)
|
|
812
867
|
world.customBlockModels.delete(`${data.x},${data.z}`)
|
|
868
|
+
pendingNeighborHeal.clearColumn(data.x, data.z)
|
|
813
869
|
requestTracker.clearColumn(data.x, data.z)
|
|
814
870
|
for (const key of [...dirtySections.keys()]) {
|
|
815
871
|
const [sx, , sz] = key.split(',').map(Number)
|
|
@@ -856,6 +912,7 @@ const handleMessage = async (data: any) => {
|
|
|
856
912
|
numSections: data.numSections as number
|
|
857
913
|
})
|
|
858
914
|
invalidateConversion(data.x, data.z)
|
|
915
|
+
onColumnDataArrived(data.x, data.z)
|
|
859
916
|
break
|
|
860
917
|
}
|
|
861
918
|
case 'setParsedMapChunkV17': {
|
|
@@ -869,6 +926,7 @@ const handleMessage = async (data: any) => {
|
|
|
869
926
|
biomes: data.biomes as Int32Array | undefined
|
|
870
927
|
})
|
|
871
928
|
invalidateConversion(data.x, data.z)
|
|
929
|
+
onColumnDataArrived(data.x, data.z)
|
|
872
930
|
break
|
|
873
931
|
}
|
|
874
932
|
case 'setUpdateLightV17': {
|
|
@@ -893,6 +951,7 @@ const handleMessage = async (data: any) => {
|
|
|
893
951
|
biomes: data.biomes as Int32Array
|
|
894
952
|
})
|
|
895
953
|
invalidateConversion(data.x, data.z)
|
|
954
|
+
onColumnDataArrived(data.x, data.z)
|
|
896
955
|
break
|
|
897
956
|
}
|
|
898
957
|
case 'setUpdateLightV16': {
|
|
@@ -913,6 +972,9 @@ const handleMessage = async (data: any) => {
|
|
|
913
972
|
pendingLightDirtyColumns.clear()
|
|
914
973
|
parsedV16Cache.clear()
|
|
915
974
|
updateLightV16Cache.clear()
|
|
975
|
+
pendingChunks.clear()
|
|
976
|
+
pendingNeighborHeal.clear()
|
|
977
|
+
blindMeshWarnCount = 0
|
|
916
978
|
globalVar.mcData = null
|
|
917
979
|
globalVar.loadedData = null
|
|
918
980
|
allDataReady = false
|
|
@@ -984,23 +1046,8 @@ self.onmessage = ({ data }) => {
|
|
|
984
1046
|
// Section height is always 16 in column mode (the only WASM path).
|
|
985
1047
|
const getSectionHeight = () => SECTION_HEIGHT
|
|
986
1048
|
|
|
987
|
-
// 3x3 X/Z neighbor set for column meshing. Y-agnostic because full-column
|
|
988
|
-
// meshing converts the entire world Y range in one go.
|
|
989
1049
|
function collectChunksForColumn(x: number, z: number) {
|
|
990
|
-
|
|
991
|
-
const target = world.getColumn(x, z)
|
|
992
|
-
if (target) result.push({ x, z, chunk: target })
|
|
993
|
-
const offsets = [-16, 0, 16]
|
|
994
|
-
for (const dx of offsets) {
|
|
995
|
-
for (const dz of offsets) {
|
|
996
|
-
if (dx === 0 && dz === 0) continue
|
|
997
|
-
const nx = x + dx
|
|
998
|
-
const nz = z + dz
|
|
999
|
-
const c = world.getColumn(nx, nz)
|
|
1000
|
-
if (c) result.push({ x: nx, z: nz, chunk: c })
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
return result
|
|
1050
|
+
return collectChunksForColumnUnion(x, z, (nx, nz) => world.getColumn(nx, nz), packetCaches())
|
|
1004
1051
|
}
|
|
1005
1052
|
|
|
1006
1053
|
function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeight: number, hadErrors: boolean): MesherGeometryOutput {
|
|
@@ -1079,6 +1126,9 @@ function processColumnTick() {
|
|
|
1079
1126
|
let preCacheMisses = 0
|
|
1080
1127
|
let hadError = false
|
|
1081
1128
|
let columnMeshPath = 'none'
|
|
1129
|
+
let chunkCount = 0
|
|
1130
|
+
let worldColumns3x3 = 0
|
|
1131
|
+
let parsedCache3x3 = 0
|
|
1082
1132
|
// Outer-scope timestamps so we can finalize `processTime` and
|
|
1083
1133
|
// `postPhase` AFTER the per-section emit loop runs (the loop builds
|
|
1084
1134
|
// typed arrays, walks block-entity metadata, and calls postMessage —
|
|
@@ -1093,7 +1143,21 @@ function processColumnTick() {
|
|
|
1093
1143
|
const t0 = start
|
|
1094
1144
|
try {
|
|
1095
1145
|
const chunksToUse = collectChunksForColumn(x, z)
|
|
1096
|
-
|
|
1146
|
+
chunkCount = chunksToUse.length
|
|
1147
|
+
const caches = packetCaches()
|
|
1148
|
+
worldColumns3x3 = countWorldColumns3x3(x, z, (nx, nz) => world.getColumn(nx, nz))
|
|
1149
|
+
parsedCache3x3 = countParsedCache3x3(x, z, caches)
|
|
1150
|
+
|
|
1151
|
+
let missingSideCount = 0
|
|
1152
|
+
for (const [dx, dz] of SIDE_NEIGHBOR_OFFSETS) {
|
|
1153
|
+
const nx = x + dx
|
|
1154
|
+
const nz = z + dz
|
|
1155
|
+
if (!columnDataAvailable(nx, nz, (cx, cz) => world.getColumn(cx, cz), caches)) {
|
|
1156
|
+
pendingNeighborHeal.recordMissingSide(x, z, nx, nz)
|
|
1157
|
+
missingSideCount++
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
warnGenuinelyBlindMesh(x, z, missingSideCount)
|
|
1097
1161
|
|
|
1098
1162
|
let wasmResult: any
|
|
1099
1163
|
let t1 = 0
|
|
@@ -1183,7 +1247,9 @@ function processColumnTick() {
|
|
|
1183
1247
|
|
|
1184
1248
|
if (!wasmResult) {
|
|
1185
1249
|
// --- Old two-step path (multi-column or fused fallback) ---
|
|
1186
|
-
const conversions
|
|
1250
|
+
const conversions: ChunkConversionResult[] = []
|
|
1251
|
+
const meshChunks: typeof chunksToUse = []
|
|
1252
|
+
for (const { x: cx, z: cz, chunk } of chunksToUse) {
|
|
1187
1253
|
const cs = performance.now()
|
|
1188
1254
|
const rawEntry = rawMapChunkCache.get(rawCacheKey(cx, cz))
|
|
1189
1255
|
const v17Entry = parsedV17Cache.get(rawCacheKey(cx, cz))
|
|
@@ -1211,6 +1277,9 @@ function processColumnTick() {
|
|
|
1211
1277
|
if (!conv) {
|
|
1212
1278
|
// JS-fallback (column walk) — still cached, since this is the
|
|
1213
1279
|
// expensive path the conversion cache was built for.
|
|
1280
|
+
// Cache-only neighbors (pipeline B without world.columns) are
|
|
1281
|
+
// skipped — fused multi should have handled them already.
|
|
1282
|
+
if (!chunk) continue
|
|
1214
1283
|
const cached = getOrConvertColumn(
|
|
1215
1284
|
cx,
|
|
1216
1285
|
cz,
|
|
@@ -1234,12 +1303,18 @@ function processColumnTick() {
|
|
|
1234
1303
|
preNeighborConvert += ce - cs
|
|
1235
1304
|
preNeighborCount++
|
|
1236
1305
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1306
|
+
conversions.push(conv)
|
|
1307
|
+
meshChunks.push({ x: cx, z: cz, chunk })
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const twoStepChunkCount = conversions.length
|
|
1311
|
+
if (twoStepChunkCount === 0) {
|
|
1312
|
+
throw new Error(`[WASM Mesher] no convertible columns for ${x},${z}`)
|
|
1313
|
+
}
|
|
1239
1314
|
|
|
1240
1315
|
const { invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks } = conversions[0]
|
|
1241
1316
|
|
|
1242
|
-
if (
|
|
1317
|
+
if (twoStepChunkCount === 1 || !(wasm as any).generate_geometry_multi) {
|
|
1243
1318
|
const { blockStates, blockLight, skyLight, biomesArray } = conversions[0]
|
|
1244
1319
|
t1 = performance.now()
|
|
1245
1320
|
wasmResult = wasm.generate_geometry(
|
|
@@ -1267,17 +1342,17 @@ function processColumnTick() {
|
|
|
1267
1342
|
} else {
|
|
1268
1343
|
const tBuildStart = performance.now()
|
|
1269
1344
|
const perChunkLen = conversions[0].blockStates.length
|
|
1270
|
-
const xs = new Int32Array(
|
|
1271
|
-
const zs = new Int32Array(
|
|
1272
|
-
const blockStatesAll = new Uint16Array(perChunkLen *
|
|
1273
|
-
const blockLightAll = new Uint8Array(perChunkLen *
|
|
1274
|
-
const skyLightAll = new Uint8Array(perChunkLen *
|
|
1275
|
-
const biomesAll = new Uint8Array(perChunkLen *
|
|
1276
|
-
|
|
1277
|
-
for (let i = 0; i <
|
|
1345
|
+
const xs = new Int32Array(twoStepChunkCount)
|
|
1346
|
+
const zs = new Int32Array(twoStepChunkCount)
|
|
1347
|
+
const blockStatesAll = new Uint16Array(perChunkLen * twoStepChunkCount)
|
|
1348
|
+
const blockLightAll = new Uint8Array(perChunkLen * twoStepChunkCount)
|
|
1349
|
+
const skyLightAll = new Uint8Array(perChunkLen * twoStepChunkCount)
|
|
1350
|
+
const biomesAll = new Uint8Array(perChunkLen * twoStepChunkCount)
|
|
1351
|
+
|
|
1352
|
+
for (let i = 0; i < twoStepChunkCount; i++) {
|
|
1278
1353
|
const c = conversions[i]
|
|
1279
|
-
xs[i] =
|
|
1280
|
-
zs[i] =
|
|
1354
|
+
xs[i] = meshChunks[i].x
|
|
1355
|
+
zs[i] = meshChunks[i].z
|
|
1281
1356
|
blockStatesAll.set(c.blockStates, perChunkLen * i)
|
|
1282
1357
|
blockLightAll.set(c.blockLight, perChunkLen * i)
|
|
1283
1358
|
skyLightAll.set(c.skyLight, perChunkLen * i)
|
|
@@ -1545,7 +1620,11 @@ function processColumnTick() {
|
|
|
1545
1620
|
preTypedArrayBuild: !attributed ? preTypedArrayBuild : 0,
|
|
1546
1621
|
preOther: !attributed ? preOther : 0,
|
|
1547
1622
|
preCacheHits: !attributed ? preCacheHits : 0,
|
|
1548
|
-
preCacheMisses: !attributed ? preCacheMisses : 0
|
|
1623
|
+
preCacheMisses: !attributed ? preCacheMisses : 0,
|
|
1624
|
+
chunkCount: !attributed ? chunkCount : 0,
|
|
1625
|
+
worldColumns3x3: !attributed ? worldColumns3x3 : 0,
|
|
1626
|
+
parsedCache3x3: !attributed ? parsedCache3x3 : 0,
|
|
1627
|
+
columnMeshPath: !attributed ? columnMeshPath : 'none'
|
|
1549
1628
|
})
|
|
1550
1629
|
attributed = true
|
|
1551
1630
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export type PendingChunkMessage = {
|
|
3
|
+
x: number
|
|
4
|
+
z: number
|
|
5
|
+
chunk: any
|
|
6
|
+
customBlockModels?: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PendingChunkBuffer {
|
|
10
|
+
private pending: PendingChunkMessage[] = []
|
|
11
|
+
|
|
12
|
+
enqueue(msg: PendingChunkMessage) {
|
|
13
|
+
this.pending.push(msg)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
drain(apply: (msg: PendingChunkMessage) => void) {
|
|
17
|
+
const batch = this.pending.splice(0)
|
|
18
|
+
for (const msg of batch) apply(msg)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get size() {
|
|
22
|
+
return this.pending.length
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clear() {
|
|
26
|
+
this.pending.length = 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export type ColumnChunkEntry = { x: number; z: number; chunk: any | null }
|
|
3
|
+
|
|
4
|
+
export interface PacketCacheSet {
|
|
5
|
+
has: (key: string) => boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PacketCaches {
|
|
9
|
+
raw: PacketCacheSet
|
|
10
|
+
v17: PacketCacheSet
|
|
11
|
+
v16: PacketCacheSet
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function columnCacheKey(x: number, z: number) {
|
|
15
|
+
return `${x},${z}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function columnHasPacketCache(x: number, z: number, caches: PacketCaches): boolean {
|
|
19
|
+
const k = columnCacheKey(x, z)
|
|
20
|
+
return caches.raw.has(k) || caches.v17.has(k) || caches.v16.has(k)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function columnDataAvailable(x: number, z: number, getColumn: (x: number, z: number) => any | null | undefined, caches: PacketCaches): boolean {
|
|
24
|
+
return !!getColumn(x, z) || columnHasPacketCache(x, z, caches)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function countWorldColumns3x3(x: number, z: number, getColumn: (x: number, z: number) => any | null | undefined): number {
|
|
28
|
+
let count = 0
|
|
29
|
+
for (const dx of [-16, 0, 16]) {
|
|
30
|
+
for (const dz of [-16, 0, 16]) {
|
|
31
|
+
if (getColumn(x + dx, z + dz)) count++
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return count
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function countParsedCache3x3(x: number, z: number, caches: PacketCaches): number {
|
|
38
|
+
let count = 0
|
|
39
|
+
for (const dx of [-16, 0, 16]) {
|
|
40
|
+
for (const dz of [-16, 0, 16]) {
|
|
41
|
+
if (columnHasPacketCache(x + dx, z + dz, caches)) count++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return count
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function collectChunksForColumnUnion(
|
|
48
|
+
x: number,
|
|
49
|
+
z: number,
|
|
50
|
+
getColumn: (x: number, z: number) => any | null | undefined,
|
|
51
|
+
caches: PacketCaches
|
|
52
|
+
): ColumnChunkEntry[] {
|
|
53
|
+
const result: ColumnChunkEntry[] = []
|
|
54
|
+
const seen = new Set<string>()
|
|
55
|
+
|
|
56
|
+
const add = (nx: number, nz: number) => {
|
|
57
|
+
const key = columnCacheKey(nx, nz)
|
|
58
|
+
if (seen.has(key)) return
|
|
59
|
+
seen.add(key)
|
|
60
|
+
const chunk = getColumn(nx, nz) ?? null
|
|
61
|
+
if (chunk || columnHasPacketCache(nx, nz, caches)) {
|
|
62
|
+
result.push({ x: nx, z: nz, chunk })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
add(x, z)
|
|
67
|
+
for (const dx of [-16, 0, 16]) {
|
|
68
|
+
for (const dz of [-16, 0, 16]) {
|
|
69
|
+
if (dx === 0 && dz === 0) continue
|
|
70
|
+
add(x + dx, z + dz)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const SIDE_NEIGHBOR_OFFSETS = [
|
|
77
|
+
[-16, 0],
|
|
78
|
+
[16, 0],
|
|
79
|
+
[0, -16],
|
|
80
|
+
[0, 16]
|
|
81
|
+
] as const
|
|
82
|
+
|
|
83
|
+
export class PendingNeighborHealTracker {
|
|
84
|
+
// Neighbor column key -> columns that meshed without this neighbor.
|
|
85
|
+
private awaiting = new Map<string, Set<string>>()
|
|
86
|
+
|
|
87
|
+
private colKey(x: number, z: number) {
|
|
88
|
+
return columnCacheKey(x, z)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
recordMissingSide(columnX: number, columnZ: number, missingNeighborX: number, missingNeighborZ: number) {
|
|
92
|
+
const neighborKey = this.colKey(missingNeighborX, missingNeighborZ)
|
|
93
|
+
const columnKey = this.colKey(columnX, columnZ)
|
|
94
|
+
let set = this.awaiting.get(neighborKey)
|
|
95
|
+
if (!set) {
|
|
96
|
+
set = new Set()
|
|
97
|
+
this.awaiting.set(neighborKey, set)
|
|
98
|
+
}
|
|
99
|
+
set.add(columnKey)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
takeColumnsAwaitingNeighbor(neighborX: number, neighborZ: number): Array<{ x: number; z: number }> {
|
|
103
|
+
const key = this.colKey(neighborX, neighborZ)
|
|
104
|
+
const waiting = this.awaiting.get(key)
|
|
105
|
+
if (!waiting || waiting.size === 0) return []
|
|
106
|
+
this.awaiting.delete(key)
|
|
107
|
+
return [...waiting].map(k => {
|
|
108
|
+
const [sx, sz] = k.split(',').map(Number)
|
|
109
|
+
return { x: sx, z: sz }
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clearColumn(x: number, z: number) {
|
|
114
|
+
const columnKey = this.colKey(x, z)
|
|
115
|
+
for (const [neighborKey, set] of this.awaiting) {
|
|
116
|
+
set.delete(columnKey)
|
|
117
|
+
if (set.size === 0) this.awaiting.delete(neighborKey)
|
|
118
|
+
}
|
|
119
|
+
this.awaiting.delete(columnKey)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
clear() {
|
|
123
|
+
this.awaiting.clear()
|
|
124
|
+
}
|
|
125
|
+
}
|