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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -676,6 +676,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
676
676
  const heightmap = new Int16Array(data.heightmap)
677
677
  this.reactiveState.world.heightmaps[data.key] = heightmap
678
678
  }
679
+
680
+ if (data.type === 'neighborDataArrived') {
681
+ const sectionHeight = this.getSectionHeight()
682
+ for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
683
+ this.setSectionDirty(new Vec3(data.x, y, data.z))
684
+ }
685
+ }
679
686
  }
680
687
 
681
688
  downloadMesherLog() {
@@ -105,7 +105,12 @@ export interface MesherMainEvents {
105
105
  // Per-event counts for the column-mode conversion cache.
106
106
  preCacheHits?: number
107
107
  preCacheMisses?: number
108
+ chunkCount?: number
109
+ worldColumns3x3?: number
110
+ parsedCache3x3?: number
111
+ columnMeshPath?: string
108
112
  }
113
+ neighborDataArrived: { type: 'neighborDataArrived'; x: number; z: number; workerIndex: number }
109
114
  blockStateModelInfo: { type: 'blockStateModelInfo'; info: Record<string, BlockStateModelInfo> }
110
115
  heightmap: { type: 'heightmap'; key: string; heightmap: Int16Array }
111
116
  /** Reply to `{ type: 'mc-web-ping', t?, workerIndex? }` from the main thread (not batched in worker). */
@@ -9,7 +9,7 @@ const threeVersion = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10)
9
9
  class StarfieldMaterial extends THREE.ShaderMaterial {
10
10
  constructor() {
11
11
  super({
12
- uniforms: { time: { value: 0 }, fade: { value: 1 } },
12
+ uniforms: { time: { value: 0 } },
13
13
  vertexShader: /* glsl */ `
14
14
  uniform float time;
15
15
  attribute float size;
@@ -23,13 +23,9 @@ class StarfieldMaterial extends THREE.ShaderMaterial {
23
23
  }`,
24
24
  fragmentShader: /* glsl */ `
25
25
  uniform sampler2D pointTexture;
26
- uniform float fade;
27
26
  varying vec3 vColor;
28
27
  void main() {
29
- // fade scales star brightness (0 = invisible). With additive blending this is
30
- // the only way to dim stars — scene fog never reaches this shader. Driven by the
31
- // rain state so stars disappear in rain like in vanilla.
32
- gl_FragColor = vec4(vColor * fade, 1.0);
28
+ gl_FragColor = vec4(vColor, 1.0);
33
29
 
34
30
  #include <tonemapping_fragment>
35
31
  #include <${threeVersion >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
@@ -43,8 +39,6 @@ export class StarfieldModule implements RendererModuleController {
43
39
  private timer = new THREE.Timer()
44
40
  private enabled = false
45
41
  private currentTime?: number
46
- /** Current star brightness multiplier; lerps toward 0 while raining, 1 otherwise. */
47
- private fade = 1
48
42
 
49
43
  constructor(private readonly worldRenderer: WorldRendererThree) {}
50
44
 
@@ -76,20 +70,13 @@ export class StarfieldModule implements RendererModuleController {
76
70
  return this.currentTime > nightTime && this.currentTime < morningStart
77
71
  }
78
72
 
79
- render?: (deltaTime: number) => void = deltaTime => {
73
+ render?: (deltaTime: number) => void = () => {
80
74
  if (!this.points) return
81
75
  this.points.position.set(0, 0, 0)
82
76
 
83
77
  const material = this.points.material as StarfieldMaterial
84
78
  this.timer.update(performance.now())
85
79
  material.uniforms.time.value = this.timer.getElapsed() * 0.2
86
-
87
- // Fade stars out while raining (vanilla scales star brightness by 1 - rainLevel).
88
- // isRaining is a boolean here, so ease toward the target instead of snapping.
89
- const target = this.worldRenderer.worldRendererConfig.isRaining ? 0 : 1
90
- const t = Math.min(1, deltaTime * 2)
91
- this.fade += (target - this.fade) * t
92
- material.uniforms.fade.value = this.fade
93
80
  }
94
81
 
95
82
  /**
@@ -559,13 +559,21 @@ export function renderWasmOutputToGeometry(
559
559
  const uvs: number[] = []
560
560
  const indices: number[] = []
561
561
 
562
- const blendPositions: number[] = []
563
- const blendNormals: number[] = []
564
- const blendColors: number[] = []
565
- const blendSkyLights: number[] = []
566
- const blendBlockLights: number[] = []
567
- const blendUvs: number[] = []
568
- const blendIndices: number[] = []
562
+ const blockBlendPositions: number[] = []
563
+ const blockBlendNormals: number[] = []
564
+ const blockBlendColors: number[] = []
565
+ const blockBlendSkyLights: number[] = []
566
+ const blockBlendBlockLights: number[] = []
567
+ const blockBlendUvs: number[] = []
568
+ const blockBlendIndices: number[] = []
569
+
570
+ const liquidPositions: number[] = []
571
+ const liquidNormals: number[] = []
572
+ const liquidColors: number[] = []
573
+ const liquidSkyLights: number[] = []
574
+ const liquidBlockLights: number[] = []
575
+ const liquidUvs: number[] = []
576
+ const liquidIndices: number[] = []
569
577
 
570
578
  const liquidQueue: Array<{
571
579
  pos: Vec3
@@ -691,13 +699,13 @@ export function renderWasmOutputToGeometry(
691
699
  if (!models || models.length == 0) continue
692
700
 
693
701
  const routeToBlend = prismBlock.transparent && isSemiTransparentBlockName(cachedModel.blockName)
694
- const tgtPos = routeToBlend ? blendPositions : positions
695
- const tgtNorm = routeToBlend ? blendNormals : normals
696
- const tgtCol = routeToBlend ? blendColors : colors
697
- const tgtSky = routeToBlend ? blendSkyLights : skyLights
698
- const tgtBlock = routeToBlend ? blendBlockLights : blockLights
699
- const tgtUv = routeToBlend ? blendUvs : uvs
700
- const tgtIdx = routeToBlend ? blendIndices : indices
702
+ const tgtPos = routeToBlend ? blockBlendPositions : positions
703
+ const tgtNorm = routeToBlend ? blockBlendNormals : normals
704
+ const tgtCol = routeToBlend ? blockBlendColors : colors
705
+ const tgtSky = routeToBlend ? blockBlendSkyLights : skyLights
706
+ const tgtBlock = routeToBlend ? blockBlendBlockLights : blockLights
707
+ const tgtUv = routeToBlend ? blockBlendUvs : uvs
708
+ const tgtIdx = routeToBlend ? blockBlendIndices : indices
701
709
 
702
710
  const faceNameToIndex: Record<string, number> = {
703
711
  up: 0,
@@ -947,17 +955,39 @@ export function renderWasmOutputToGeometry(
947
955
  q.biome,
948
956
  q.water,
949
957
  q.isRealWater,
950
- blendPositions,
951
- blendNormals,
952
- blendColors,
953
- blendSkyLights,
954
- blendBlockLights,
955
- blendUvs,
956
- blendIndices
958
+ liquidPositions,
959
+ liquidNormals,
960
+ liquidColors,
961
+ liquidSkyLights,
962
+ liquidBlockLights,
963
+ liquidUvs,
964
+ liquidIndices
957
965
  )
958
966
  }
959
967
  }
960
968
 
969
+ let blendPositions: number[] = []
970
+ let blendNormals: number[] = []
971
+ let blendColors: number[] = []
972
+ let blendSkyLights: number[] = []
973
+ let blendBlockLights: number[] = []
974
+ let blendUvs: number[] = []
975
+ const blendIndices: number[] = []
976
+
977
+ if (liquidPositions.length > 0 || blockBlendPositions.length > 0) {
978
+ const liquidVertexCount = liquidPositions.length / 3
979
+ blendPositions = liquidPositions.concat(blockBlendPositions)
980
+ blendNormals = liquidNormals.concat(blockBlendNormals)
981
+ blendColors = liquidColors.concat(blockBlendColors)
982
+ blendSkyLights = liquidSkyLights.concat(blockBlendSkyLights)
983
+ blendBlockLights = liquidBlockLights.concat(blockBlendBlockLights)
984
+ blendUvs = liquidUvs.concat(blockBlendUvs)
985
+ blendIndices.push(...liquidIndices)
986
+ for (const idx of blockBlendIndices) {
987
+ blendIndices.push(idx + liquidVertexCount)
988
+ }
989
+ }
990
+
961
991
  const shaderCubes = buildShaderCubesFromWords(shaderWordBuffer)
962
992
 
963
993
  const result: ExportedSection = {
@@ -0,0 +1,123 @@
1
+ //@ts-nocheck
2
+ import { beforeAll, beforeEach, expect, test } from 'vitest'
3
+ import { readFileSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { dirname, join } from 'node:path'
6
+ import Chunks from 'prismarine-chunk'
7
+ import MinecraftData from 'minecraft-data'
8
+ import { Vec3 } from 'vec3'
9
+ import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
10
+ import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
11
+ import { World } from '../../mesher-shared/world'
12
+ import { setBlockStatesData } from '../../mesher-shared/models'
13
+ import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
14
+ import { convertChunkToWasm, getBlockMeta } from '../bridge/convertChunk'
15
+ import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
16
+
17
+ const VERSION = '1.17.1'
18
+ const SECTION_Y = 0
19
+ const ICE_Y = 2
20
+ const WATER_Y = 1
21
+ const WORLD_MIN_Y = 0
22
+ const WORLD_MAX_Y = 256
23
+ const COLUMN_HEIGHT = WORLD_MAX_Y - WORLD_MIN_Y
24
+
25
+ let wasmModule: typeof import('../runtime-build/wasm_mesher.js')
26
+
27
+ beforeAll(async () => {
28
+ wasmModule = await import('../runtime-build/wasm_mesher.js')
29
+ const wasmDir = dirname(fileURLToPath(import.meta.url))
30
+ const wasmBytes = readFileSync(join(wasmDir, '../runtime-build/wasm_mesher_bg.wasm'))
31
+ wasmModule.initSync(wasmBytes)
32
+ })
33
+
34
+ beforeEach(() => {
35
+ resetFaceOcclusionCache()
36
+ const mcData = MinecraftData(VERSION)
37
+ setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
38
+ ;(globalThis as any).__wasmBlockModelCache = new Map()
39
+ })
40
+
41
+ function resolveStateId(name: string) {
42
+ const mcData = MinecraftData(VERSION)
43
+ const block = mcData.blocksByName[name]
44
+ if (!block) throw new Error(`Unknown block: ${name}`)
45
+ return block.defaultState
46
+ }
47
+
48
+ function buildIceOverWaterWorld(): World {
49
+ const Chunk = Chunks(VERSION) as any
50
+ const chunk = new Chunk(undefined as any)
51
+ const iceId = resolveStateId('ice')
52
+ const waterId = resolveStateId('water')
53
+
54
+ for (let z = 0; z < 16; z++) {
55
+ for (let x = 0; x < 16; x++) {
56
+ chunk.setBlockStateId(new Vec3(x, WATER_Y, z), waterId)
57
+ chunk.setBlockStateId(new Vec3(x, ICE_Y, z), iceId)
58
+ chunk.setBlockLight(new Vec3(x, ICE_Y, z), 15)
59
+ chunk.setSkyLight(new Vec3(x, ICE_Y, z), 15)
60
+ chunk.setBlockLight(new Vec3(x, WATER_Y, z), 15)
61
+ chunk.setSkyLight(new Vec3(x, WATER_Y, z), 15)
62
+ }
63
+ }
64
+
65
+ const world = new World(VERSION)
66
+ world.addColumn(0, 0, chunk.toJson())
67
+ return world
68
+ }
69
+
70
+ function findLiquidBeforeBlockIndexSplit(indices: number[]) {
71
+ for (let split = 1; split < indices.length; split++) {
72
+ const prefix = indices.slice(0, split)
73
+ const suffix = indices.slice(split)
74
+ const maxPrefix = Math.max(...prefix)
75
+ const minSuffix = Math.min(...suffix)
76
+ if (maxPrefix < minSuffix) {
77
+ return { split, liquidVertexCount: minSuffix }
78
+ }
79
+ }
80
+ return null
81
+ }
82
+
83
+ test('blend emission order: liquid quads precede ice block quads in the index buffer', () => {
84
+ const world = buildIceOverWaterWorld()
85
+ const meta = getBlockMeta(VERSION)
86
+ const column = world.getColumn(0, 0)!
87
+ const conversion = convertChunkToWasm(column, VERSION, 0, 0, WORLD_MIN_Y, WORLD_MAX_Y)
88
+ const wasmResult = wasmModule.generate_geometry(
89
+ 0,
90
+ WORLD_MIN_Y,
91
+ 0,
92
+ COLUMN_HEIGHT,
93
+ WORLD_MIN_Y,
94
+ WORLD_MAX_Y,
95
+ WORLD_MIN_Y,
96
+ conversion.blockStates,
97
+ conversion.blockLight,
98
+ conversion.skyLight,
99
+ conversion.biomesArray,
100
+ meta.invisibleBlocks,
101
+ meta.transparentBlocks,
102
+ meta.noAoBlocks,
103
+ meta.cullIdenticalBlocks,
104
+ meta.occludingBlocks,
105
+ true,
106
+ false,
107
+ 15
108
+ )
109
+
110
+ const section = renderWasmOutputToGeometry(wasmResult, VERSION, `0,${SECTION_Y},0`, { x: 8, y: 8, z: 8 }, world, {
111
+ sectionHeight: 16,
112
+ shaderCubes: false
113
+ })
114
+
115
+ const blend = section.blendGeometry
116
+ expect(blend).toBeDefined()
117
+ expect(blend!.indices.length).toBeGreaterThan(0)
118
+
119
+ const split = findLiquidBeforeBlockIndexSplit(blend!.indices)
120
+ expect(split).not.toBeNull()
121
+ expect(split!.split).toBeGreaterThan(0)
122
+ expect(split!.liquidVertexCount).toBeGreaterThan(0)
123
+ })
@@ -0,0 +1,205 @@
1
+ //@ts-nocheck
2
+ import { beforeAll, beforeEach, describe, expect, test } from 'vitest'
3
+ import { readFileSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { dirname, join } from 'node:path'
6
+ import Chunks from 'prismarine-chunk'
7
+ import MinecraftData from 'minecraft-data'
8
+ import { Vec3 } from 'vec3'
9
+ import PrismarineBlockLoader from 'prismarine-block'
10
+ import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
11
+ import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
12
+ import { setBlockStatesData } from '../../mesher-shared/models'
13
+ import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
14
+ import { convertChunkToWasm, getBlockMeta } from '../bridge/convertChunk'
15
+ import type { WasmGeometryOutput } from '../bridge/render-from-wasm'
16
+
17
+ const VERSION = '1.17.1'
18
+ const ICE_Y = 62
19
+ const WATER_Y = 61
20
+ const WORLD_MIN_Y = 0
21
+ // Full overworld height (256) makes multi-column WASM meshing exceed CI's 5s
22
+ // default timeout. Culling only needs the section band that contains ice/water.
23
+ const WORLD_MAX_Y = 80
24
+ const COLUMN_HEIGHT = WORLD_MAX_Y - WORLD_MIN_Y
25
+
26
+ const EAST_FACE = 1 << 2
27
+ const WEST_FACE = 1 << 3
28
+
29
+ let wasmModule: typeof import('../runtime-build/wasm_mesher.js')
30
+
31
+ beforeAll(async () => {
32
+ wasmModule = await import('../runtime-build/wasm_mesher.js')
33
+ const wasmDir = dirname(fileURLToPath(import.meta.url))
34
+ const wasmBytes = readFileSync(join(wasmDir, '../runtime-build/wasm_mesher_bg.wasm'))
35
+ wasmModule.initSync(wasmBytes)
36
+ })
37
+
38
+ beforeEach(() => {
39
+ resetFaceOcclusionCache()
40
+ const mcData = MinecraftData(VERSION)
41
+ setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
42
+ ;(globalThis as any).__wasmBlockModelCache = new Map()
43
+ })
44
+
45
+ function resolveStateId(name: string) {
46
+ const mcData = MinecraftData(VERSION)
47
+ const block = mcData.blocksByName[name]
48
+ if (!block) throw new Error(`Unknown block: ${name}`)
49
+ return block.defaultState
50
+ }
51
+
52
+ function buildIceOverWaterColumn() {
53
+ const Chunk = Chunks(VERSION) as any
54
+ const chunk = new Chunk(undefined as any)
55
+ const iceId = resolveStateId('ice')
56
+ const waterId = resolveStateId('water')
57
+
58
+ for (let z = 0; z < 16; z++) {
59
+ for (let x = 0; x < 16; x++) {
60
+ chunk.setBlockStateId(new Vec3(x, WATER_Y, z), waterId)
61
+ chunk.setBlockStateId(new Vec3(x, ICE_Y, z), iceId)
62
+ chunk.setBlockLight(new Vec3(x, ICE_Y, z), 15)
63
+ chunk.setSkyLight(new Vec3(x, ICE_Y, z), 15)
64
+ }
65
+ }
66
+ return chunk.toJson()
67
+ }
68
+
69
+ function meshColumns(columnOrigins: Array<{ x: number; z: number }>, targetX: number, targetZ: number, options?: { forceMulti?: boolean }): WasmGeometryOutput {
70
+ const meta = getBlockMeta(VERSION)
71
+ const conversions = columnOrigins.map(({ x, z }) => {
72
+ const chunkJson = buildIceOverWaterColumn()
73
+ return convertChunkToWasm(chunkJson, VERSION, x, z, WORLD_MIN_Y, WORLD_MAX_Y)
74
+ })
75
+
76
+ if (conversions.length === 1 && !options?.forceMulti) {
77
+ const c = conversions[0]
78
+ return wasmModule.generate_geometry(
79
+ targetX,
80
+ WORLD_MIN_Y,
81
+ targetZ,
82
+ COLUMN_HEIGHT,
83
+ WORLD_MIN_Y,
84
+ WORLD_MAX_Y,
85
+ WORLD_MIN_Y,
86
+ c.blockStates,
87
+ c.blockLight,
88
+ c.skyLight,
89
+ c.biomesArray,
90
+ meta.invisibleBlocks,
91
+ meta.transparentBlocks,
92
+ meta.noAoBlocks,
93
+ meta.cullIdenticalBlocks,
94
+ meta.occludingBlocks,
95
+ true,
96
+ false,
97
+ 15
98
+ ) as WasmGeometryOutput
99
+ }
100
+
101
+ const perChunkLen = conversions[0].blockStates.length
102
+ const xs = new Int32Array(columnOrigins.length)
103
+ const zs = new Int32Array(columnOrigins.length)
104
+ const blockStatesAll = new Uint16Array(perChunkLen * columnOrigins.length)
105
+ const blockLightAll = new Uint8Array(perChunkLen * columnOrigins.length)
106
+ const skyLightAll = new Uint8Array(perChunkLen * columnOrigins.length)
107
+ const biomesAll = new Uint8Array(perChunkLen * columnOrigins.length)
108
+
109
+ for (let i = 0; i < columnOrigins.length; i++) {
110
+ const c = conversions[i]
111
+ xs[i] = columnOrigins[i].x
112
+ zs[i] = columnOrigins[i].z
113
+ blockStatesAll.set(c.blockStates, perChunkLen * i)
114
+ blockLightAll.set(c.blockLight, perChunkLen * i)
115
+ skyLightAll.set(c.skyLight, perChunkLen * i)
116
+ biomesAll.set(c.biomesArray, perChunkLen * i)
117
+ }
118
+
119
+ const first = conversions[0]
120
+ return (wasmModule as any).generate_geometry_multi(
121
+ targetX,
122
+ WORLD_MIN_Y,
123
+ targetZ,
124
+ COLUMN_HEIGHT,
125
+ WORLD_MIN_Y,
126
+ WORLD_MAX_Y,
127
+ WORLD_MIN_Y,
128
+ xs,
129
+ zs,
130
+ blockStatesAll,
131
+ blockLightAll,
132
+ skyLightAll,
133
+ biomesAll,
134
+ first.invisibleBlocks,
135
+ first.transparentBlocks,
136
+ first.noAoBlocks,
137
+ first.cullIdenticalBlocks,
138
+ first.occludingBlocks,
139
+ true,
140
+ false,
141
+ 15
142
+ ) as WasmGeometryOutput
143
+ }
144
+
145
+ function countIceBorderFaces(output: WasmGeometryOutput, iceStateId: number, planeX: number, faceBit: number) {
146
+ let count = 0
147
+ for (const block of output.blocks) {
148
+ if (block.block_state_id !== iceStateId) continue
149
+ const [bx, by] = block.position
150
+ if (bx !== planeX || by !== ICE_Y) continue
151
+ if ((block.visible_faces & faceBit) !== 0) count++
152
+ }
153
+ return count
154
+ }
155
+
156
+ describe('cross-column ice culling', { timeout: 15_000 }, () => {
157
+ test('two adjacent columns produce zero border faces', () => {
158
+ const iceStateId = resolveStateId('ice')
159
+ const output = meshColumns(
160
+ [
161
+ { x: 0, z: 0 },
162
+ { x: 16, z: 0 }
163
+ ],
164
+ 0,
165
+ 0
166
+ )
167
+
168
+ expect(countIceBorderFaces(output, iceStateId, 15, EAST_FACE)).toBe(0)
169
+ expect(countIceBorderFaces(output, iceStateId, 16, WEST_FACE)).toBe(0)
170
+ })
171
+
172
+ test('negative coordinates', () => {
173
+ const iceStateId = resolveStateId('ice')
174
+ const ax = -504928
175
+ const bx = ax + 16
176
+ const z = 496768
177
+ const output = meshColumns(
178
+ [
179
+ { x: ax, z },
180
+ { x: bx, z }
181
+ ],
182
+ ax,
183
+ z
184
+ )
185
+
186
+ expect(countIceBorderFaces(output, iceStateId, ax + 15, EAST_FACE)).toBe(0)
187
+ expect(countIceBorderFaces(output, iceStateId, bx, WEST_FACE)).toBe(0)
188
+ })
189
+
190
+ test('healed mesh has fewer blocks at the shared boundary plane', () => {
191
+ const alone = meshColumns([{ x: 0, z: 0 }], 0, 0, { forceMulti: true })
192
+ const pair = meshColumns(
193
+ [
194
+ { x: 0, z: 0 },
195
+ { x: 16, z: 0 }
196
+ ],
197
+ 0,
198
+ 0
199
+ )
200
+ const aloneBoundaryBlocks = alone.blocks.filter(b => b.position[0] === 15 && b.position[1] === ICE_Y).length
201
+ const pairBoundaryBlocks = pair.blocks.filter(b => b.position[0] === 15 && b.position[1] === ICE_Y).length
202
+ expect(aloneBoundaryBlocks).toBeGreaterThanOrEqual(pairBoundaryBlocks)
203
+ expect(pair.blocks.filter(b => b.position[0] === 16 && b.position[1] === ICE_Y && (b.visible_faces & WEST_FACE) !== 0).length).toBe(0)
204
+ })
205
+ })
@@ -0,0 +1,24 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, test } from 'vitest'
3
+ import { PendingChunkBuffer } from '../worker/mesherWasmChunkBuffer'
4
+
5
+ describe('PendingChunkBuffer', () => {
6
+ test('buffers chunk messages until drained', () => {
7
+ const buffer = new PendingChunkBuffer()
8
+ buffer.enqueue({ x: 0, z: 0, chunk: { sections: [] } })
9
+ buffer.enqueue({ x: 16, z: 0, chunk: { sections: [] } })
10
+ expect(buffer.size).toBe(2)
11
+
12
+ const applied: number[] = []
13
+ buffer.drain(msg => applied.push(msg.x))
14
+ expect(applied).toEqual([0, 16])
15
+ expect(buffer.size).toBe(0)
16
+ })
17
+
18
+ test('clear drops pending messages', () => {
19
+ const buffer = new PendingChunkBuffer()
20
+ buffer.enqueue({ x: 0, z: 0, chunk: {} })
21
+ buffer.clear()
22
+ expect(buffer.size).toBe(0)
23
+ })
24
+ })
@@ -0,0 +1,91 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, test } from 'vitest'
3
+ import {
4
+ PendingNeighborHealTracker,
5
+ collectChunksForColumnUnion,
6
+ columnDataAvailable,
7
+ countParsedCache3x3,
8
+ countWorldColumns3x3
9
+ } from '../worker/mesherWasmNeighborhood'
10
+
11
+ describe('collectChunksForColumnUnion', () => {
12
+ test('includes cache-only side neighbors not yet in world.columns', () => {
13
+ const worldCols = new Map<string, any>([['0,0', { id: 'A' }]])
14
+ const packet = new Set(['16,0'])
15
+
16
+ const result = collectChunksForColumnUnion(0, 0, (x, z) => worldCols.get(`${x},${z}`) ?? null, { raw: packet, v17: new Set(), v16: new Set() })
17
+
18
+ expect(result).toHaveLength(2)
19
+ expect(result[0]).toEqual({ x: 0, z: 0, chunk: { id: 'A' } })
20
+ expect(result[1]).toEqual({ x: 16, z: 0, chunk: null })
21
+ })
22
+
23
+ test('returns full 3x3 when both pipelines are complete', () => {
24
+ const worldCols = new Map<string, any>()
25
+ const packet = new Set<string>()
26
+ for (const dx of [-16, 0, 16]) {
27
+ for (const dz of [-16, 0, 16]) {
28
+ worldCols.set(`${dx},${dz}`, {})
29
+ packet.add(`${dx},${dz}`)
30
+ }
31
+ }
32
+
33
+ const result = collectChunksForColumnUnion(0, 0, (x, z) => worldCols.get(`${x},${z}`) ?? null, {
34
+ raw: packet,
35
+ v17: new Set(),
36
+ v16: new Set()
37
+ })
38
+
39
+ expect(result).toHaveLength(9)
40
+ })
41
+ })
42
+
43
+ describe('column availability counters', () => {
44
+ test('detects pipeline A lag behind packet caches', () => {
45
+ const worldCols = new Map<string, any>([
46
+ ['0,0', {}],
47
+ ['16,0', {}],
48
+ ['-16,0', {}],
49
+ ['0,16', {}]
50
+ ])
51
+ const packet = new Set(['0,0', '16,0', '-16,0', '0,16', '0,-16', '16,16', '-16,16', '-16,-16', '16,-16'])
52
+ const caches = { raw: packet, v17: new Set(), v16: new Set() }
53
+
54
+ expect(countWorldColumns3x3(0, 0, (x, z) => worldCols.get(`${x},${z}`) ?? null)).toBe(4)
55
+ expect(countParsedCache3x3(0, 0, caches)).toBe(9)
56
+ expect(columnDataAvailable(0, -16, (x, z) => worldCols.get(`${x},${z}`) ?? null, caches)).toBe(true)
57
+ expect(columnDataAvailable(16, 16, (x, z) => worldCols.get(`${x},${z}`) ?? null, caches)).toBe(true)
58
+ })
59
+ })
60
+
61
+ describe('PendingNeighborHealTracker', () => {
62
+ test('queues and releases columns awaiting a missing neighbor', () => {
63
+ const tracker = new PendingNeighborHealTracker()
64
+ tracker.recordMissingSide(0, 0, 16, 0)
65
+ tracker.recordMissingSide(0, 16, 16, 0)
66
+
67
+ expect(tracker.takeColumnsAwaitingNeighbor(16, 0).sort((a, b) => a.z - b.z)).toEqual([
68
+ { x: 0, z: 0 },
69
+ { x: 0, z: 16 }
70
+ ])
71
+ expect(tracker.takeColumnsAwaitingNeighbor(16, 0)).toEqual([])
72
+ })
73
+
74
+ test('clears column references on unload', () => {
75
+ const tracker = new PendingNeighborHealTracker()
76
+ tracker.recordMissingSide(0, 0, 16, 0)
77
+ tracker.clearColumn(0, 0)
78
+ expect(tracker.takeColumnsAwaitingNeighbor(16, 0)).toEqual([])
79
+ })
80
+
81
+ test('union grows when packet cache arrives for a missing side neighbor (I1 heal)', () => {
82
+ const worldCols = new Map<string, any>([['0,0', {}]])
83
+ const v17 = new Set<string>()
84
+ const caches = { raw: new Set<string>(), v17, v16: new Set<string>() }
85
+ const getColumn = (x: number, z: number) => worldCols.get(`${x},${z}`) ?? null
86
+
87
+ expect(collectChunksForColumnUnion(0, 0, getColumn, caches)).toHaveLength(1)
88
+ v17.add('16,0')
89
+ expect(collectChunksForColumnUnion(0, 0, getColumn, caches)).toHaveLength(2)
90
+ })
91
+ })