minecraft-renderer 0.1.107 → 0.1.109
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/lightOwnerWorker.js +9 -0
- package/dist/lightOwnerWorker.js.map +7 -0
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +222 -222
- package/dist/minecraft-renderer.js +84 -84
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +517 -517
- package/package.json +1 -1
- package/src/bundler/bundlePrepare.test.ts +25 -0
- package/src/bundler/bundlePrepare.ts +1 -1
- package/src/graphicsBackend/appViewer.ts +15 -0
- package/src/graphicsBackend/config.ts +5 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +13 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +3 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +329 -67
- package/src/lighting/cache/rendererLightCache.test.ts +158 -0
- package/src/lighting/cache/rendererLightCache.ts +248 -0
- package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
- package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
- package/src/lighting/debug/clientLightProbe.test.ts +24 -0
- package/src/lighting/debug/clientLightProbe.ts +20 -0
- package/src/lighting/debug/clientLightTrace.test.ts +271 -0
- package/src/lighting/debug/clientLightTrace.ts +335 -0
- package/src/lighting/entity/entityLightController.test.ts +216 -0
- package/src/lighting/entity/entityLightController.ts +322 -0
- package/src/lighting/owner/clientLightOwner.test.ts +467 -0
- package/src/lighting/owner/clientLightOwner.ts +523 -0
- package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
- package/src/lighting/owner/lightOwnerHost.ts +202 -0
- package/src/lighting/owner/lightOwnerWorker.ts +154 -0
- package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
- package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
- package/src/lighting/tables/lightTables1171.generated.json +10 -0
- package/src/lighting/tables/lightTables1171.test.ts +168 -0
- package/src/lighting/tables/lightTables1171.ts +59 -0
- package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
- package/src/lighting/versioning/clientLightVersions.ts +203 -0
- package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
- package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
- package/src/mesher-shared/lightNibblePack.ts +26 -0
- package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
- package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
- package/src/mesher-shared/shared.ts +48 -1
- package/src/three/chunkMeshManager.ts +26 -0
- package/src/three/cloudCover.ts +22 -0
- package/src/three/cloudGeometry.ts +71 -0
- package/src/three/entities.ts +77 -11
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +11 -2
- package/src/three/entity/legWalkAnimation.ts +1 -9
- package/src/three/globalBlockBuffer.ts +94 -2
- package/src/three/globalLegacyBuffer.ts +94 -2
- package/src/three/graphicsBackendBase.ts +2 -9
- package/src/three/modules/clouds.ts +98 -75
- package/src/three/tests/cloudGeometry.test.ts +87 -0
- package/src/three/tests/clouds.test.ts +65 -14
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/worldRendererThree.ts +75 -34
- package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
- package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
- package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
- package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
- package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
- package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
- package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
- package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
- package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
- package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
- package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
- package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
- package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
- package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
- package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
- package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
- package/src/worldView/types.ts +1 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +46 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import Chunks from 'prismarine-chunk'
|
|
4
|
+
import MinecraftData from 'minecraft-data'
|
|
5
|
+
import { Vec3 } from 'vec3'
|
|
6
|
+
import { RendererLightCache } from '../cache/rendererLightCache'
|
|
7
|
+
import { defaultWorldRendererConfig } from '../../graphicsBackend/config'
|
|
8
|
+
import { LightOwnerHost, type LightPublication, type PublicationGate } from './lightOwnerHost'
|
|
9
|
+
import { maskBitSet, worldSectionMaskBit, type ParsedUpdateLight } from '../../wasm-mesher/worker/mesherWasmLightMerge'
|
|
10
|
+
import {
|
|
11
|
+
applyOwnerPublicationToRenderer,
|
|
12
|
+
blockChangeEvent,
|
|
13
|
+
ClientLightOwnerSession,
|
|
14
|
+
dirtyMeshSectionsFromChangedLight,
|
|
15
|
+
eventsFromColumnLoad,
|
|
16
|
+
meshWorkerIndexesForDirtySections,
|
|
17
|
+
eventsFromColumnUnload,
|
|
18
|
+
eventsFromParsedUpdateLight,
|
|
19
|
+
packUnpackedLightSection,
|
|
20
|
+
raiseRequiredLightRevisions,
|
|
21
|
+
shouldAcceptMeshGeometry,
|
|
22
|
+
clientLightOwnerVersionBlockReason,
|
|
23
|
+
shouldSpawnClientLightOwner,
|
|
24
|
+
skyLightEnabledFromRendererState,
|
|
25
|
+
unpackPackedLightSection
|
|
26
|
+
} from './clientLightOwner'
|
|
27
|
+
|
|
28
|
+
const VERSION = '1.17.1'
|
|
29
|
+
|
|
30
|
+
function packedNibble(value: number): Uint8Array {
|
|
31
|
+
const n = value & 0x0f
|
|
32
|
+
return new Uint8Array(2048).fill(n | (n << 4))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeAirColumnJson() {
|
|
36
|
+
const Chunk = Chunks(VERSION) as any
|
|
37
|
+
const chunk = new Chunk()
|
|
38
|
+
return chunk.toJson()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unpackedSection(value: number): Uint8Array {
|
|
42
|
+
return new Uint8Array(4096).fill(value & 0x0f)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeSession() {
|
|
46
|
+
let onMessage: (data: any) => void = () => {}
|
|
47
|
+
const worker = {
|
|
48
|
+
postMessage: () => {},
|
|
49
|
+
terminate: () => {},
|
|
50
|
+
onerror: null as ((event: ErrorEvent) => void) | null
|
|
51
|
+
}
|
|
52
|
+
const cache = new RendererLightCache(VERSION)
|
|
53
|
+
cache.setWorldBounds(0, 256)
|
|
54
|
+
const session = new ClientLightOwnerSession(cache, {
|
|
55
|
+
createWorker: handler => {
|
|
56
|
+
onMessage = handler
|
|
57
|
+
return worker as unknown as Worker
|
|
58
|
+
},
|
|
59
|
+
worldMinY: 0,
|
|
60
|
+
worldHeight: 256,
|
|
61
|
+
skyLightEnabled: true,
|
|
62
|
+
onApplied: () => {}
|
|
63
|
+
})
|
|
64
|
+
return { session, worker, deliver: (data: any) => onMessage(data) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('client light owner flag', () => {
|
|
68
|
+
it('stays default-off so production does not spawn the owner', () => {
|
|
69
|
+
expect(defaultWorldRendererConfig.enableClientLightOwner).toBe(false)
|
|
70
|
+
expect(shouldSpawnClientLightOwner(defaultWorldRendererConfig, '1.17.1')).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('spawns only when the flag is true and the session is 1.17.1', () => {
|
|
74
|
+
expect(shouldSpawnClientLightOwner({ enableClientLightOwner: true }, '1.17.1')).toBe(true)
|
|
75
|
+
expect(shouldSpawnClientLightOwner({ enableClientLightOwner: false }, '1.17.1')).toBe(false)
|
|
76
|
+
expect(shouldSpawnClientLightOwner({}, '1.17.1')).toBe(false)
|
|
77
|
+
expect(shouldSpawnClientLightOwner({ enableClientLightOwner: true }, '1.16.5')).toBe(false)
|
|
78
|
+
expect(shouldSpawnClientLightOwner({ enableClientLightOwner: true }, '1.18.2')).toBe(false)
|
|
79
|
+
expect(clientLightOwnerVersionBlockReason('1.17.1')).toBeNull()
|
|
80
|
+
expect(clientLightOwnerVersionBlockReason('1.16.5')).toMatch(/1\.17\.1/)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('client light owner lifecycle', () => {
|
|
85
|
+
it('retains light requirements but clears topology only for geometry-reloaded columns', () => {
|
|
86
|
+
const { session, worker } = makeSession()
|
|
87
|
+
const send = vi.spyOn(worker, 'postMessage')
|
|
88
|
+
session.requiredLightBySection.set('0,64,0', { worldGeneration: 1, requiredVersion: 7, topologyRevision: 9 })
|
|
89
|
+
session.requiredLightBySection.set('16,64,0', { worldGeneration: 1, requiredVersion: 8, topologyRevision: 10 })
|
|
90
|
+
session.onGeometryReload(0, 0)
|
|
91
|
+
expect(session.requiredLightBySection.get('0,64,0')).toEqual({ worldGeneration: 1, requiredVersion: 7 })
|
|
92
|
+
expect(session.requiredLightBySection.get('16,64,0')?.topologyRevision).toBe(10)
|
|
93
|
+
expect(send).not.toHaveBeenCalled()
|
|
94
|
+
session.onUnload(0, 0)
|
|
95
|
+
expect(session.requiredLightBySection.has('0,64,0')).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('starts in starting and is not ready until the worker reports ready', () => {
|
|
99
|
+
const { session } = makeSession()
|
|
100
|
+
expect(session.state).toBe('starting')
|
|
101
|
+
expect(session.isReady).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('becomes ready after the worker ready message', () => {
|
|
105
|
+
const { session, deliver } = makeSession()
|
|
106
|
+
deliver({ type: 'ready' })
|
|
107
|
+
expect(session.state).toBe('ready')
|
|
108
|
+
expect(session.isReady).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('goes to failed on a worker error message', () => {
|
|
112
|
+
const { session, deliver } = makeSession()
|
|
113
|
+
deliver({ type: 'error', error: 'NetworkError loading lightOwnerWorker.js' })
|
|
114
|
+
expect(session.state).toBe('failed')
|
|
115
|
+
expect(session.isReady).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('goes to failed on worker onerror (script 404)', () => {
|
|
119
|
+
const { session, worker } = makeSession()
|
|
120
|
+
expect(typeof worker.onerror).toBe('function')
|
|
121
|
+
worker.onerror?.({ type: 'error', message: 'NetworkError' } as ErrorEvent)
|
|
122
|
+
expect(session.state).toBe('failed')
|
|
123
|
+
expect(session.isReady).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does not leave failed after a late ready', () => {
|
|
127
|
+
const { session, deliver } = makeSession()
|
|
128
|
+
deliver({ type: 'error', error: 'init failed' })
|
|
129
|
+
deliver({ type: 'ready' })
|
|
130
|
+
expect(session.state).toBe('failed')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('owner step scheduling', () => {
|
|
135
|
+
it('does not bounce remaining slices through main setTimeout', () => {
|
|
136
|
+
vi.useFakeTimers()
|
|
137
|
+
try {
|
|
138
|
+
const posts: Array<{ type?: string }> = []
|
|
139
|
+
let onMessage: (data: any) => void = () => {}
|
|
140
|
+
const worker = {
|
|
141
|
+
postMessage: (message: { type?: string }) => {
|
|
142
|
+
posts.push(message)
|
|
143
|
+
},
|
|
144
|
+
terminate: () => {},
|
|
145
|
+
onerror: null as ((event: ErrorEvent) => void) | null
|
|
146
|
+
}
|
|
147
|
+
const cache = new RendererLightCache(VERSION)
|
|
148
|
+
cache.setWorldBounds(0, 256)
|
|
149
|
+
const session = new ClientLightOwnerSession(cache, {
|
|
150
|
+
createWorker: handler => {
|
|
151
|
+
onMessage = handler
|
|
152
|
+
return worker as unknown as Worker
|
|
153
|
+
},
|
|
154
|
+
worldMinY: 0,
|
|
155
|
+
worldHeight: 256,
|
|
156
|
+
skyLightEnabled: true,
|
|
157
|
+
onApplied: () => {}
|
|
158
|
+
})
|
|
159
|
+
onMessage({ type: 'ready' })
|
|
160
|
+
posts.length = 0
|
|
161
|
+
session.pushEvent(blockChangeEvent(8, 64, 8, 1))
|
|
162
|
+
vi.runAllTimers()
|
|
163
|
+
expect(posts.filter(message => message.type === 'step')).toHaveLength(1)
|
|
164
|
+
posts.length = 0
|
|
165
|
+
onMessage({ type: 'stepped', remaining: true, publication: null })
|
|
166
|
+
vi.runAllTimers()
|
|
167
|
+
expect(posts.filter(message => message.type === 'step')).toHaveLength(0)
|
|
168
|
+
} finally {
|
|
169
|
+
vi.useRealTimers()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('selects only mesh workers that own dirty sections', () => {
|
|
174
|
+
expect(
|
|
175
|
+
meshWorkerIndexesForDirtySections(
|
|
176
|
+
[
|
|
177
|
+
{ sx: 0, sy: 64, sz: 0 },
|
|
178
|
+
{ sx: 16, sy: 64, sz: 0 },
|
|
179
|
+
{ sx: 0, sy: 80, sz: 0 }
|
|
180
|
+
],
|
|
181
|
+
8,
|
|
182
|
+
(sx, _sy, sz) => (((sx / 16 + sz / 16) % 8) + 8) % 8
|
|
183
|
+
).sort((a, b) => a - b)
|
|
184
|
+
).toEqual([0, 1])
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('nether/end sky detection', () => {
|
|
189
|
+
it('disables sky from lightingDisabled (nether/end / !hasSkyLight)', () => {
|
|
190
|
+
expect(skyLightEnabledFromRendererState({ lightingDisabled: false })).toBe(true)
|
|
191
|
+
expect(skyLightEnabledFromRendererState({ lightingDisabled: true })).toBe(false)
|
|
192
|
+
expect(skyLightEnabledFromRendererState({})).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('dirtyMeshSections stencil', () => {
|
|
197
|
+
it('covers the 3x3x3 sample neighborhood, including the diagonal section', () => {
|
|
198
|
+
const dirty = dirtyMeshSectionsFromChangedLight([{ sx: 0, sy: 64, sz: 0 }], { coords: 'world' })
|
|
199
|
+
const keys = new Set(dirty.map(s => `${s.sx},${s.sy},${s.sz}`))
|
|
200
|
+
expect(keys.has('0,64,0')).toBe(true)
|
|
201
|
+
expect(keys.has('-16,64,0')).toBe(true)
|
|
202
|
+
expect(keys.has('16,64,0')).toBe(true)
|
|
203
|
+
expect(keys.has('0,48,0')).toBe(true)
|
|
204
|
+
expect(keys.has('0,80,0')).toBe(true)
|
|
205
|
+
expect(keys.has('0,64,-16')).toBe(true)
|
|
206
|
+
expect(keys.has('0,64,16')).toBe(true)
|
|
207
|
+
expect(keys.has('16,80,16')).toBe(true)
|
|
208
|
+
expect(keys.size).toBe(27)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('schedules geometry (16,16,16) when light (0,0,0) changes — west-face corner samples (15,15,15)', () => {
|
|
212
|
+
const dirty = dirtyMeshSectionsFromChangedLight([{ sx: 0, sy: 0, sz: 0 }], { coords: 'world' })
|
|
213
|
+
expect(dirty.some(s => s.sx === 16 && s.sy === 16 && s.sz === 16)).toBe(true)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('converts engine section-index publications to world origins before expanding', () => {
|
|
217
|
+
const dirty = dirtyMeshSectionsFromChangedLight([{ sx: 0, sy: 4, sz: 0 }], { coords: 'section-index' })
|
|
218
|
+
expect(dirty.some(s => s.sx === 0 && s.sy === 64 && s.sz === 0)).toBe(true)
|
|
219
|
+
expect(dirty.some(s => s.sx === 0 && s.sy === 80 && s.sz === 0)).toBe(true)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('versioned mesh drop', () => {
|
|
224
|
+
const gate: PublicationGate = { acceptedGeneration: 2, lastVersion: 5 }
|
|
225
|
+
|
|
226
|
+
it('accepts geometry with no version (flag-off / pre-owner mesh path)', () => {
|
|
227
|
+
expect(shouldAcceptMeshGeometry({}, gate)).toBe(true)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('owner-managed path rejects unversioned geometry as current', () => {
|
|
231
|
+
const required = { requiredVersion: 5, worldGeneration: 2 }
|
|
232
|
+
expect(shouldAcceptMeshGeometry({}, gate, required, { ownerManaged: true })).toBe(false)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('accepts a mesh of an independent section after an unrelated later publication', () => {
|
|
236
|
+
expect(shouldAcceptMeshGeometry({ worldGeneration: 1, lightPublicationVersion: 1 }, { acceptedGeneration: 1, lastVersion: 2 })).toBe(true)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('drops a mesh below the required revision of its own section', () => {
|
|
240
|
+
const required = { requiredVersion: 5, worldGeneration: 2 }
|
|
241
|
+
expect(shouldAcceptMeshGeometry({ worldGeneration: 1, lightPublicationVersion: 9 }, gate, required)).toBe(false)
|
|
242
|
+
expect(shouldAcceptMeshGeometry({ worldGeneration: 2, lightPublicationVersion: 4 }, gate, required)).toBe(false)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('accepts geometry stamped with the required publication of its section', () => {
|
|
246
|
+
expect(shouldAcceptMeshGeometry({ worldGeneration: 2, lightPublicationVersion: 5 }, gate, { requiredVersion: 5, worldGeneration: 2 })).toBe(true)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('raiseRequiredLightRevisions', () => {
|
|
251
|
+
it('keeps the higher topology revision when a later publication raises light version', () => {
|
|
252
|
+
const required = new Map()
|
|
253
|
+
raiseRequiredLightRevisions(required, [{ sx: 0, sy: 64, sz: 0 }], 2, 1)
|
|
254
|
+
required.set('0,64,0', { ...required.get('0,64,0')!, topologyRevision: 4 })
|
|
255
|
+
raiseRequiredLightRevisions(required, [{ sx: 0, sy: 64, sz: 0 }], 5, 1)
|
|
256
|
+
expect(required.get('0,64,0')).toEqual({ requiredVersion: 5, worldGeneration: 1, topologyRevision: 4 })
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('does not roll topology back when merging a lower live snapshot after a higher one', () => {
|
|
260
|
+
const required = new Map()
|
|
261
|
+
raiseRequiredLightRevisions(required, [{ sx: 0, sy: 64, sz: 0 }], 2, 1, new Map([['0,64,0', 7]]))
|
|
262
|
+
raiseRequiredLightRevisions(required, [{ sx: 0, sy: 64, sz: 0 }], 3, 1, new Map([['0,64,0', 5]]))
|
|
263
|
+
expect(required.get('0,64,0')).toEqual({ requiredVersion: 3, worldGeneration: 1, topologyRevision: 7 })
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('packed nibble convert', () => {
|
|
268
|
+
it('round-trips unpacked 4096 nibble-bytes through packed 2048', () => {
|
|
269
|
+
const unpacked = unpackedSection(0)
|
|
270
|
+
unpacked[0] = 7
|
|
271
|
+
unpacked[1] = 14
|
|
272
|
+
unpacked[256] = 3
|
|
273
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
274
|
+
expect(packed).toHaveLength(2048)
|
|
275
|
+
const back = unpackPackedLightSection(packed)
|
|
276
|
+
expect(back[0]).toBe(7)
|
|
277
|
+
expect(back[1]).toBe(14)
|
|
278
|
+
expect(back[256]).toBe(3)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe('update_light → owner serverLight events', () => {
|
|
283
|
+
it('emits omitted/empty/data independently for sky and block', () => {
|
|
284
|
+
const skyLightMask = new Uint32Array(2)
|
|
285
|
+
const emptySkyLightMask = new Uint32Array(2)
|
|
286
|
+
const blockLightMask = new Uint32Array(2)
|
|
287
|
+
const emptyBlockLightMask = new Uint32Array(2)
|
|
288
|
+
maskBitSet(blockLightMask, worldSectionMaskBit(4))
|
|
289
|
+
maskBitSet(emptyBlockLightMask, worldSectionMaskBit(0))
|
|
290
|
+
maskBitSet(skyLightMask, worldSectionMaskBit(4))
|
|
291
|
+
const blockLight = new Uint8Array(16 * 4096)
|
|
292
|
+
blockLight.fill(9, 4 * 4096, 5 * 4096)
|
|
293
|
+
const skyLight = new Uint8Array(16 * 4096)
|
|
294
|
+
skyLight.fill(15, 4 * 4096, 5 * 4096)
|
|
295
|
+
const parsed: ParsedUpdateLight = {
|
|
296
|
+
x: 1,
|
|
297
|
+
z: 2,
|
|
298
|
+
trustEdges: true,
|
|
299
|
+
numSections: 16,
|
|
300
|
+
skyLight,
|
|
301
|
+
blockLight,
|
|
302
|
+
skyLightMask,
|
|
303
|
+
emptySkyLightMask,
|
|
304
|
+
blockLightMask,
|
|
305
|
+
emptyBlockLightMask
|
|
306
|
+
}
|
|
307
|
+
const events = eventsFromParsedUpdateLight(parsed, 0)
|
|
308
|
+
const blockData = events.find(e => e.type === 'serverLight' && e.channel === 'block' && e.sy === 4 && e.kind === 'data')
|
|
309
|
+
const blockEmpty = events.find(e => e.type === 'serverLight' && e.channel === 'block' && e.sy === 0 && e.kind === 'empty')
|
|
310
|
+
const skyData = events.find(e => e.type === 'serverLight' && e.channel === 'sky' && e.sy === 4 && e.kind === 'data')
|
|
311
|
+
const omittedBlock = events.find(e => e.type === 'serverLight' && e.channel === 'block' && e.sy === 1)
|
|
312
|
+
expect(blockData).toBeTruthy()
|
|
313
|
+
expect(blockEmpty).toBeTruthy()
|
|
314
|
+
expect(skyData).toBeTruthy()
|
|
315
|
+
expect(omittedBlock).toBeUndefined()
|
|
316
|
+
if (blockData?.type === 'serverLight' && blockData.kind === 'data') {
|
|
317
|
+
expect(blockData.data).toHaveLength(2048)
|
|
318
|
+
expect(blockData.sx).toBe(1)
|
|
319
|
+
expect(blockData.sz).toBe(2)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
describe('column load events', () => {
|
|
325
|
+
it('ingests block sections from prismarine JSON as Loaded (main thread, not mesh workers)', () => {
|
|
326
|
+
const mcData = MinecraftData(VERSION)
|
|
327
|
+
const Chunk = Chunks(VERSION) as any
|
|
328
|
+
const chunk = new Chunk()
|
|
329
|
+
const torch = mcData.blocksByName.torch.defaultState
|
|
330
|
+
chunk.setBlockStateId(new Vec3(8, 64, 8), torch)
|
|
331
|
+
const events = eventsFromColumnLoad({
|
|
332
|
+
chunkX: 0,
|
|
333
|
+
chunkZ: 16,
|
|
334
|
+
chunkJson: chunk.toJson(),
|
|
335
|
+
version: VERSION,
|
|
336
|
+
worldMinY: 0,
|
|
337
|
+
worldHeight: 256
|
|
338
|
+
})
|
|
339
|
+
const ingest = events.filter(e => e.type === 'ingestBlockSection')
|
|
340
|
+
const loaded = events.filter(e => e.type === 'setAvailability' && e.availability === 'loaded')
|
|
341
|
+
expect(ingest.length).toBeGreaterThan(0)
|
|
342
|
+
expect(loaded.length).toBe(ingest.length)
|
|
343
|
+
const section4 = ingest.find(e => e.type === 'ingestBlockSection' && e.sy === 4 && e.sz === 1)
|
|
344
|
+
expect(section4?.type === 'ingestBlockSection' && section4.states[8 + 8 * 16 + 0 * 256]).toBe(torch)
|
|
345
|
+
expect(blockChangeEvent(8, 64, 8, 0)).toEqual({ type: 'blockChange', x: 8, y: 64, z: 8, stateId: 0 })
|
|
346
|
+
expect(eventsFromColumnUnload(32, 48)).toEqual({ type: 'unloadColumn', sx: 2, sz: 3 })
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('does not call getBlockStateId 4096 times per section when section data exists', () => {
|
|
350
|
+
const mcData = MinecraftData(VERSION)
|
|
351
|
+
const Chunk = Chunks(VERSION) as any
|
|
352
|
+
const chunk = new Chunk()
|
|
353
|
+
const torch = mcData.blocksByName.torch.defaultState
|
|
354
|
+
const stone = mcData.blocksByName.stone.defaultState
|
|
355
|
+
chunk.setBlockStateId(new Vec3(8, 64, 8), torch)
|
|
356
|
+
chunk.setBlockStateId(new Vec3(3, 20, 3), stone)
|
|
357
|
+
let calls = 0
|
|
358
|
+
const original = chunk.getBlockStateId.bind(chunk)
|
|
359
|
+
chunk.getBlockStateId = (pos: Vec3) => {
|
|
360
|
+
calls++
|
|
361
|
+
return original(pos)
|
|
362
|
+
}
|
|
363
|
+
const events = eventsFromColumnLoad({
|
|
364
|
+
chunkX: 0,
|
|
365
|
+
chunkZ: 0,
|
|
366
|
+
chunkJson: chunk,
|
|
367
|
+
version: VERSION,
|
|
368
|
+
worldMinY: 0,
|
|
369
|
+
worldHeight: 256
|
|
370
|
+
})
|
|
371
|
+
expect(calls).toBe(0)
|
|
372
|
+
const ingest = events.filter(e => e.type === 'ingestBlockSection')
|
|
373
|
+
const section4 = ingest.find(e => e.type === 'ingestBlockSection' && e.sy === 4)
|
|
374
|
+
const section1 = ingest.find(e => e.type === 'ingestBlockSection' && e.sy === 1)
|
|
375
|
+
expect(section4?.type === 'ingestBlockSection' && section4.states[8 + 8 * 16 + 0 * 256]).toBe(torch)
|
|
376
|
+
expect(section1?.type === 'ingestBlockSection' && section1.states[3 + 3 * 16 + 4 * 256]).toBe(stone)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
describe('apply publication → cache + dirty mesh + worker fan-out', () => {
|
|
381
|
+
it('writes packed channels directly and reports dirtyMeshSections plus a worker message', () => {
|
|
382
|
+
const cache = new RendererLightCache(VERSION)
|
|
383
|
+
cache.setWorldBounds(0, 256)
|
|
384
|
+
const pub: LightPublication = {
|
|
385
|
+
worldGeneration: 1,
|
|
386
|
+
publicationVersion: 3,
|
|
387
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: packedNibble(9), skyLight: packedNibble(2) }]
|
|
388
|
+
}
|
|
389
|
+
const result = applyOwnerPublicationToRenderer(cache, pub, { acceptedGeneration: 1, lastVersion: 0 }, 'section-index')
|
|
390
|
+
expect(result.applied).toBe(true)
|
|
391
|
+
expect(result.lastVersion).toBe(3)
|
|
392
|
+
expect(cache.getLight(0, 64, 0).block).toBeCloseTo(11 / 15, 5)
|
|
393
|
+
expect(cache.getLight(0, 64, 0).sky).toBeCloseTo(4 / 15, 5)
|
|
394
|
+
expect(result.dirtyMeshSections.some(s => s.sx === 0 && s.sy === 64 && s.sz === 0)).toBe(true)
|
|
395
|
+
expect(result.dirtyMeshSections.some(s => s.sx === 16 && s.sy === 64 && s.sz === 0)).toBe(true)
|
|
396
|
+
expect(result.workerMessage?.type).toBe('applyOwnerLightPublication')
|
|
397
|
+
expect(result.workerMessage?.publicationVersion).toBe(3)
|
|
398
|
+
expect(result.workerMessage?.sections[0]?.blockLight).toHaveLength(2048)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('uses exact owner world-origin dirtiness without expanding the stencil again', () => {
|
|
402
|
+
const cache = new RendererLightCache(VERSION)
|
|
403
|
+
cache.setWorldBounds(0, 256)
|
|
404
|
+
const result = applyOwnerPublicationToRenderer(
|
|
405
|
+
cache,
|
|
406
|
+
{
|
|
407
|
+
worldGeneration: 1,
|
|
408
|
+
publicationVersion: 1,
|
|
409
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: packedNibble(1) }],
|
|
410
|
+
dirtyMeshSections: new Int32Array([0, 64, 0])
|
|
411
|
+
},
|
|
412
|
+
{ acceptedGeneration: 1, lastVersion: 0 },
|
|
413
|
+
'section-index'
|
|
414
|
+
)
|
|
415
|
+
expect(result.dirtyMeshSections).toEqual([{ sx: 0, sy: 64, sz: 0 }])
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('rejects a foreign publication with the wrong packed length and does not write the cache', () => {
|
|
419
|
+
const cache = new RendererLightCache(VERSION)
|
|
420
|
+
cache.setWorldBounds(0, 256)
|
|
421
|
+
const result = applyOwnerPublicationToRenderer(
|
|
422
|
+
cache,
|
|
423
|
+
{
|
|
424
|
+
worldGeneration: 1,
|
|
425
|
+
publicationVersion: 4,
|
|
426
|
+
sections: [{ sx: 0, sy: 4, sz: 0, blockLight: new Uint8Array(8) }]
|
|
427
|
+
},
|
|
428
|
+
{ acceptedGeneration: 1, lastVersion: 0 },
|
|
429
|
+
'section-index'
|
|
430
|
+
)
|
|
431
|
+
expect(result.applied).toBe(false)
|
|
432
|
+
expect(result.workerMessage).toBeNull()
|
|
433
|
+
expect(cache.getLight(0, 64, 0).block).toBe(0)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
describe('flag-on in-process host: load / setBlock / unload → publication → cache', () => {
|
|
438
|
+
it('ingests a torch place and then air through the owner ABI', async () => {
|
|
439
|
+
const cache = new RendererLightCache(VERSION)
|
|
440
|
+
cache.setWorldBounds(0, 256)
|
|
441
|
+
const host = await LightOwnerHost.createInProcess(cache, { worldMinY: 0, worldHeight: 256 })
|
|
442
|
+
host.setLightTables({
|
|
443
|
+
emission: Uint8Array.of(0, 0, 14),
|
|
444
|
+
opacity: Uint8Array.of(0, 15, 0)
|
|
445
|
+
})
|
|
446
|
+
for (const event of eventsFromColumnLoad({
|
|
447
|
+
chunkX: 0,
|
|
448
|
+
chunkZ: 0,
|
|
449
|
+
chunkJson: makeAirColumnJson(),
|
|
450
|
+
version: VERSION,
|
|
451
|
+
worldMinY: 0,
|
|
452
|
+
worldHeight: 256
|
|
453
|
+
})) {
|
|
454
|
+
if ('sy' in event && event.sy !== 4) continue
|
|
455
|
+
host.pushEvent(event)
|
|
456
|
+
}
|
|
457
|
+
host.pushEvent(blockChangeEvent(8, 64, 8, 2))
|
|
458
|
+
const placed = await host.stepUntilIdle(64)
|
|
459
|
+
expect(placed?.publicationVersion).toBeGreaterThan(0)
|
|
460
|
+
expect(cache.getLight(8, 64, 8).block).toBeGreaterThan(0.5)
|
|
461
|
+
host.pushEvent(blockChangeEvent(8, 64, 8, 0))
|
|
462
|
+
await host.stepUntilIdle(32)
|
|
463
|
+
expect(cache.getLight(8, 64, 8).block).toBeCloseTo(2 / 15, 5)
|
|
464
|
+
host.pushEvent(eventsFromColumnUnload(0, 0))
|
|
465
|
+
await host.stepUntilIdle(16)
|
|
466
|
+
})
|
|
467
|
+
})
|