minecraft-renderer 0.1.106 → 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.
- 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 +105 -76
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +537 -508
- 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 +8 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/skyLight.ts +17 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +340 -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/entities.ts +97 -8
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +154 -0
- package/src/three/entity/legWalkAnimation.ts +193 -0
- 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 +191 -0
- package/src/three/modules/index.ts +4 -0
- package/src/three/modules/sunMoon.ts +151 -0
- package/src/three/skyboxRenderer.ts +15 -8
- package/src/three/tests/clouds.test.ts +59 -0
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/tests/waterFog.test.ts +25 -0
- package/src/three/waterFog.ts +28 -0
- package/src/three/worldRendererThree.ts +84 -39
- 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 +3 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +49 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { createEmptyLightCache, isLightSectionPresent, worldSectionMaskBit, type ParsedUpdateLight } from './mesherWasmLightMerge'
|
|
4
|
+
import {
|
|
5
|
+
applyOwnerPublicationToColumnCaches,
|
|
6
|
+
applyPackedOwnerSectionsToLightCache,
|
|
7
|
+
applyRawLightPacketToCaches,
|
|
8
|
+
detachDisplayOnOwnerTakeover,
|
|
9
|
+
ownerDeltaSectionWorldYs,
|
|
10
|
+
revertDisplayToIncoming
|
|
11
|
+
} from './mesherWasmOwnerLight'
|
|
12
|
+
import { packUnpackedLightSection } from '../../mesher-shared/lightNibblePack'
|
|
13
|
+
|
|
14
|
+
describe('applyPackedOwnerSectionsToLightCache', () => {
|
|
15
|
+
it('unpacks owner packed channels into the worker column cache without touching other sections', () => {
|
|
16
|
+
const cache = createEmptyLightCache(16)
|
|
17
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
18
|
+
unpacked[0] = 9
|
|
19
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
20
|
+
const next = applyPackedOwnerSectionsToLightCache(cache, [{ sx: 0, sy: 64, sz: 0, blockLight: packed, skyLight: packed }], 0, 0, 0, 16)
|
|
21
|
+
expect(next.blockLight[4 * 4096]).toBe(9)
|
|
22
|
+
expect(next.skyLight[4 * 4096]).toBe(9)
|
|
23
|
+
expect(isLightSectionPresent(next.blockPresent, worldSectionMaskBit(4))).toBe(true)
|
|
24
|
+
expect(next.blockLight[0]).toBe(0)
|
|
25
|
+
expect(isLightSectionPresent(next.blockPresent, worldSectionMaskBit(0))).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('writes a section delta in place without cloning the full column arrays', () => {
|
|
29
|
+
const cache = createEmptyLightCache(16)
|
|
30
|
+
cache.blockLight[0] = 4
|
|
31
|
+
cache.skyLight[0] = 7
|
|
32
|
+
const blockRef = cache.blockLight
|
|
33
|
+
const skyRef = cache.skyLight
|
|
34
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
35
|
+
unpacked[0] = 9
|
|
36
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
37
|
+
const next = applyPackedOwnerSectionsToLightCache(cache, [{ sx: 0, sy: 64, sz: 0, blockLight: packed, skyLight: packed }], 0, 0, 0, 16)
|
|
38
|
+
expect(next.blockLight).toBe(blockRef)
|
|
39
|
+
expect(next.skyLight).toBe(skyRef)
|
|
40
|
+
expect(next.blockLight[0]).toBe(4)
|
|
41
|
+
expect(next.skyLight[0]).toBe(7)
|
|
42
|
+
expect(next.blockLight[4 * 4096]).toBe(9)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('lists only the published section Y origins for a column', () => {
|
|
46
|
+
expect(
|
|
47
|
+
ownerDeltaSectionWorldYs(
|
|
48
|
+
[
|
|
49
|
+
{ sx: 0, sy: 64, sz: 0 },
|
|
50
|
+
{ sx: 16, sy: 64, sz: 0 },
|
|
51
|
+
{ sx: 0, sy: 80, sz: 0 }
|
|
52
|
+
],
|
|
53
|
+
0,
|
|
54
|
+
0
|
|
55
|
+
)
|
|
56
|
+
).toEqual([64, 80])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('applyRawLightPacketToCaches', () => {
|
|
61
|
+
function parsedBlock(sectionIndex: number, value: number): ParsedUpdateLight {
|
|
62
|
+
const blockLight = new Uint8Array(16 * 4096)
|
|
63
|
+
blockLight.fill(value, sectionIndex * 4096, (sectionIndex + 1) * 4096)
|
|
64
|
+
const blockLightMask = new Uint32Array(2)
|
|
65
|
+
blockLightMask[0] = 1 << (sectionIndex + 1)
|
|
66
|
+
return {
|
|
67
|
+
x: 0,
|
|
68
|
+
z: 0,
|
|
69
|
+
trustEdges: true,
|
|
70
|
+
numSections: 16,
|
|
71
|
+
skyLight: new Uint8Array(16 * 4096),
|
|
72
|
+
blockLight,
|
|
73
|
+
skyLightMask: new Uint32Array(2),
|
|
74
|
+
emptySkyLightMask: new Uint32Array(2),
|
|
75
|
+
blockLightMask,
|
|
76
|
+
emptyBlockLightMask: new Uint32Array(2)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('writes incoming server light into the display cache when the owner does not own the column', () => {
|
|
81
|
+
const result = applyRawLightPacketToCaches({
|
|
82
|
+
ownerOwnsColumn: false,
|
|
83
|
+
incoming: undefined,
|
|
84
|
+
display: undefined,
|
|
85
|
+
parsed: parsedBlock(4, 9)
|
|
86
|
+
})
|
|
87
|
+
expect(result.dirtyDisplay).toBe(true)
|
|
88
|
+
expect(result.display?.blockLight[4 * 4096]).toBe(9)
|
|
89
|
+
expect(result.incoming.blockLight[4 * 4096]).toBe(9)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('keeps incoming server light separate and does not overwrite owner display cache', () => {
|
|
93
|
+
const display = createEmptyLightCache(16)
|
|
94
|
+
display.blockLight[4 * 4096] = 3
|
|
95
|
+
const result = applyRawLightPacketToCaches({
|
|
96
|
+
ownerOwnsColumn: true,
|
|
97
|
+
incoming: undefined,
|
|
98
|
+
display,
|
|
99
|
+
parsed: parsedBlock(4, 9)
|
|
100
|
+
})
|
|
101
|
+
expect(result.dirtyDisplay).toBe(false)
|
|
102
|
+
expect(result.display?.blockLight[4 * 4096]).toBe(3)
|
|
103
|
+
expect(result.incoming.blockLight[4 * 4096]).toBe(9)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('owner takeover incoming/display split', () => {
|
|
108
|
+
it('clones display once so an in-place owner write does not change incoming server light', () => {
|
|
109
|
+
const incoming = createEmptyLightCache(16)
|
|
110
|
+
incoming.blockLight[4 * 4096] = 9
|
|
111
|
+
const aliased = detachDisplayOnOwnerTakeover({ incoming, display: incoming })
|
|
112
|
+
expect(aliased.display).not.toBe(aliased.incoming)
|
|
113
|
+
|
|
114
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
115
|
+
unpacked[0] = 3
|
|
116
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
117
|
+
const applied = applyOwnerPublicationToColumnCaches({
|
|
118
|
+
incoming: aliased.incoming,
|
|
119
|
+
display: aliased.display,
|
|
120
|
+
sections: [{ sx: 0, sy: 64, sz: 0, blockLight: packed }],
|
|
121
|
+
worldMinY: 0,
|
|
122
|
+
columnWorldX: 0,
|
|
123
|
+
columnWorldZ: 0,
|
|
124
|
+
numSections: 16
|
|
125
|
+
})
|
|
126
|
+
expect(applied.incoming?.blockLight[4 * 4096]).toBe(9)
|
|
127
|
+
expect(applied.display.blockLight[4 * 4096]).toBe(3)
|
|
128
|
+
expect(applied.display.blockLight).not.toBe(applied.incoming?.blockLight)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('does not clone the full column on a second owner delta', () => {
|
|
132
|
+
const incoming = createEmptyLightCache(16)
|
|
133
|
+
incoming.blockLight[4 * 4096] = 9
|
|
134
|
+
const first = detachDisplayOnOwnerTakeover({ incoming, display: incoming })
|
|
135
|
+
const displayRef = first.display!.blockLight
|
|
136
|
+
const unpacked = new Uint8Array(4096).fill(0)
|
|
137
|
+
unpacked[0] = 3
|
|
138
|
+
const packed = packUnpackedLightSection(unpacked)
|
|
139
|
+
const second = applyOwnerPublicationToColumnCaches({
|
|
140
|
+
incoming: first.incoming,
|
|
141
|
+
display: first.display,
|
|
142
|
+
sections: [{ sx: 0, sy: 64, sz: 0, blockLight: packed }],
|
|
143
|
+
worldMinY: 0,
|
|
144
|
+
columnWorldX: 0,
|
|
145
|
+
columnWorldZ: 0,
|
|
146
|
+
numSections: 16
|
|
147
|
+
})
|
|
148
|
+
expect(second.display.blockLight).toBe(displayRef)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reverts display to last server incoming without zeroing omitted sections', () => {
|
|
152
|
+
const incoming = createEmptyLightCache(16)
|
|
153
|
+
incoming.blockLight[4 * 4096] = 9
|
|
154
|
+
const display = createEmptyLightCache(16)
|
|
155
|
+
display.blockLight[4 * 4096] = 3
|
|
156
|
+
const reverted = revertDisplayToIncoming({ incoming, display })
|
|
157
|
+
expect(reverted.display?.blockLight[4 * 4096]).toBe(9)
|
|
158
|
+
expect(reverted.incoming?.blockLight[4 * 4096]).toBe(9)
|
|
159
|
+
expect(isLightSectionPresent(reverted.incoming!.blockPresent, worldSectionMaskBit(0))).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import {
|
|
3
|
+
cloneLightCache,
|
|
4
|
+
createEmptyLightCache,
|
|
5
|
+
maskBitSet,
|
|
6
|
+
mergeUpdateLight,
|
|
7
|
+
worldSectionMaskBit,
|
|
8
|
+
type ParsedUpdateLight,
|
|
9
|
+
type UpdateLightColumnCache
|
|
10
|
+
} from './mesherWasmLightMerge'
|
|
11
|
+
import { unpackPackedLightSection } from '../../mesher-shared/lightNibblePack'
|
|
12
|
+
|
|
13
|
+
export type OwnerPackedSection = {
|
|
14
|
+
sx: number
|
|
15
|
+
sy: number
|
|
16
|
+
sz: number
|
|
17
|
+
blockLight: Uint8Array
|
|
18
|
+
skyLight?: Uint8Array
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** World-origin Y of published sections that belong to this column. */
|
|
22
|
+
export function ownerDeltaSectionWorldYs(sections: Array<{ sx: number; sy: number; sz: number }>, columnWorldX: number, columnWorldZ: number): number[] {
|
|
23
|
+
const colSx = Math.floor(columnWorldX / 16) * 16
|
|
24
|
+
const colSz = Math.floor(columnWorldZ / 16) * 16
|
|
25
|
+
const ys: number[] = []
|
|
26
|
+
const seen = new Set<number>()
|
|
27
|
+
for (const section of sections) {
|
|
28
|
+
if (section.sx !== colSx || section.sz !== colSz) continue
|
|
29
|
+
if (seen.has(section.sy)) continue
|
|
30
|
+
seen.add(section.sy)
|
|
31
|
+
ys.push(section.sy)
|
|
32
|
+
}
|
|
33
|
+
return ys
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Write owner packed 2048-byte channels into the worker's unpacked column cache. */
|
|
37
|
+
export function applyPackedOwnerSectionsToLightCache(
|
|
38
|
+
cache: UpdateLightColumnCache | undefined,
|
|
39
|
+
sections: OwnerPackedSection[],
|
|
40
|
+
worldMinY: number,
|
|
41
|
+
columnWorldX: number,
|
|
42
|
+
columnWorldZ: number,
|
|
43
|
+
numSections: number
|
|
44
|
+
): UpdateLightColumnCache {
|
|
45
|
+
const next = cache ?? createEmptyLightCache(numSections)
|
|
46
|
+
const colSx = Math.floor(columnWorldX / 16) * 16
|
|
47
|
+
const colSz = Math.floor(columnWorldZ / 16) * 16
|
|
48
|
+
for (const section of sections) {
|
|
49
|
+
if (section.sx !== colSx || section.sz !== colSz) continue
|
|
50
|
+
const sectionIndex = Math.floor((section.sy - worldMinY) / 16)
|
|
51
|
+
if (sectionIndex < 0 || sectionIndex >= next.numSections) continue
|
|
52
|
+
const start = sectionIndex * 4096
|
|
53
|
+
const block = unpackPackedLightSection(section.blockLight)
|
|
54
|
+
next.blockLight.set(block, start)
|
|
55
|
+
maskBitSet(next.blockPresent, worldSectionMaskBit(sectionIndex))
|
|
56
|
+
if (section.skyLight) {
|
|
57
|
+
const sky = unpackPackedLightSection(section.skyLight)
|
|
58
|
+
next.skyLight.set(sky, start)
|
|
59
|
+
maskBitSet(next.skyPresent, worldSectionMaskBit(sectionIndex))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return next
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function applyRawLightPacketToCaches(opts: {
|
|
66
|
+
ownerOwnsColumn: boolean
|
|
67
|
+
incoming: UpdateLightColumnCache | undefined
|
|
68
|
+
display: UpdateLightColumnCache | undefined
|
|
69
|
+
parsed: ParsedUpdateLight
|
|
70
|
+
}): { incoming: UpdateLightColumnCache; display: UpdateLightColumnCache | undefined; dirtyDisplay: boolean } {
|
|
71
|
+
const incoming = mergeUpdateLight(opts.incoming, opts.parsed)
|
|
72
|
+
if (opts.ownerOwnsColumn) {
|
|
73
|
+
return { incoming, display: opts.display, dirtyDisplay: false }
|
|
74
|
+
}
|
|
75
|
+
return { incoming, display: incoming, dirtyDisplay: true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** First owner takeover: clone incoming onto display once. Later deltas write display in place. */
|
|
79
|
+
export function detachDisplayOnOwnerTakeover(opts: { incoming: UpdateLightColumnCache | undefined; display: UpdateLightColumnCache | undefined }): {
|
|
80
|
+
incoming: UpdateLightColumnCache | undefined
|
|
81
|
+
display: UpdateLightColumnCache | undefined
|
|
82
|
+
} {
|
|
83
|
+
if (!opts.incoming) return { incoming: undefined, display: opts.display }
|
|
84
|
+
if (opts.display == null || opts.display === opts.incoming) {
|
|
85
|
+
return { incoming: opts.incoming, display: cloneLightCache(opts.incoming) }
|
|
86
|
+
}
|
|
87
|
+
return { incoming: opts.incoming, display: opts.display }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function applyOwnerPublicationToColumnCaches(opts: {
|
|
91
|
+
incoming: UpdateLightColumnCache | undefined
|
|
92
|
+
display: UpdateLightColumnCache | undefined
|
|
93
|
+
sections: OwnerPackedSection[]
|
|
94
|
+
worldMinY: number
|
|
95
|
+
columnWorldX: number
|
|
96
|
+
columnWorldZ: number
|
|
97
|
+
numSections: number
|
|
98
|
+
}): { incoming: UpdateLightColumnCache | undefined; display: UpdateLightColumnCache } {
|
|
99
|
+
const detached = detachDisplayOnOwnerTakeover({ incoming: opts.incoming, display: opts.display })
|
|
100
|
+
return {
|
|
101
|
+
incoming: detached.incoming,
|
|
102
|
+
display: applyPackedOwnerSectionsToLightCache(detached.display, opts.sections, opts.worldMinY, opts.columnWorldX, opts.columnWorldZ, opts.numSections)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function revertDisplayToIncoming(opts: { incoming: UpdateLightColumnCache | undefined; display: UpdateLightColumnCache | undefined }): {
|
|
107
|
+
incoming: UpdateLightColumnCache | undefined
|
|
108
|
+
display: UpdateLightColumnCache | undefined
|
|
109
|
+
} {
|
|
110
|
+
if (!opts.incoming) return { incoming: undefined, display: opts.display }
|
|
111
|
+
return { incoming: opts.incoming, display: cloneLightCache(opts.incoming) }
|
|
112
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Tick-scoped cache for WASM column parses in the two-step mesh path.
|
|
4
|
+
*
|
|
5
|
+
* `getOrConvertColumn` only covers the JS walk. The three WASM converters
|
|
6
|
+
* (`convertRawMapChunkToWasm` / `convertParsedV17ToWasm` / `convertParsedV16ToWasm`)
|
|
7
|
+
* used to re-parse the same neighbour for every dirty target column in a tick.
|
|
8
|
+
* This map lives for one `processColumnTick` and is dropped in `finally`.
|
|
9
|
+
*
|
|
10
|
+
* Same contract as `getOrConvertColumn`: cached values are `set()` sources /
|
|
11
|
+
* WASM inputs only. Consumers must not mutate them, so we do not copy.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let cache: Map<string, unknown> | null = null
|
|
15
|
+
let hits = 0
|
|
16
|
+
let misses = 0
|
|
17
|
+
|
|
18
|
+
export function beginTickWasmParseCache(): void {
|
|
19
|
+
cache = new Map()
|
|
20
|
+
hits = 0
|
|
21
|
+
misses = 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function endTickWasmParseCache(): void {
|
|
25
|
+
cache = null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTickWasmParseStats(): { hits: number; misses: number } {
|
|
29
|
+
return { hits, misses }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getOrConvertTickWasmParse<T>(key: string, convert: () => T | null): { result: T | null; hit: boolean } {
|
|
33
|
+
if (!cache) {
|
|
34
|
+
misses++
|
|
35
|
+
return { result: convert(), hit: false }
|
|
36
|
+
}
|
|
37
|
+
if (cache.has(key)) {
|
|
38
|
+
hits++
|
|
39
|
+
return { result: cache.get(key) as T, hit: true }
|
|
40
|
+
}
|
|
41
|
+
const result = convert()
|
|
42
|
+
misses++
|
|
43
|
+
if (result != null) cache.set(key, result)
|
|
44
|
+
return { result, hit: false }
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Mesh ticks. Bulk work is polled on an interval. An urgent dirty kicks the
|
|
4
|
+
* same runner on the next turn, and a kick during an in-progress tick reruns
|
|
5
|
+
* after that tick yields instead of waiting for the next poll.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const BULK_MESH_INTERVAL_MS = 50
|
|
9
|
+
|
|
10
|
+
export function createMeshTickScheduler(run: () => Promise<void> | void) {
|
|
11
|
+
let running = false
|
|
12
|
+
let rerun = false
|
|
13
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
14
|
+
|
|
15
|
+
const pump = async () => {
|
|
16
|
+
timer = null
|
|
17
|
+
if (running) {
|
|
18
|
+
rerun = true
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
running = true
|
|
22
|
+
try {
|
|
23
|
+
do {
|
|
24
|
+
rerun = false
|
|
25
|
+
await run()
|
|
26
|
+
} while (rerun)
|
|
27
|
+
} finally {
|
|
28
|
+
running = false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
kick() {
|
|
34
|
+
if (running) {
|
|
35
|
+
rerun = true
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (timer != null) return
|
|
39
|
+
timer = setTimeout(() => {
|
|
40
|
+
void pump()
|
|
41
|
+
}, 0)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Topology-keyed cache for the per-section visGraph + block-entity post-pass.
|
|
4
|
+
*
|
|
5
|
+
* Wave-2 covering remeshes carry the same `topologyRevision` as wave-1, so the
|
|
6
|
+
* 4096-cell walk can be reused. A new revision always recomputes. Legacy /
|
|
7
|
+
* unversioned jobs (no revision) never cache.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const TOPOLOGY_POST_CACHE_LIMIT = 1024
|
|
11
|
+
|
|
12
|
+
export type TopologyPostPayload = {
|
|
13
|
+
visibilitySet: unknown
|
|
14
|
+
signs: Record<string, unknown>
|
|
15
|
+
heads: Record<string, unknown>
|
|
16
|
+
banners: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CacheEntry = {
|
|
20
|
+
topologyRevision: number
|
|
21
|
+
payload: TopologyPostPayload
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cache = new Map<string, CacheEntry>()
|
|
25
|
+
|
|
26
|
+
export function getOrComputeTopologyPost<T extends TopologyPostPayload>(
|
|
27
|
+
key: string,
|
|
28
|
+
topologyRevision: number | undefined,
|
|
29
|
+
compute: () => T
|
|
30
|
+
): { payload: T; hit: boolean } {
|
|
31
|
+
if (topologyRevision == null) {
|
|
32
|
+
return { payload: compute(), hit: false }
|
|
33
|
+
}
|
|
34
|
+
const existing = cache.get(key)
|
|
35
|
+
if (existing && existing.topologyRevision === topologyRevision) {
|
|
36
|
+
cache.delete(key)
|
|
37
|
+
cache.set(key, existing)
|
|
38
|
+
return { payload: existing.payload as T, hit: true }
|
|
39
|
+
}
|
|
40
|
+
const payload = compute()
|
|
41
|
+
cache.set(key, { topologyRevision, payload })
|
|
42
|
+
evictIfNeeded()
|
|
43
|
+
return { payload, hit: false }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function clearTopologyPostCacheColumn(x: number, z: number): void {
|
|
47
|
+
for (const key of [...cache.keys()]) {
|
|
48
|
+
const [sx, , sz] = key.split(',').map(Number)
|
|
49
|
+
if (sx === x && sz === z) cache.delete(key)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearTopologyPostCache(): void {
|
|
54
|
+
cache.clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function evictIfNeeded(): void {
|
|
58
|
+
while (cache.size > TOPOLOGY_POST_CACHE_LIMIT) {
|
|
59
|
+
const oldest = cache.keys().next().value
|
|
60
|
+
if (oldest == null) break
|
|
61
|
+
cache.delete(oldest)
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/worldView/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface LoadChunkData {
|
|
|
37
37
|
export interface UnloadChunkData {
|
|
38
38
|
x: number
|
|
39
39
|
z: number
|
|
40
|
+
preserveClientLight?: boolean
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -144,6 +145,8 @@ export type WorldViewEvents = {
|
|
|
144
145
|
entityMoved: (data: any) => void
|
|
145
146
|
playerEntity: (data: any) => void
|
|
146
147
|
time: (data: number) => void
|
|
148
|
+
/** 0 = full moon … 7 (vanilla order). Hosts should re-emit on day change (`bot.time.moonPhase`). */
|
|
149
|
+
moonPhase: (phase: number) => void
|
|
147
150
|
renderDistance: (viewDistance: number) => void
|
|
148
151
|
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
|
|
149
152
|
markAsLoaded: (data: ChunkPos) => void
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { WorldView } from './worldView'
|
|
4
|
+
|
|
5
|
+
const column = () => ({ toJson: () => '{}', minY: 0, worldHeight: 256 })
|
|
6
|
+
function setup(getColumnAt = vi.fn(async () => column())) {
|
|
7
|
+
const view = new WorldView({ getColumnAt, setBlockStateId: vi.fn() }, 4)
|
|
8
|
+
view.loadedChunks = { '0,0': true, '16,0': true }
|
|
9
|
+
return { view, getColumnAt }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('geometry-only column reload', () => {
|
|
13
|
+
it('retains membership and requests light preservation', async () => {
|
|
14
|
+
const { view } = setup()
|
|
15
|
+
const unload = vi.fn()
|
|
16
|
+
const load = vi.fn()
|
|
17
|
+
view.on('unloadChunk', unload)
|
|
18
|
+
view.on('loadChunk', load)
|
|
19
|
+
await view.reloadColumnGeometry()
|
|
20
|
+
expect(view.loadedChunks).toEqual({ '0,0': true, '16,0': true })
|
|
21
|
+
expect(unload.mock.calls.map(([data]) => data)).toEqual([
|
|
22
|
+
{ x: 0, z: 0, preserveClientLight: true },
|
|
23
|
+
{ x: 16, z: 0, preserveClientLight: true }
|
|
24
|
+
])
|
|
25
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('does not resurrect a column unloaded during an asynchronous fetch', async () => {
|
|
29
|
+
let resolve!: (value: ReturnType<typeof column>) => void
|
|
30
|
+
const pending = new Promise<ReturnType<typeof column>>(r => {
|
|
31
|
+
resolve = r
|
|
32
|
+
})
|
|
33
|
+
const { view } = setup(vi.fn(() => pending))
|
|
34
|
+
view.loadedChunks = { '0,0': true }
|
|
35
|
+
const load = vi.fn()
|
|
36
|
+
view.on('loadChunk', load)
|
|
37
|
+
const reload = view.reloadColumnGeometry()
|
|
38
|
+
expect(view.reloadColumnGeometry()).toBe(reload)
|
|
39
|
+
view.unloadChunk({ x: 0, z: 0 })
|
|
40
|
+
resolve(column())
|
|
41
|
+
await reload
|
|
42
|
+
expect(load).not.toHaveBeenCalled()
|
|
43
|
+
expect(view.loadedChunks['0,0']).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('cleans up failed columns and continues loading the rest', async () => {
|
|
47
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
48
|
+
try {
|
|
49
|
+
const get = vi.fn().mockRejectedValueOnce(new Error('fetch failed')).mockResolvedValue(column())
|
|
50
|
+
const { view } = setup(get)
|
|
51
|
+
const load = vi.fn()
|
|
52
|
+
view.on('loadChunk', load)
|
|
53
|
+
await view.reloadColumnGeometry()
|
|
54
|
+
expect(view.loadedChunks).toEqual({ '16,0': true })
|
|
55
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
56
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
57
|
+
} finally {
|
|
58
|
+
warn.mockRestore()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('truly unloads columns outside the current range or missing from the world', async () => {
|
|
63
|
+
const { view } = setup(vi.fn(async () => null) as any)
|
|
64
|
+
view.loadedChunks['160,0'] = true
|
|
65
|
+
await view.reloadColumnGeometry()
|
|
66
|
+
expect(view.loadedChunks).toEqual({})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -11,7 +11,7 @@ describe('WorldView._loadChunks spiral guard', () => {
|
|
|
11
11
|
}
|
|
12
12
|
const view = new WorldView(world, 8, new Vec3(0, 64, 0))
|
|
13
13
|
view.addWaitTime = 0
|
|
14
|
-
view.loadChunk = vi.fn(async () =>
|
|
14
|
+
view.loadChunk = vi.fn(async () => true)
|
|
15
15
|
|
|
16
16
|
const positions = [new Vec3(0, 0, 0)]
|
|
17
17
|
const spiralA = view._loadChunks(positions, new Vec3(0, 64, 0))
|
|
@@ -115,6 +115,8 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
115
115
|
gotPanicLastTime = false
|
|
116
116
|
panicChunksReload = () => {}
|
|
117
117
|
loadedChunks: Record<ChunkPosKey, boolean> = {}
|
|
118
|
+
private columnUnloadEpoch = new Map<string, number>()
|
|
119
|
+
private geometryReload?: Promise<void>
|
|
118
120
|
inLoading = false
|
|
119
121
|
chunkReceiveTimes: number[] = []
|
|
120
122
|
lastChunkReceiveTime = 0
|
|
@@ -201,6 +203,9 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
201
203
|
if (bot?.time?.timeOfDay !== undefined) {
|
|
202
204
|
this.emit('time', bot.time.timeOfDay)
|
|
203
205
|
}
|
|
206
|
+
if (typeof bot?.time?.moonPhase === 'number') {
|
|
207
|
+
this.emit('moonPhase', bot.time.moonPhase)
|
|
208
|
+
}
|
|
204
209
|
if (bot?.entity) {
|
|
205
210
|
this.emit('playerEntity', bot.entity)
|
|
206
211
|
}
|
|
@@ -273,7 +278,9 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
273
278
|
/**
|
|
274
279
|
* Load a chunk at the given position.
|
|
275
280
|
*/
|
|
276
|
-
async loadChunk(pos: { x: number; z: number; y?: number }, isLightUpdate = false, reason = 'spiral'): Promise<
|
|
281
|
+
async loadChunk(pos: { x: number; z: number; y?: number }, isLightUpdate = false, reason = 'spiral'): Promise<boolean> {
|
|
282
|
+
const key = `${pos.x},${pos.z}`
|
|
283
|
+
const epoch = this.columnUnloadEpoch.get(key) ?? 0
|
|
277
284
|
const [botX, botZ] = chunkPos(this.lastPos)
|
|
278
285
|
const dx = Math.abs(botX - Math.floor(pos.x / 16))
|
|
279
286
|
const dz = Math.abs(botZ - Math.floor(pos.z / 16))
|
|
@@ -281,6 +288,14 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
281
288
|
if (dx <= this.viewDistance && dz <= this.viewDistance) {
|
|
282
289
|
const column = await this.world.getColumnAt(pos.y !== undefined ? (pos as Vec3) : new Vec3(pos.x, 0, pos.z))
|
|
283
290
|
|
|
291
|
+
// An asynchronous fetch must not resurrect a truly unloaded column.
|
|
292
|
+
const [currentX, currentZ] = chunkPos(this.lastPos)
|
|
293
|
+
if (
|
|
294
|
+
(this.columnUnloadEpoch.get(key) ?? 0) !== epoch ||
|
|
295
|
+
Math.abs(currentX - Math.floor(pos.x / 16)) > this.viewDistance ||
|
|
296
|
+
Math.abs(currentZ - Math.floor(pos.z / 16)) > this.viewDistance
|
|
297
|
+
)
|
|
298
|
+
return false
|
|
284
299
|
if (column) {
|
|
285
300
|
const chunk = column.toJson()
|
|
286
301
|
const worldConfig: WorldSizeParams = {
|
|
@@ -304,10 +319,41 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
304
319
|
reason,
|
|
305
320
|
time: Date.now()
|
|
306
321
|
})
|
|
322
|
+
return true
|
|
307
323
|
} else if (this.isPlayground) {
|
|
308
324
|
this.emit('markAsLoaded', { x: pos.x, z: pos.z })
|
|
309
325
|
}
|
|
310
326
|
}
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
reloadColumnGeometry(): Promise<void> {
|
|
331
|
+
this.geometryReload ??= this.runColumnGeometryReload().finally(() => {
|
|
332
|
+
this.geometryReload = undefined
|
|
333
|
+
})
|
|
334
|
+
return this.geometryReload
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async runColumnGeometryReload(): Promise<void> {
|
|
338
|
+
const coords = Object.keys(this.loadedChunks)
|
|
339
|
+
.filter(key => this.loadedChunks[key])
|
|
340
|
+
.map(key => key.split(',').map(Number) as [number, number])
|
|
341
|
+
coords.sort(([ax, az], [bx, bz]) => (ax - this.lastPos.x) ** 2 + (az - this.lastPos.z) ** 2 - (bx - this.lastPos.x) ** 2 - (bz - this.lastPos.z) ** 2)
|
|
342
|
+
for (const [x, z] of coords) this.emit('unloadChunk', { x, z, preserveClientLight: true })
|
|
343
|
+
for (const [x, z] of coords) {
|
|
344
|
+
const key = `${x},${z}`
|
|
345
|
+
if (!this.loadedChunks[key]) continue
|
|
346
|
+
const epoch = this.columnUnloadEpoch.get(key) ?? 0
|
|
347
|
+
let reloaded = false
|
|
348
|
+
try {
|
|
349
|
+
reloaded = await this.loadChunk({ x, z }, false, 'geometry-reload')
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.warn(`Failed to reload column geometry ${key}`, error)
|
|
352
|
+
}
|
|
353
|
+
if (!reloaded && this.loadedChunks[key] && (this.columnUnloadEpoch.get(key) ?? 0) === epoch) {
|
|
354
|
+
this.unloadChunk({ x, z })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
311
357
|
}
|
|
312
358
|
|
|
313
359
|
/**
|
|
@@ -335,6 +381,8 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
335
381
|
* Unload a specific chunk.
|
|
336
382
|
*/
|
|
337
383
|
unloadChunk(pos: { x: number; z: number }): void {
|
|
384
|
+
const key = `${pos.x},${pos.z}`
|
|
385
|
+
this.columnUnloadEpoch.set(key, (this.columnUnloadEpoch.get(key) ?? 0) + 1)
|
|
338
386
|
this.emit('unloadChunk', { x: pos.x, z: pos.z })
|
|
339
387
|
delete this.loadedChunks[`${pos.x},${pos.z}`]
|
|
340
388
|
delete this.debugChunksInfo[`${pos.x},${pos.z}`]
|