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,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
|
+
})
|