minecraft-renderer 0.1.83 → 0.1.85

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.
@@ -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
@@ -44,6 +54,9 @@ function processUpdateLightV17(rawPacket: Uint8Array, numSections: number): void
44
54
  })
45
55
  invalidateConversion(x, z)
46
56
  dirtyColumnSectionsForLightUpdate(x, z)
57
+ const hadColumn = !!world?.getColumn(x, z)
58
+ syncV17LightToColumn(x, z)
59
+ if (!hadColumn) pendingLightDirtyColumns.add(rawCacheKey(x, z))
47
60
  } catch (err) {
48
61
  console.warn('[WASM Mesher] parseUpdateLightV17 failed:', err)
49
62
  }
@@ -248,6 +261,44 @@ interface UpdateLightV17Entry {
248
261
  blockLight: Uint8Array
249
262
  }
250
263
  const updateLightV17Cache = new Map<string, UpdateLightV17Entry>()
264
+ /** Columns that received `update_light` before the worker column existed. */
265
+ const pendingLightDirtyColumns = new Set<string>()
266
+ const _syncLightPos = new Vec3(0, 0, 0)
267
+
268
+ function resolveUpdateLightV17Entry(x: number, z: number, chunk: any | undefined, worldMinY: number, worldMaxY: number): UpdateLightV17Entry | undefined {
269
+ const cached = updateLightV17Cache.get(rawCacheKey(x, z))
270
+ if (cached?.blockLight?.length) return cached
271
+ if (!chunk) return cached
272
+ const { result } = getOrConvertColumn(x, z, chunk, version, worldMinY, worldMaxY, () => convertChunkToWasm(chunk, version, x, z, worldMinY, worldMaxY), chunk)
273
+ return { blockLight: result.blockLight, skyLight: result.skyLight }
274
+ }
275
+
276
+ /** Write cached 1.17 `update_light` arrays into the worker prismarine column. */
277
+ function syncV17LightToColumn(x: number, z: number): boolean {
278
+ if (!world) return false
279
+ const col = world.getColumn(x, z)
280
+ const entry = updateLightV17Cache.get(rawCacheKey(x, z))
281
+ if (!col || !entry) return false
282
+
283
+ const CHUNK_SIZE = 16
284
+ const minY = config?.worldMinY ?? 0
285
+ const maxY = config?.worldMaxY ?? 256
286
+ const { blockLight, skyLight } = entry
287
+
288
+ for (let y = minY; y < maxY; y++) {
289
+ for (let lz = 0; lz < CHUNK_SIZE; lz++) {
290
+ for (let lx = 0; lx < CHUNK_SIZE; lx++) {
291
+ const idx = lx + lz * CHUNK_SIZE + (y - minY) * CHUNK_SIZE * CHUNK_SIZE
292
+ if (idx >= blockLight.length) continue
293
+ _syncLightPos.set(lx, y, lz)
294
+ col.setBlockLight(_syncLightPos, blockLight[idx]!)
295
+ col.setSkyLight(_syncLightPos, skyLight[idx]!)
296
+ }
297
+ }
298
+ }
299
+
300
+ return true
301
+ }
251
302
 
252
303
  // 1.16 path: pre-extracted section bytes + bit mask. Bit mask in 1.16
253
304
  // is a varint that fits in i32 (only 16 sections), so we accept it as a
@@ -263,6 +314,77 @@ interface ParsedV16Entry {
263
314
  }
264
315
  const parsedV16Cache = new Map<string, ParsedV16Entry>()
265
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
+
266
388
  // 1.16 sky/block light cache — same shape as the v17 entry, populated by
267
389
  // `processUpdateLightV16` (which calls the shared `parseUpdateLightV17`
268
390
  // WASM export). Separate map for the same isolation reasons as
@@ -625,7 +747,7 @@ const meshMultiColumnsFromParsedV16V17 = (
625
747
  bitMapLoHi[i * 2] = entry.bitMapLoHi[0]
626
748
  bitMapLoHi[i * 2 + 1] = entry.bitMapLoHi[1]
627
749
  if (i === 0) maxBitsPerBlock = entry.maxBitsPerBlock
628
- const light = updateLightV17Cache.get(key)
750
+ const light = resolveUpdateLightV17Entry(chunksToUse[i].x, chunksToUse[i].z, chunksToUse[i].chunk, worldMinY, worldMaxY)
629
751
  skyLightList.push(light?.skyLight ?? new Uint8Array(0))
630
752
  blockLightList.push(light?.blockLight ?? new Uint8Array(0))
631
753
  } else {
@@ -707,6 +829,7 @@ const handleMessage = async (data: any) => {
707
829
  await initWasm()
708
830
  allDataReady = true
709
831
  workerIndex = data.workerIndex
832
+ drainPendingChunks()
710
833
  break
711
834
  }
712
835
  case 'dirty': {
@@ -718,39 +841,17 @@ const handleMessage = async (data: any) => {
718
841
  // Invalidate BEFORE replacing the column reference so a stale entry
719
842
  // can never outlive the old chunk object.
720
843
  invalidateConversion(data.x, data.z)
721
- if (!world) break
722
- world.addColumn(data.x, data.z, data.chunk)
723
- if (data.customBlockModels) {
724
- const chunkKey = `${data.x},${data.z}`
725
- world.customBlockModels.set(chunkKey, data.customBlockModels)
726
- }
727
- // Safety-net heightmap push for fully empty columns. With WASM
728
- // mesher as the sole path, the main thread no longer requests
729
- // `getHeightmap` on chunk load — heightmaps come from
730
- // `processColumnTick`. But a fully empty column (no sections, or
731
- // all sections missing) never enters that path because
732
- // `setSectionDirty` short-circuits when `chunk.getSection(pos)` is
733
- // falsy, so `processColumnTick` never sees it. Without this push
734
- // downstream consumers (e.g. `rain.ts`) would have no heightmap
735
- // entry for such columns. We send a cheap sentinel-filled
736
- // `Int16Array(256).fill(-32768)` — no JS heightmap scan — only when
737
- // we detect zero sections; non-empty columns get their real
738
- // heightmap from the next `processColumnTick`.
739
- const sectionH = SECTION_HEIGHT
740
- const minY = config?.worldMinY ?? 0
741
- const maxY = config?.worldMaxY ?? 256
742
- const column = world.getColumn(data.x, data.z)
743
- let hasAnySection = false
744
- for (let y = minY; y < maxY; y += sectionH) {
745
- if (column?.getSection?.(new Vec3(0, y, 0))) {
746
- hasAnySection = true
747
- break
748
- }
749
- }
750
- if (!hasAnySection) {
751
- const emptyHeightmap = new Int16Array(256).fill(EMPTY_COLUMN_HEIGHTMAP_SENTINEL)
752
- 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
753
852
  }
853
+ processChunkMessage(data)
854
+ onColumnDataArrived(data.x, data.z)
754
855
  break
755
856
  }
756
857
  case 'unloadChunk': {
@@ -760,9 +861,11 @@ const handleMessage = async (data: any) => {
760
861
  updateLightV17Cache.delete(rawCacheKey(data.x, data.z))
761
862
  parsedV16Cache.delete(rawCacheKey(data.x, data.z))
762
863
  updateLightV16Cache.delete(rawCacheKey(data.x, data.z))
864
+ pendingLightDirtyColumns.delete(rawCacheKey(data.x, data.z))
763
865
  if (!world) break
764
866
  world.removeColumn(data.x, data.z)
765
867
  world.customBlockModels.delete(`${data.x},${data.z}`)
868
+ pendingNeighborHeal.clearColumn(data.x, data.z)
766
869
  requestTracker.clearColumn(data.x, data.z)
767
870
  for (const key of [...dirtySections.keys()]) {
768
871
  const [sx, , sz] = key.split(',').map(Number)
@@ -809,6 +912,7 @@ const handleMessage = async (data: any) => {
809
912
  numSections: data.numSections as number
810
913
  })
811
914
  invalidateConversion(data.x, data.z)
915
+ onColumnDataArrived(data.x, data.z)
812
916
  break
813
917
  }
814
918
  case 'setParsedMapChunkV17': {
@@ -822,6 +926,7 @@ const handleMessage = async (data: any) => {
822
926
  biomes: data.biomes as Int32Array | undefined
823
927
  })
824
928
  invalidateConversion(data.x, data.z)
929
+ onColumnDataArrived(data.x, data.z)
825
930
  break
826
931
  }
827
932
  case 'setUpdateLightV17': {
@@ -846,6 +951,7 @@ const handleMessage = async (data: any) => {
846
951
  biomes: data.biomes as Int32Array
847
952
  })
848
953
  invalidateConversion(data.x, data.z)
954
+ onColumnDataArrived(data.x, data.z)
849
955
  break
850
956
  }
851
957
  case 'setUpdateLightV16': {
@@ -863,8 +969,12 @@ const handleMessage = async (data: any) => {
863
969
  rawMapChunkCache.clear()
864
970
  parsedV17Cache.clear()
865
971
  updateLightV17Cache.clear()
972
+ pendingLightDirtyColumns.clear()
866
973
  parsedV16Cache.clear()
867
974
  updateLightV16Cache.clear()
975
+ pendingChunks.clear()
976
+ pendingNeighborHeal.clear()
977
+ blindMeshWarnCount = 0
868
978
  globalVar.mcData = null
869
979
  globalVar.loadedData = null
870
980
  allDataReady = false
@@ -936,23 +1046,8 @@ self.onmessage = ({ data }) => {
936
1046
  // Section height is always 16 in column mode (the only WASM path).
937
1047
  const getSectionHeight = () => SECTION_HEIGHT
938
1048
 
939
- // 3x3 X/Z neighbor set for column meshing. Y-agnostic because full-column
940
- // meshing converts the entire world Y range in one go.
941
1049
  function collectChunksForColumn(x: number, z: number) {
942
- const result = [] as Array<{ x: number; z: number; chunk: any }>
943
- const target = world.getColumn(x, z)
944
- if (target) result.push({ x, z, chunk: target })
945
- const offsets = [-16, 0, 16]
946
- for (const dx of offsets) {
947
- for (const dz of offsets) {
948
- if (dx === 0 && dz === 0) continue
949
- const nx = x + dx
950
- const nz = z + dz
951
- const c = world.getColumn(nx, nz)
952
- if (c) result.push({ x: nx, z: nz, chunk: c })
953
- }
954
- }
955
- return result
1050
+ return collectChunksForColumnUnion(x, z, (nx, nz) => world.getColumn(nx, nz), packetCaches())
956
1051
  }
957
1052
 
958
1053
  function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeight: number, hadErrors: boolean): MesherGeometryOutput {
@@ -1031,6 +1126,9 @@ function processColumnTick() {
1031
1126
  let preCacheMisses = 0
1032
1127
  let hadError = false
1033
1128
  let columnMeshPath = 'none'
1129
+ let chunkCount = 0
1130
+ let worldColumns3x3 = 0
1131
+ let parsedCache3x3 = 0
1034
1132
  // Outer-scope timestamps so we can finalize `processTime` and
1035
1133
  // `postPhase` AFTER the per-section emit loop runs (the loop builds
1036
1134
  // typed arrays, walks block-entity metadata, and calls postMessage —
@@ -1045,7 +1143,21 @@ function processColumnTick() {
1045
1143
  const t0 = start
1046
1144
  try {
1047
1145
  const chunksToUse = collectChunksForColumn(x, z)
1048
- const chunkCount = chunksToUse.length
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)
1049
1161
 
1050
1162
  let wasmResult: any
1051
1163
  let t1 = 0
@@ -1068,7 +1180,7 @@ function processColumnTick() {
1068
1180
  wasmResult = meshColumnFromRawV18Plus(rawEntry, x, z, worldMinY, worldMaxY, meta)
1069
1181
  if (wasmResult) columnMeshPath = 'v18_fused'
1070
1182
  } else if (v17Entry) {
1071
- const v17Light = updateLightV17Cache.get(rawCacheKey(x, z))
1183
+ const v17Light = resolveUpdateLightV17Entry(x, z, targetChunk, worldMinY, worldMaxY)
1072
1184
  wasmResult = meshColumnFromParsedV16V17(
1073
1185
  v17Entry.chunkData,
1074
1186
  v17Entry.bitMapLoHi,
@@ -1135,11 +1247,13 @@ function processColumnTick() {
1135
1247
 
1136
1248
  if (!wasmResult) {
1137
1249
  // --- Old two-step path (multi-column or fused fallback) ---
1138
- const conversions = chunksToUse.map(({ x: cx, z: cz, chunk }) => {
1250
+ const conversions: ChunkConversionResult[] = []
1251
+ const meshChunks: typeof chunksToUse = []
1252
+ for (const { x: cx, z: cz, chunk } of chunksToUse) {
1139
1253
  const cs = performance.now()
1140
1254
  const rawEntry = rawMapChunkCache.get(rawCacheKey(cx, cz))
1141
1255
  const v17Entry = parsedV17Cache.get(rawCacheKey(cx, cz))
1142
- const v17Light = updateLightV17Cache.get(rawCacheKey(cx, cz))
1256
+ const v17Light = resolveUpdateLightV17Entry(cx, cz, chunk, worldMinY, worldMaxY)
1143
1257
  const v16Entry = parsedV16Cache.get(rawCacheKey(cx, cz))
1144
1258
  const v16Light = updateLightV16Cache.get(rawCacheKey(cx, cz))
1145
1259
 
@@ -1163,6 +1277,9 @@ function processColumnTick() {
1163
1277
  if (!conv) {
1164
1278
  // JS-fallback (column walk) — still cached, since this is the
1165
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
1166
1283
  const cached = getOrConvertColumn(
1167
1284
  cx,
1168
1285
  cz,
@@ -1186,12 +1303,18 @@ function processColumnTick() {
1186
1303
  preNeighborConvert += ce - cs
1187
1304
  preNeighborCount++
1188
1305
  }
1189
- return conv
1190
- })
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
+ }
1191
1314
 
1192
1315
  const { invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks } = conversions[0]
1193
1316
 
1194
- if (chunkCount === 1 || !(wasm as any).generate_geometry_multi) {
1317
+ if (twoStepChunkCount === 1 || !(wasm as any).generate_geometry_multi) {
1195
1318
  const { blockStates, blockLight, skyLight, biomesArray } = conversions[0]
1196
1319
  t1 = performance.now()
1197
1320
  wasmResult = wasm.generate_geometry(
@@ -1219,17 +1342,17 @@ function processColumnTick() {
1219
1342
  } else {
1220
1343
  const tBuildStart = performance.now()
1221
1344
  const perChunkLen = conversions[0].blockStates.length
1222
- const xs = new Int32Array(chunkCount)
1223
- const zs = new Int32Array(chunkCount)
1224
- const blockStatesAll = new Uint16Array(perChunkLen * chunkCount)
1225
- const blockLightAll = new Uint8Array(perChunkLen * chunkCount)
1226
- const skyLightAll = new Uint8Array(perChunkLen * chunkCount)
1227
- const biomesAll = new Uint8Array(perChunkLen * chunkCount)
1228
-
1229
- for (let i = 0; i < chunkCount; 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++) {
1230
1353
  const c = conversions[i]
1231
- xs[i] = chunksToUse[i].x
1232
- zs[i] = chunksToUse[i].z
1354
+ xs[i] = meshChunks[i].x
1355
+ zs[i] = meshChunks[i].z
1233
1356
  blockStatesAll.set(c.blockStates, perChunkLen * i)
1234
1357
  blockLightAll.set(c.blockLight, perChunkLen * i)
1235
1358
  skyLightAll.set(c.skyLight, perChunkLen * i)
@@ -1497,7 +1620,11 @@ function processColumnTick() {
1497
1620
  preTypedArrayBuild: !attributed ? preTypedArrayBuild : 0,
1498
1621
  preOther: !attributed ? preOther : 0,
1499
1622
  preCacheHits: !attributed ? preCacheHits : 0,
1500
- 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'
1501
1628
  })
1502
1629
  attributed = true
1503
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
+ }