minecraft-renderer 0.1.107 → 0.1.108

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.
Files changed (90) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +80 -80
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +513 -513
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +5 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +13 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +3 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  19. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  20. package/src/lib/worldrendererCommon.ts +329 -67
  21. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  22. package/src/lighting/cache/rendererLightCache.ts +248 -0
  23. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  25. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  26. package/src/lighting/debug/clientLightProbe.ts +20 -0
  27. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  28. package/src/lighting/debug/clientLightTrace.ts +335 -0
  29. package/src/lighting/entity/entityLightController.test.ts +216 -0
  30. package/src/lighting/entity/entityLightController.ts +322 -0
  31. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  32. package/src/lighting/owner/clientLightOwner.ts +523 -0
  33. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  34. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  35. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  36. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  37. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  38. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  39. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  40. package/src/lighting/tables/lightTables1171.ts +59 -0
  41. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  42. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  43. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  44. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  45. package/src/mesher-shared/lightNibblePack.ts +26 -0
  46. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  47. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  48. package/src/mesher-shared/shared.ts +48 -1
  49. package/src/three/chunkMeshManager.ts +26 -0
  50. package/src/three/entities.ts +77 -11
  51. package/src/three/entity/EntityMesh.ts +1 -0
  52. package/src/three/entity/legWalkAnimation.test.ts +11 -2
  53. package/src/three/entity/legWalkAnimation.ts +1 -9
  54. package/src/three/globalBlockBuffer.ts +94 -2
  55. package/src/three/globalLegacyBuffer.ts +94 -2
  56. package/src/three/graphicsBackendBase.ts +2 -9
  57. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  58. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  59. package/src/three/worldRendererThree.ts +75 -34
  60. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  61. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  62. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  63. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  64. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  65. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  66. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  67. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  68. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  69. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  70. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  71. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  72. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  73. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  74. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  75. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  76. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  77. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  78. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  79. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  80. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  81. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  82. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  83. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  84. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  85. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  86. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  87. package/src/worldView/types.ts +1 -0
  88. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  89. package/src/worldView/worldView.spiral.test.ts +1 -1
  90. package/src/worldView/worldView.ts +46 -1
@@ -0,0 +1,523 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Production wiring for the dedicated WASM light-owner, behind
4
+ * `enableClientLightOwner` (default OFF).
5
+ *
6
+ * Product forks (matches §10, simpler option):
7
+ * - `ingestBlockSection` is sent from the main thread from prismarine column
8
+ * JSON in `addColumn`. Mesh workers stay read-only copies; N workers must
9
+ * not each ingest (would duplicate/reorder). The renderer is the sequencer.
10
+ * - Nether/End sky: `playerStateReactive.lightingDisabled` is already true for
11
+ * the_nether / the_end / !has_skylight. That drives `setSkyLightEnabled`.
12
+ */
13
+
14
+ import Chunks from 'prismarine-chunk'
15
+ import { Vec3 } from 'vec3'
16
+ import { RendererLightCache } from '../cache/rendererLightCache'
17
+ import { applyLightPublication, sectionIndexToWorldOrigin, type LightOwnerEvent, type LightPublication, type PublicationGate } from './lightOwnerHost'
18
+ import { buildLightTables1171, LIGHT_TABLES_MC_VERSION } from '../tables/lightTables1171'
19
+ import {
20
+ comparableNow,
21
+ ingestRemoteClientLightTraceMessage,
22
+ isClientLightTraceEnabled,
23
+ isClientLightTraceMessage,
24
+ recordClientLightTrace
25
+ } from '../debug/clientLightTrace'
26
+ import {
27
+ shouldAcceptVersionedMesh,
28
+ validateOwnerPublicationMessage,
29
+ type MeshGeometryMode,
30
+ type MeshSectionLightRequirement as VersionedMeshRequirement
31
+ } from '../versioning/clientLightVersions'
32
+
33
+ export { packUnpackedLightSection, unpackPackedLightSection } from '../../mesher-shared/lightNibblePack'
34
+ export { eventsFromParsedUpdateLight } from './updateLightToOwnerEvents'
35
+
36
+ export const LIGHT_OWNER_WORKER_SCRIPT = 'lightOwnerWorker.js'
37
+
38
+ export type ClientLightOwnerLifecycle = 'starting' | 'ready' | 'failed'
39
+
40
+ /** Owner tables are the 1.17.1 registry. Another session version must not start the worker. */
41
+ export function shouldSpawnClientLightOwner(config: { enableClientLightOwner?: boolean }, version: string): boolean {
42
+ return config.enableClientLightOwner === true && version === LIGHT_TABLES_MC_VERSION
43
+ }
44
+
45
+ export function clientLightOwnerVersionBlockReason(version: string): string | null {
46
+ if (version === LIGHT_TABLES_MC_VERSION) return null
47
+ return `client light owner supports ${LIGHT_TABLES_MC_VERSION} only (session ${version})`
48
+ }
49
+
50
+ /** Sky engine on unless the dimension already reported no skylight (nether/end). */
51
+ export function skyLightEnabledFromRendererState(state: { lightingDisabled?: boolean }): boolean {
52
+ return state.lightingDisabled !== true
53
+ }
54
+
55
+ export function blockChangeEvent(x: number, y: number, z: number, stateId: number): LightOwnerEvent {
56
+ return { type: 'blockChange', x, y, z, stateId }
57
+ }
58
+
59
+ export function eventsFromColumnUnload(chunkX: number, chunkZ: number): LightOwnerEvent {
60
+ return { type: 'unloadColumn', sx: Math.floor(chunkX / 16), sz: Math.floor(chunkZ / 16) }
61
+ }
62
+
63
+ export function dirtyMeshSectionsFromChangedLight(
64
+ changed: Array<{ sx: number; sy: number; sz: number }>,
65
+ opts?: { coords?: 'world' | 'section-index' }
66
+ ): Array<{ sx: number; sy: number; sz: number }> {
67
+ const coords = opts?.coords ?? 'world'
68
+ const keys = new Set<string>()
69
+ const out: Array<{ sx: number; sy: number; sz: number }> = []
70
+ const add = (sx: number, sy: number, sz: number) => {
71
+ const key = `${sx},${sy},${sz}`
72
+ if (keys.has(key)) return
73
+ keys.add(key)
74
+ out.push({ sx, sy, sz })
75
+ }
76
+ for (const section of changed) {
77
+ const origin = coords === 'section-index' ? sectionIndexToWorldOrigin(section.sx, section.sy, section.sz) : section
78
+ for (let dx = -16; dx <= 16; dx += 16) {
79
+ for (let dy = -16; dy <= 16; dy += 16) {
80
+ for (let dz = -16; dz <= 16; dz += 16) {
81
+ add(origin.sx + dx, origin.sy + dy, origin.sz + dz)
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return out
87
+ }
88
+
89
+ export type MeshSectionLightRequirement = VersionedMeshRequirement
90
+
91
+ export function meshSectionKey(sx: number, sy: number, sz: number): string {
92
+ return `${sx},${sy},${sz}`
93
+ }
94
+
95
+ /** Workers that mesh any dirty section; empty means fall back to every worker. */
96
+ export function meshWorkerIndexesForDirtySections(
97
+ dirty: Array<{ sx: number; sy: number; sz: number }>,
98
+ workerCount: number,
99
+ indexForSection: (sx: number, sy: number, sz: number) => number
100
+ ): number[] {
101
+ if (workerCount <= 0 || dirty.length === 0) return []
102
+ const indexes = new Set<number>()
103
+ for (const section of dirty) {
104
+ const index = indexForSection(section.sx, section.sy, section.sz)
105
+ if (index >= 0 && index < workerCount) indexes.add(index)
106
+ }
107
+ return [...indexes]
108
+ }
109
+
110
+ export function raiseRequiredLightRevisions(
111
+ required: Map<string, MeshSectionLightRequirement>,
112
+ dirtyMeshSections: Array<{ sx: number; sy: number; sz: number }>,
113
+ publicationVersion: number,
114
+ worldGeneration: number,
115
+ topologyBySection?: Map<string, number>
116
+ ): void {
117
+ for (const section of dirtyMeshSections) {
118
+ const key = meshSectionKey(section.sx, section.sy, section.sz)
119
+ const prev = required.get(key)
120
+ const liveTopology = topologyBySection?.get(key)
121
+ const topologyRevision = Math.max(prev?.topologyRevision ?? 0, liveTopology ?? 0) || undefined
122
+ const next: MeshSectionLightRequirement = {
123
+ requiredVersion: prev?.requiredVersion ?? publicationVersion,
124
+ worldGeneration: prev?.worldGeneration ?? worldGeneration,
125
+ ...(topologyRevision != null ? { topologyRevision } : {})
126
+ }
127
+ if (!prev || worldGeneration > prev.worldGeneration) {
128
+ next.requiredVersion = publicationVersion
129
+ next.worldGeneration = worldGeneration
130
+ } else if (worldGeneration === prev.worldGeneration && publicationVersion > prev.requiredVersion) {
131
+ next.requiredVersion = publicationVersion
132
+ }
133
+ if (
134
+ !prev ||
135
+ next.requiredVersion !== prev.requiredVersion ||
136
+ next.worldGeneration !== prev.worldGeneration ||
137
+ next.topologyRevision !== prev.topologyRevision
138
+ ) {
139
+ required.set(key, next)
140
+ }
141
+ }
142
+ }
143
+
144
+ export function coveringReplacementForReject(opts: {
145
+ accepted: boolean
146
+ required?: MeshSectionLightRequirement | null
147
+ }): { lightPublicationVersion: number; worldGeneration: number } | null {
148
+ if (opts.accepted || opts.required == null) return null
149
+ return { lightPublicationVersion: opts.required.requiredVersion, worldGeneration: opts.required.worldGeneration }
150
+ }
151
+
152
+ export function shouldAcceptMeshGeometry(
153
+ mesh: {
154
+ worldGeneration?: number
155
+ lightPublicationVersion?: number
156
+ topologyRevision?: number
157
+ sessionEpoch?: number
158
+ columnIncarnation?: number
159
+ hadErrors?: boolean
160
+ meshMode?: MeshGeometryMode
161
+ },
162
+ _gate: PublicationGate,
163
+ required?: MeshSectionLightRequirement | null,
164
+ opts?: { ownerManaged?: boolean; sessionEpoch?: number; columnIncarnation?: number }
165
+ ): boolean {
166
+ return shouldAcceptVersionedMesh(mesh, required, {
167
+ ownerManaged: opts?.ownerManaged === true,
168
+ sessionEpoch: opts?.sessionEpoch,
169
+ columnIncarnation: opts?.columnIncarnation
170
+ })
171
+ }
172
+
173
+ export function eventsFromColumnLoad(opts: {
174
+ chunkX: number
175
+ chunkZ: number
176
+ chunkJson: unknown
177
+ version: string
178
+ worldMinY: number
179
+ worldHeight: number
180
+ }): LightOwnerEvent[] {
181
+ const Chunk = Chunks(opts.version) as ReturnType<typeof Chunks>
182
+ let column: { getBlockStateId: (pos: Vec3) => number }
183
+ try {
184
+ if (opts.chunkJson && typeof (opts.chunkJson as { getBlockStateId?: unknown }).getBlockStateId === 'function') {
185
+ column = opts.chunkJson as { getBlockStateId: (pos: Vec3) => number }
186
+ } else {
187
+ column = Chunk.fromJson(opts.chunkJson as Parameters<ReturnType<typeof Chunks>['fromJson']>[0]) as unknown as {
188
+ getBlockStateId: (pos: Vec3) => number
189
+ }
190
+ }
191
+ } catch {
192
+ return []
193
+ }
194
+
195
+ const sx = Math.floor(opts.chunkX / 16)
196
+ const sz = Math.floor(opts.chunkZ / 16)
197
+ const events: LightOwnerEvent[] = []
198
+ const worldMaxY = opts.worldMinY + opts.worldHeight
199
+ for (let y0 = opts.worldMinY; y0 < worldMaxY; y0 += 16) {
200
+ const states = new Uint16Array(4096)
201
+ fillSectionStates(column, y0, states)
202
+ const sy = Math.floor(y0 / 16)
203
+ events.push({ type: 'ingestBlockSection', sx, sy, sz, states })
204
+ events.push({ type: 'setAvailability', sx, sy, sz, availability: 'loaded' })
205
+ }
206
+ return events
207
+ }
208
+
209
+ type SectionLike = {
210
+ palette?: number[] | null
211
+ data?: { get: (index: number) => number }
212
+ isEmpty?: () => boolean
213
+ getBlock?: (pos: { x: number; y: number; z: number }) => number
214
+ }
215
+
216
+ function sectionAt(column: object, y0: number): SectionLike | null | undefined {
217
+ const withGet = column as { getSection?: (pos: { x: number; y: number; z: number }) => SectionLike | null }
218
+ if (typeof withGet.getSection === 'function') {
219
+ return withGet.getSection({ x: 0, y: y0, z: 0 })
220
+ }
221
+ const withArray = column as { sections?: Array<SectionLike | null>; minY?: number }
222
+ if (Array.isArray(withArray.sections)) {
223
+ return withArray.sections[(y0 - (withArray.minY ?? 0)) >> 4]
224
+ }
225
+ return undefined
226
+ }
227
+
228
+ function fillSectionStates(column: { getBlockStateId: (pos: Vec3) => number }, y0: number, states: Uint16Array) {
229
+ const section = sectionAt(column, y0)
230
+ if (section === null) {
231
+ return
232
+ }
233
+ if (section && typeof section.isEmpty === 'function' && section.isEmpty()) {
234
+ return
235
+ }
236
+ if (section && Array.isArray(section.palette) && section.palette.length === 1) {
237
+ const only = section.palette[0] || 0
238
+ if (only !== 0) states.fill(only)
239
+ return
240
+ }
241
+ if (section?.data && typeof section.data.get === 'function') {
242
+ const palette = section.palette
243
+ for (let i = 0; i < 4096; i++) {
244
+ let stateId = section.data.get(i)
245
+ if (palette) stateId = palette[stateId]
246
+ states[i] = stateId || 0
247
+ }
248
+ return
249
+ }
250
+ const pos = new Vec3(0, y0, 0)
251
+ for (let ly = 0; ly < 16; ly++) {
252
+ pos.y = y0 + ly
253
+ for (let lz = 0; lz < 16; lz++) {
254
+ pos.z = lz
255
+ for (let lx = 0; lx < 16; lx++) {
256
+ pos.x = lx
257
+ let stateId = 0
258
+ try {
259
+ stateId = column.getBlockStateId(pos) || 0
260
+ } catch {
261
+ stateId = 0
262
+ }
263
+ states[lx + lz * 16 + ly * 256] = stateId
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ export type OwnerWorkerLightMessage = {
270
+ type: 'applyOwnerLightPublication'
271
+ worldGeneration: number
272
+ publicationVersion: number
273
+ sessionEpoch?: number
274
+ sections: Array<{ sx: number; sy: number; sz: number; blockLight: Uint8Array; skyLight?: Uint8Array }>
275
+ }
276
+
277
+ export type OwnerPublicationApplyResult = {
278
+ applied: boolean
279
+ lastVersion: number
280
+ acceptedGeneration: number
281
+ dirtyMeshSections: Array<{ sx: number; sy: number; sz: number }>
282
+ workerMessage: OwnerWorkerLightMessage | null
283
+ }
284
+
285
+ export function applyOwnerPublicationToRenderer(
286
+ cache: RendererLightCache,
287
+ publication: LightPublication,
288
+ gate: PublicationGate,
289
+ coords: 'world' | 'section-index' = 'section-index'
290
+ ): OwnerPublicationApplyResult {
291
+ const checked = validateOwnerPublicationMessage(publication)
292
+ if (!checked.ok) {
293
+ return {
294
+ applied: false,
295
+ lastVersion: gate.lastVersion,
296
+ acceptedGeneration: gate.acceptedGeneration,
297
+ dirtyMeshSections: [],
298
+ workerMessage: null
299
+ }
300
+ }
301
+ const result = applyLightPublication(cache, publication, gate, coords)
302
+ if (!result.applied) {
303
+ return {
304
+ applied: false,
305
+ lastVersion: result.lastVersion,
306
+ acceptedGeneration: result.acceptedGeneration,
307
+ dirtyMeshSections: [],
308
+ workerMessage: null
309
+ }
310
+ }
311
+ const worldSections = publication.sections.map(section => {
312
+ const origin = coords === 'section-index' ? sectionIndexToWorldOrigin(section.sx, section.sy, section.sz) : section
313
+ return {
314
+ sx: origin.sx,
315
+ sy: origin.sy,
316
+ sz: origin.sz,
317
+ blockLight: section.blockLight,
318
+ skyLight: section.skyLight
319
+ }
320
+ })
321
+ return {
322
+ applied: true,
323
+ lastVersion: result.lastVersion,
324
+ acceptedGeneration: result.acceptedGeneration,
325
+ dirtyMeshSections:
326
+ publication.dirtyMeshSections != null
327
+ ? Array.from({ length: publication.dirtyMeshSections.length / 3 }, (_, i) => ({
328
+ sx: publication.dirtyMeshSections![i * 3]!,
329
+ sy: publication.dirtyMeshSections![i * 3 + 1]!,
330
+ sz: publication.dirtyMeshSections![i * 3 + 2]!
331
+ }))
332
+ : dirtyMeshSectionsFromChangedLight(worldSections, { coords: 'world' }),
333
+ workerMessage: {
334
+ type: 'applyOwnerLightPublication',
335
+ worldGeneration: publication.worldGeneration,
336
+ publicationVersion: publication.publicationVersion,
337
+ sections: worldSections
338
+ }
339
+ }
340
+ }
341
+
342
+ export function loadDefaultOwnerLightTables(): { emission: Uint8Array; opacity: Uint8Array; occupancy: Uint8Array } {
343
+ const tables = buildLightTables1171()
344
+ return { emission: tables.emission, opacity: tables.opacity, occupancy: tables.occupancy }
345
+ }
346
+
347
+ export class ClientLightOwnerSession {
348
+ readonly worker: Worker
349
+ gate: PublicationGate = { acceptedGeneration: 1, lastVersion: 0 }
350
+ readonly requiredLightBySection = new Map<string, MeshSectionLightRequirement>()
351
+ private stepScheduled = false
352
+ private latestEditGeneration = 0
353
+ private lifecycle: ClientLightOwnerLifecycle = 'starting'
354
+ private lastOwnerEnqueueAt = 0
355
+ private failureReason: string | null = null
356
+
357
+ constructor(
358
+ private readonly cache: RendererLightCache,
359
+ opts: {
360
+ createWorker: (onMessage: (data: any) => void) => Worker
361
+ worldMinY: number
362
+ worldHeight: number
363
+ skyLightEnabled: boolean
364
+ onApplied: (result: OwnerPublicationApplyResult) => void
365
+ onFailed?: (reason: string) => void
366
+ onOwnerIdle?: (settledEditGeneration: number) => void
367
+ }
368
+ ) {
369
+ this.onApplied = opts.onApplied
370
+ this.onFailed = opts.onFailed
371
+ this.onOwnerIdle = opts.onOwnerIdle
372
+ this.worker = opts.createWorker(data => this.onMessage(data))
373
+ this.worker.onerror = event => {
374
+ this.fail(event?.message || 'light owner worker error')
375
+ }
376
+ this.worker.postMessage({ type: 'init', worldMinY: opts.worldMinY, worldHeight: opts.worldHeight })
377
+ // 1.17.1 state ids. `shouldSpawnClientLightOwner` refuses every other session version before this runs.
378
+ const tables = loadDefaultOwnerLightTables()
379
+ this.worker.postMessage({ type: 'setLightTables', emission: tables.emission, opacity: tables.opacity, occupancy: tables.occupancy })
380
+ this.worker.postMessage({ type: 'setSkyLightEnabled', enabled: opts.skyLightEnabled })
381
+ }
382
+
383
+ get state(): ClientLightOwnerLifecycle {
384
+ return this.lifecycle
385
+ }
386
+
387
+ get isReady(): boolean {
388
+ return this.lifecycle === 'ready'
389
+ }
390
+
391
+ get lastFailureReason(): string | null {
392
+ return this.failureReason
393
+ }
394
+
395
+ private readonly onApplied: (result: OwnerPublicationApplyResult) => void
396
+ private readonly onFailed?: (reason: string) => void
397
+ private readonly onOwnerIdle?: (settledEditGeneration: number) => void
398
+
399
+ notePlayerEdit(): number {
400
+ this.latestEditGeneration += 1
401
+ return this.latestEditGeneration
402
+ }
403
+
404
+ pushEvent(event: LightOwnerEvent) {
405
+ if (this.lifecycle === 'failed') return
406
+ this.worker.postMessage({ type: 'pushEvent', event })
407
+ this.scheduleStep()
408
+ this.lastOwnerEnqueueAt = comparableNow()
409
+ if (isClientLightTraceEnabled()) {
410
+ const currentColumn =
411
+ 'x' in event && typeof event.x === 'number'
412
+ ? `${Math.floor(event.x / 16)},${Math.floor((event as { z: number }).z / 16)}`
413
+ : 'sx' in event && typeof event.sx === 'number'
414
+ ? `${event.sx},${(event as { sz: number }).sz}`
415
+ : undefined
416
+ recordClientLightTrace({
417
+ phase: 'ownerEnqueue',
418
+ currentColumn,
419
+ eventAgeMs: 0
420
+ })
421
+ }
422
+ }
423
+
424
+ pushRawUpdateLight(kind: 'setUpdateLightV17' | 'setUpdateLightV16', payload: Record<string, unknown>) {
425
+ if (this.lifecycle === 'failed') return
426
+ this.worker.postMessage({ type: kind, ...payload })
427
+ this.scheduleStep()
428
+ }
429
+
430
+ setSkyLightEnabled(enabled: boolean) {
431
+ if (this.lifecycle === 'failed') return
432
+ this.worker.postMessage({ type: 'setSkyLightEnabled', enabled })
433
+ this.scheduleStep()
434
+ }
435
+
436
+ ingestColumn(opts: Parameters<typeof eventsFromColumnLoad>[0]) {
437
+ for (const event of eventsFromColumnLoad(opts)) this.pushEvent(event)
438
+ }
439
+
440
+ onBlockChange(x: number, y: number, z: number, stateId: number) {
441
+ this.pushEvent(blockChangeEvent(x, y, z, stateId))
442
+ }
443
+
444
+ requiredLightForSection(key: string): MeshSectionLightRequirement | undefined {
445
+ return this.requiredLightBySection.get(key)
446
+ }
447
+
448
+ onGeometryReload(chunkX: number, chunkZ: number) {
449
+ for (const [key, required] of this.requiredLightBySection) {
450
+ const [sx, , sz] = key.split(',').map(Number)
451
+ if (sx === chunkX && sz === chunkZ) delete required.topologyRevision
452
+ }
453
+ }
454
+
455
+ onUnload(chunkX: number, chunkZ: number) {
456
+ this.pushEvent(eventsFromColumnUnload(chunkX, chunkZ))
457
+ for (const key of [...this.requiredLightBySection.keys()]) {
458
+ const [sx, , sz] = key.split(',').map(Number)
459
+ if (sx === chunkX && sz === chunkZ) this.requiredLightBySection.delete(key)
460
+ }
461
+ }
462
+
463
+ terminate() {
464
+ this.worker.terminate()
465
+ }
466
+
467
+ private fail(reason: string) {
468
+ if (this.lifecycle === 'failed') return
469
+ this.lifecycle = 'failed'
470
+ this.failureReason = reason
471
+ this.stepScheduled = false
472
+ this.onFailed?.(reason)
473
+ }
474
+
475
+ private scheduleStep() {
476
+ if (this.stepScheduled || this.lifecycle === 'failed') return
477
+ this.stepScheduled = true
478
+ setTimeout(() => {
479
+ this.stepScheduled = false
480
+ if (this.lifecycle === 'failed') return
481
+ this.worker.postMessage({ type: 'step', budgetMs: 5, stepGeneration: this.latestEditGeneration })
482
+ }, 0)
483
+ }
484
+
485
+ private onMessage(data: any) {
486
+ if (!data || typeof data !== 'object') return
487
+ if (isClientLightTraceMessage(data)) {
488
+ ingestRemoteClientLightTraceMessage(data)
489
+ return
490
+ }
491
+ if (data.type === 'error') {
492
+ this.fail(typeof data.error === 'string' ? data.error : 'light owner worker error')
493
+ return
494
+ }
495
+ if (this.lifecycle === 'failed') return
496
+ if (data.type === 'ready' || data.type === 'tablesSet') {
497
+ this.lifecycle = 'ready'
498
+ }
499
+ if (data.type === 'stepped') {
500
+ if (data.publication) this.applyPublication(data.publication)
501
+ if (data.remaining !== true && typeof data.stepGeneration === 'number') this.onOwnerIdle?.(data.stepGeneration)
502
+ }
503
+ if (data.type === 'publication' && data.publication) this.applyPublication(data.publication)
504
+ }
505
+
506
+ private applyPublication(publication: LightPublication) {
507
+ if (this.lifecycle !== 'ready') return
508
+ const result = applyOwnerPublicationToRenderer(this.cache, publication, this.gate, 'section-index')
509
+ if (!result.applied) return
510
+ this.gate = { acceptedGeneration: result.acceptedGeneration, lastVersion: result.lastVersion }
511
+ raiseRequiredLightRevisions(this.requiredLightBySection, result.dirtyMeshSections, result.lastVersion, result.acceptedGeneration)
512
+ this.onApplied(result)
513
+ if (isClientLightTraceEnabled()) {
514
+ recordClientLightTrace({
515
+ phase: 'ownerComplete',
516
+ lightVersion: result.lastVersion,
517
+ worldGeneration: result.acceptedGeneration,
518
+ queueDepth: result.dirtyMeshSections.length,
519
+ txnMs: this.lastOwnerEnqueueAt ? comparableNow() - this.lastOwnerEnqueueAt : undefined
520
+ })
521
+ }
522
+ }
523
+ }
@@ -0,0 +1,78 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import { RendererLightCache } from '../cache/rendererLightCache'
4
+ import { applyLightPublication, LightOwnerHost, type LightPublication } from './lightOwnerHost'
5
+
6
+ function packedNibble(value: number): Uint8Array {
7
+ const n = value & 0x0f
8
+ return new Uint8Array(2048).fill(n | (n << 4))
9
+ }
10
+
11
+ describe('LightOwnerHost publications', () => {
12
+ it('writes packed block-only sections without baking missing sky as 15', () => {
13
+ const cache = new RendererLightCache('1.17.1')
14
+ cache.setWorldBounds(0, 256)
15
+ const pub: LightPublication = {
16
+ worldGeneration: 1,
17
+ publicationVersion: 1,
18
+ sections: [{ sx: 0, sy: 64, sz: 0, blockLight: packedNibble(9) }]
19
+ }
20
+ const result = applyLightPublication(cache, pub, { acceptedGeneration: 1, lastVersion: 0 })
21
+ expect(result.applied).toBe(true)
22
+ expect(cache.getLight(0, 64, 0).block).toBeCloseTo(11 / 15, 5)
23
+ expect(cache.getLight(0, 64, 0).sky).toBeCloseTo(2 / 15, 5)
24
+ })
25
+
26
+ it('discards a stale publication version', () => {
27
+ const cache = new RendererLightCache('1.17.1')
28
+ cache.setWorldBounds(0, 256)
29
+ const stale: LightPublication = {
30
+ worldGeneration: 1,
31
+ publicationVersion: 3,
32
+ sections: [{ sx: 0, sy: 64, sz: 0, blockLight: packedNibble(4) }]
33
+ }
34
+ const result = applyLightPublication(cache, stale, { acceptedGeneration: 1, lastVersion: 4 })
35
+ expect(result.applied).toBe(false)
36
+ expect(cache.getLight(0, 64, 0)).toEqual({ block: 0, sky: 1 })
37
+ })
38
+
39
+ it('steps an in-process owner and publishes increasing versions', async () => {
40
+ const cache = new RendererLightCache('1.17.1')
41
+ cache.setWorldBounds(0, 256)
42
+ const host = await LightOwnerHost.createInProcess(cache, { worldMinY: 0, worldHeight: 256 })
43
+ host.setLightTables({
44
+ emission: Uint8Array.of(0, 0, 14),
45
+ opacity: Uint8Array.of(0, 15, 0)
46
+ })
47
+ host.pushEvent({ type: 'ingestBlockSection', sx: 0, sy: 4, sz: 0, states: new Uint16Array(4096) })
48
+ host.pushEvent({ type: 'blockChange', x: 8, y: 64, z: 8, stateId: 2 })
49
+ const first = await host.stepUntilIdle(16)
50
+ expect(first?.publicationVersion).toBeGreaterThan(0)
51
+ host.pushEvent({ type: 'blockChange', x: 8, y: 64, z: 8, stateId: 0 })
52
+ const second = await host.stepUntilIdle(16)
53
+ expect(second?.publicationVersion).toBeGreaterThan(first!.publicationVersion)
54
+ expect(cache.getLight(8, 64, 8).block).toBeCloseTo(2 / 15, 5)
55
+ })
56
+
57
+ it('publishes sky so an opaque roof darkens the column', async () => {
58
+ const cache = new RendererLightCache('1.17.1')
59
+ cache.setWorldBounds(0, 256)
60
+ const host = await LightOwnerHost.createInProcess(cache, { worldMinY: 0, worldHeight: 256 })
61
+ host.setLightTables({
62
+ emission: Uint8Array.of(0, 0, 14),
63
+ opacity: Uint8Array.of(0, 15, 0)
64
+ })
65
+ host.pushEvent({ type: 'ingestBlockSection', sx: 0, sy: 15, sz: 0, states: new Uint16Array(4096) })
66
+ await host.stepUntilIdle(32)
67
+ expect(cache.getLight(8, 255, 8).sky).toBeCloseTo(1, 5)
68
+ expect(cache.getLight(8, 249, 8).sky).toBeCloseTo(1, 5)
69
+ for (let z = 0; z < 16; z++) {
70
+ for (let x = 0; x < 16; x++) {
71
+ host.pushEvent({ type: 'blockChange', x, y: 250, z, stateId: 1 })
72
+ }
73
+ }
74
+ await host.stepUntilIdle(64)
75
+ expect(cache.getLight(8, 251, 8).sky).toBeCloseTo(1, 5)
76
+ expect(cache.getLight(8, 249, 8).sky).toBeCloseTo(2 / 15, 5)
77
+ })
78
+ })