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.
- 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 +80 -80
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +513 -513
- 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/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/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,47 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Choose which dirty columns a mesher tick processes.
|
|
4
|
+
*
|
|
5
|
+
* Urgent (player-edit) columns go first with no cap. Bulk columns are limited
|
|
6
|
+
* so an incoming edit is not stuck behind a long FIFO of load-wave remeshes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const BULK_COLUMNS_PER_TICK = 4
|
|
10
|
+
|
|
11
|
+
export type ColumnTickSection = { key: string; x: number; y: number; z: number; count: number }
|
|
12
|
+
|
|
13
|
+
export type ColumnTickGroup = { x: number; z: number; sections: ColumnTickSection[] }
|
|
14
|
+
|
|
15
|
+
export function groupDirtySectionsByColumn(dirtySections: Iterable<[string, number]>): ColumnTickGroup[] {
|
|
16
|
+
const groups = new Map<string, ColumnTickGroup>()
|
|
17
|
+
for (const [key, count] of dirtySections) {
|
|
18
|
+
const [sx, sy, sz] = key.split(',').map(v => parseInt(v, 10))
|
|
19
|
+
const colKey = `${sx},${sz}`
|
|
20
|
+
let group = groups.get(colKey)
|
|
21
|
+
if (!group) {
|
|
22
|
+
group = { x: sx, z: sz, sections: [] }
|
|
23
|
+
groups.set(colKey, group)
|
|
24
|
+
}
|
|
25
|
+
group.sections.push({ key, x: sx, y: sy, z: sz, count })
|
|
26
|
+
}
|
|
27
|
+
return [...groups.values()]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function selectColumnGroupsForTick(
|
|
31
|
+
dirtySections: Map<string, number>,
|
|
32
|
+
urgentKeys: Set<string>,
|
|
33
|
+
bulkLimit = BULK_COLUMNS_PER_TICK
|
|
34
|
+
): { selected: ColumnTickGroup[]; remaining: Map<string, number> } {
|
|
35
|
+
const urgent: ColumnTickGroup[] = []
|
|
36
|
+
const bulk: ColumnTickGroup[] = []
|
|
37
|
+
for (const group of groupDirtySectionsByColumn(dirtySections)) {
|
|
38
|
+
if (group.sections.some(section => urgentKeys.has(section.key))) urgent.push(group)
|
|
39
|
+
else bulk.push(group)
|
|
40
|
+
}
|
|
41
|
+
const selected = [...urgent, ...bulk.slice(0, bulkLimit)]
|
|
42
|
+
const remaining = new Map<string, number>()
|
|
43
|
+
for (const group of bulk.slice(bulkLimit)) {
|
|
44
|
+
for (const section of group.sections) remaining.set(section.key, section.count)
|
|
45
|
+
}
|
|
46
|
+
return { selected, remaining }
|
|
47
|
+
}
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
// redundant conversions.
|
|
9
9
|
//
|
|
10
10
|
// Correctness invariants:
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// - Block arrays are patched only by patchConversionBlock. Light arrays may
|
|
12
|
+
// receive the current display overlay before meshing. Other consumers must
|
|
13
|
+
// not mutate cache values. WASM receives the resulting arrays as inputs.
|
|
14
14
|
// - Identity-based key validation: a hit is served only when the stored
|
|
15
15
|
// chunk reference is `===` the live `world.getColumn(x,z)` reference, so a
|
|
16
16
|
// replaced column (via `world.addColumn`) can never accidentally serve
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// skipped (defense in depth).
|
|
19
19
|
// - In-place mutation of an existing column object (e.g. via
|
|
20
20
|
// `world.setBlockStateId` from a `blockUpdate` message) preserves identity
|
|
21
|
-
// but changes content. The `blockUpdate` handler
|
|
22
|
-
//
|
|
21
|
+
// but changes content. The `blockUpdate` handler patches the cached state
|
|
22
|
+
// and count, or invalidates when it cannot patch safely.
|
|
23
23
|
|
|
24
24
|
import type { ChunkConversionResult } from '../bridge/convertChunk'
|
|
25
25
|
|
|
@@ -124,6 +124,17 @@ export function getConversionCacheLimit(): number {
|
|
|
124
124
|
return activeLimit
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** Patch a live column without discarding its converted topology or metadata. */
|
|
128
|
+
export function patchConversionBlock(x: number, z: number, chunkRef: any, blockX: number, blockY: number, blockZ: number, stateId: number): boolean {
|
|
129
|
+
const e = cache.get(keyOf(x, z))
|
|
130
|
+
if (!e || e.chunkRef !== chunkRef || blockY < e.worldMinY || blockY >= e.worldMaxY) return false
|
|
131
|
+
const index = blockX - x + (blockZ - z) * 16 + (blockY - e.worldMinY) * 256
|
|
132
|
+
const old = e.result.blockStates[index]!
|
|
133
|
+
e.result.blockStates[index] = stateId
|
|
134
|
+
e.result.blockCount += Number(stateId !== 0) - Number(old !== 0)
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
export function invalidateConversion(x: number, z: number): boolean {
|
|
128
139
|
return cache.delete(keyOf(x, z))
|
|
129
140
|
}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
|
|
3
|
+
import { dropRawMapChunkOnLightOnlyReload, sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
|
|
4
4
|
|
|
5
5
|
describe('sectionYsForLightColumnDirty', () => {
|
|
6
6
|
it('covers every section in a 256-high overworld column', () => {
|
|
7
7
|
expect(sectionYsForLightColumnDirty(0, 256)).toEqual(Array.from({ length: 16 }, (_, i) => i * 16))
|
|
8
8
|
})
|
|
9
9
|
})
|
|
10
|
+
|
|
11
|
+
describe('dropRawMapChunkOnLightOnlyReload', () => {
|
|
12
|
+
it('deletes only the fused raw entry when the chunk reload is light-only', () => {
|
|
13
|
+
const raw = new Map<string, number>([
|
|
14
|
+
['0,0', 1],
|
|
15
|
+
['16,0', 2]
|
|
16
|
+
])
|
|
17
|
+
expect(dropRawMapChunkOnLightOnlyReload(false, raw, '0,0')).toBe(false)
|
|
18
|
+
expect(raw.has('0,0')).toBe(true)
|
|
19
|
+
expect(dropRawMapChunkOnLightOnlyReload(undefined, raw, '0,0')).toBe(false)
|
|
20
|
+
expect(raw.has('0,0')).toBe(true)
|
|
21
|
+
expect(dropRawMapChunkOnLightOnlyReload(true, raw, '0,0')).toBe(true)
|
|
22
|
+
expect(raw.has('0,0')).toBe(false)
|
|
23
|
+
expect(raw.has('16,0')).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -9,3 +9,13 @@ export function sectionYsForLightColumnDirty(worldMinY: number, worldMaxY: numbe
|
|
|
9
9
|
}
|
|
10
10
|
return ys
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 1.18+ fused meshing prefers the original raw map_chunk. A later light-only
|
|
15
|
+
* column reload must drop that entry so the JSON column walk is used.
|
|
16
|
+
* Normal chunk loads must keep it (`setRawMapChunk` arrives first).
|
|
17
|
+
*/
|
|
18
|
+
export function dropRawMapChunkOnLightOnlyReload(isLightUpdate: boolean | undefined, rawCache: { delete: (key: string) => boolean }, key: string): boolean {
|
|
19
|
+
if (!isLightUpdate) return false
|
|
20
|
+
return rawCache.delete(key)
|
|
21
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
LIGHT_BLOCKS_PER_SECTION,
|
|
5
|
+
createEmptyLightCache,
|
|
6
|
+
displayLightColumn,
|
|
7
|
+
isLightSectionPresent,
|
|
8
|
+
mergeUpdateLight,
|
|
9
|
+
worldSectionMaskBit,
|
|
10
|
+
type ParsedUpdateLight,
|
|
11
|
+
type UpdateLightColumnCache
|
|
12
|
+
} from './mesherWasmLightMerge'
|
|
13
|
+
|
|
14
|
+
const NUM_SECTIONS = 16
|
|
15
|
+
const WORLD_BYTES = NUM_SECTIONS * LIGHT_BLOCKS_PER_SECTION
|
|
16
|
+
|
|
17
|
+
function maskWithBits(...bits: number[]): Uint32Array {
|
|
18
|
+
const out = new Uint32Array(2)
|
|
19
|
+
for (const bit of bits) {
|
|
20
|
+
const bitInLong = bit & 63
|
|
21
|
+
if (bitInLong < 32) out[0] |= 1 << bitInLong
|
|
22
|
+
else out[1] |= 1 << (bitInLong - 32)
|
|
23
|
+
}
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function emptyParsed(overrides: Partial<ParsedUpdateLight> = {}): ParsedUpdateLight {
|
|
28
|
+
return {
|
|
29
|
+
x: 0,
|
|
30
|
+
z: 0,
|
|
31
|
+
trustEdges: false,
|
|
32
|
+
numSections: NUM_SECTIONS,
|
|
33
|
+
skyLight: new Uint8Array(WORLD_BYTES),
|
|
34
|
+
blockLight: new Uint8Array(WORLD_BYTES),
|
|
35
|
+
skyLightMask: new Uint32Array(2),
|
|
36
|
+
emptySkyLightMask: new Uint32Array(2),
|
|
37
|
+
blockLightMask: new Uint32Array(2),
|
|
38
|
+
emptyBlockLightMask: new Uint32Array(2),
|
|
39
|
+
...overrides
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeWorldSection(column: Uint8Array, sectionIndex: number, value: number) {
|
|
44
|
+
const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
|
|
45
|
+
column.fill(value, start, start + LIGHT_BLOCKS_PER_SECTION)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sectionSlice(column: Uint8Array, sectionIndex: number): Uint8Array {
|
|
49
|
+
const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
|
|
50
|
+
return column.subarray(start, start + LIGHT_BLOCKS_PER_SECTION)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sectionAll(column: Uint8Array, sectionIndex: number, value: number): boolean {
|
|
54
|
+
return sectionSlice(column, sectionIndex).every(v => v === value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('mergeUpdateLight', () => {
|
|
58
|
+
it('does not bake omitted sky as authoritative 15 on the first packet', () => {
|
|
59
|
+
const skyLight = new Uint8Array(WORLD_BYTES)
|
|
60
|
+
writeWorldSection(skyLight, 1, 3)
|
|
61
|
+
const cache = mergeUpdateLight(
|
|
62
|
+
undefined,
|
|
63
|
+
emptyParsed({
|
|
64
|
+
skyLight,
|
|
65
|
+
skyLightMask: maskWithBits(worldSectionMaskBit(1))
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(1))).toBe(true)
|
|
70
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(false)
|
|
71
|
+
expect(sectionAll(cache.skyLight, 1, 3)).toBe(true)
|
|
72
|
+
expect(sectionAll(cache.skyLight, 0, 15)).toBe(false)
|
|
73
|
+
expect(displayLightColumn(cache, 'sky', 15)[0]).toBe(15)
|
|
74
|
+
expect(displayLightColumn(cache, 'sky')[LIGHT_BLOCKS_PER_SECTION]).toBe(3)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('preserves an omitted neighbor section across a repeated partial delta', () => {
|
|
78
|
+
const firstSky = new Uint8Array(WORLD_BYTES)
|
|
79
|
+
writeWorldSection(firstSky, 0, 7)
|
|
80
|
+
let cache = mergeUpdateLight(
|
|
81
|
+
undefined,
|
|
82
|
+
emptyParsed({
|
|
83
|
+
skyLight: firstSky,
|
|
84
|
+
skyLightMask: maskWithBits(worldSectionMaskBit(0))
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const secondSky = new Uint8Array(WORLD_BYTES)
|
|
89
|
+
writeWorldSection(secondSky, 1, 4)
|
|
90
|
+
cache = mergeUpdateLight(
|
|
91
|
+
cache,
|
|
92
|
+
emptyParsed({
|
|
93
|
+
skyLight: secondSky,
|
|
94
|
+
skyLightMask: maskWithBits(worldSectionMaskBit(1))
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
expect(sectionAll(cache.skyLight, 0, 7)).toBe(true)
|
|
99
|
+
expect(sectionAll(cache.skyLight, 1, 4)).toBe(true)
|
|
100
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(true)
|
|
101
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(1))).toBe(true)
|
|
102
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(2))).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('zeros a section when the empty-bit is set', () => {
|
|
106
|
+
const firstSky = new Uint8Array(WORLD_BYTES)
|
|
107
|
+
writeWorldSection(firstSky, 0, 7)
|
|
108
|
+
let cache = mergeUpdateLight(
|
|
109
|
+
undefined,
|
|
110
|
+
emptyParsed({
|
|
111
|
+
skyLight: firstSky,
|
|
112
|
+
skyLightMask: maskWithBits(worldSectionMaskBit(0))
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
cache = mergeUpdateLight(
|
|
117
|
+
cache,
|
|
118
|
+
emptyParsed({
|
|
119
|
+
emptySkyLightMask: maskWithBits(worldSectionMaskBit(0))
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(true)
|
|
124
|
+
expect(sectionAll(cache.skyLight, 0, 0)).toBe(true)
|
|
125
|
+
expect(displayLightColumn(cache, 'sky', 15)[0]).toBe(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('updates sky and block independently', () => {
|
|
129
|
+
const skyLight = new Uint8Array(WORLD_BYTES)
|
|
130
|
+
const blockLight = new Uint8Array(WORLD_BYTES)
|
|
131
|
+
writeWorldSection(skyLight, 0, 12)
|
|
132
|
+
writeWorldSection(blockLight, 0, 5)
|
|
133
|
+
let cache = mergeUpdateLight(
|
|
134
|
+
undefined,
|
|
135
|
+
emptyParsed({
|
|
136
|
+
skyLight,
|
|
137
|
+
blockLight,
|
|
138
|
+
skyLightMask: maskWithBits(worldSectionMaskBit(0)),
|
|
139
|
+
blockLightMask: maskWithBits(worldSectionMaskBit(0))
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
cache = mergeUpdateLight(
|
|
144
|
+
cache,
|
|
145
|
+
emptyParsed({
|
|
146
|
+
emptyBlockLightMask: maskWithBits(worldSectionMaskBit(0))
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
expect(sectionAll(cache.skyLight, 0, 12)).toBe(true)
|
|
151
|
+
expect(sectionAll(cache.blockLight, 0, 0)).toBe(true)
|
|
152
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(true)
|
|
153
|
+
expect(isLightSectionPresent(cache.blockPresent, worldSectionMaskBit(0))).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('records padding bits without writing them into the world column', () => {
|
|
157
|
+
const skyBelow = new Uint8Array(LIGHT_BLOCKS_PER_SECTION).fill(9)
|
|
158
|
+
const cache = mergeUpdateLight(
|
|
159
|
+
undefined,
|
|
160
|
+
emptyParsed({
|
|
161
|
+
skyBelow,
|
|
162
|
+
skyLightMask: maskWithBits(0)
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
expect(isLightSectionPresent(cache.skyPresent, 0)).toBe(true)
|
|
167
|
+
expect(cache.skyBelow).toBeDefined()
|
|
168
|
+
expect(cache.skyBelow!.every(v => v === 9)).toBe(true)
|
|
169
|
+
expect(sectionAll(cache.skyLight, 0, 0)).toBe(true)
|
|
170
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(false)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('applies captured 1.17.1 empty-block bits without seeding omitted sky as 15', () => {
|
|
174
|
+
const cache = mergeUpdateLight(
|
|
175
|
+
undefined,
|
|
176
|
+
emptyParsed({
|
|
177
|
+
trustEdges: true,
|
|
178
|
+
emptyBlockLightMask: maskWithBits(1, 2, 3, 4, 5)
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
expect(cache.trustEdges).toBe(true)
|
|
183
|
+
for (let s = 0; s <= 4; s++) {
|
|
184
|
+
expect(isLightSectionPresent(cache.blockPresent, worldSectionMaskBit(s))).toBe(true)
|
|
185
|
+
expect(sectionAll(cache.blockLight, s, 0)).toBe(true)
|
|
186
|
+
}
|
|
187
|
+
expect(isLightSectionPresent(cache.skyPresent, worldSectionMaskBit(0))).toBe(false)
|
|
188
|
+
expect(sectionAll(cache.skyLight, 0, 15)).toBe(false)
|
|
189
|
+
expect(displayLightColumn(cache, 'sky', 15)[0]).toBe(15)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('keeps trustEdges from the latest packet without changing merge rules', () => {
|
|
193
|
+
const cache = mergeUpdateLight(createEmptyLightCache(NUM_SECTIONS), emptyParsed({ trustEdges: true }))
|
|
194
|
+
expect(cache.trustEdges).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('displayLightColumn', () => {
|
|
199
|
+
it('fills only absent world sections and leaves explicit zeros', () => {
|
|
200
|
+
const cache: UpdateLightColumnCache = createEmptyLightCache(NUM_SECTIONS)
|
|
201
|
+
writeWorldSection(cache.blockLight, 2, 0)
|
|
202
|
+
cache.blockPresent = maskWithBits(worldSectionMaskBit(2))
|
|
203
|
+
|
|
204
|
+
const displayed = displayLightColumn(cache, 'block', 0)
|
|
205
|
+
expect(sectionAll(displayed, 2, 0)).toBe(true)
|
|
206
|
+
expect(sectionAll(displayed, 1, 0)).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/** 16³ unpacked light values per section. */
|
|
3
|
+
export const LIGHT_BLOCKS_PER_SECTION = 4096
|
|
4
|
+
|
|
5
|
+
export type ParsedUpdateLight = {
|
|
6
|
+
x: number
|
|
7
|
+
z: number
|
|
8
|
+
trustEdges: boolean
|
|
9
|
+
numSections: number
|
|
10
|
+
skyLight: Uint8Array
|
|
11
|
+
blockLight: Uint8Array
|
|
12
|
+
skyLightMask: Uint32Array
|
|
13
|
+
emptySkyLightMask: Uint32Array
|
|
14
|
+
blockLightMask: Uint32Array
|
|
15
|
+
emptyBlockLightMask: Uint32Array
|
|
16
|
+
skyBelow?: Uint8Array
|
|
17
|
+
skyAbove?: Uint8Array
|
|
18
|
+
blockBelow?: Uint8Array
|
|
19
|
+
blockAbove?: Uint8Array
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type UpdateLightColumnCache = {
|
|
23
|
+
numSections: number
|
|
24
|
+
trustEdges: boolean
|
|
25
|
+
skyLight: Uint8Array
|
|
26
|
+
blockLight: Uint8Array
|
|
27
|
+
skyPresent: Uint32Array
|
|
28
|
+
blockPresent: Uint32Array
|
|
29
|
+
skyBelow?: Uint8Array
|
|
30
|
+
skyAbove?: Uint8Array
|
|
31
|
+
blockBelow?: Uint8Array
|
|
32
|
+
blockAbove?: Uint8Array
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function worldSectionMaskBit(sectionIndex: number): number {
|
|
36
|
+
return sectionIndex + 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function lightMaskWordCount(numSections: number): number {
|
|
40
|
+
const bits = numSections + 2
|
|
41
|
+
return Math.max(2, Math.ceil(bits / 64) * 2)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function maskBitGet(mask: Uint32Array, i: number): boolean {
|
|
45
|
+
const longIdx = i >> 6
|
|
46
|
+
const bitInLong = i & 63
|
|
47
|
+
const pair = longIdx * 2
|
|
48
|
+
if (pair + 1 >= mask.length) return false
|
|
49
|
+
const val = bitInLong < 32 ? mask[pair]! : mask[pair + 1]!
|
|
50
|
+
const bit = bitInLong & 31
|
|
51
|
+
return ((val >>> bit) & 1) === 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function maskBitSet(mask: Uint32Array, i: number): void {
|
|
55
|
+
const longIdx = i >> 6
|
|
56
|
+
const bitInLong = i & 63
|
|
57
|
+
const pair = longIdx * 2
|
|
58
|
+
if (pair + 1 >= mask.length) return
|
|
59
|
+
if (bitInLong < 32) mask[pair] = (mask[pair]! | (1 << bitInLong)) >>> 0
|
|
60
|
+
else mask[pair + 1] = (mask[pair + 1]! | (1 << (bitInLong - 32))) >>> 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isLightSectionPresent(present: Uint32Array, maskBit: number): boolean {
|
|
64
|
+
return maskBitGet(present, maskBit)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createEmptyLightCache(numSections: number): UpdateLightColumnCache {
|
|
68
|
+
const worldBytes = numSections * LIGHT_BLOCKS_PER_SECTION
|
|
69
|
+
const words = lightMaskWordCount(numSections)
|
|
70
|
+
return {
|
|
71
|
+
numSections,
|
|
72
|
+
trustEdges: false,
|
|
73
|
+
skyLight: new Uint8Array(worldBytes),
|
|
74
|
+
blockLight: new Uint8Array(worldBytes),
|
|
75
|
+
skyPresent: new Uint32Array(words),
|
|
76
|
+
blockPresent: new Uint32Array(words)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function copySection(dest: Uint8Array, src: Uint8Array, sectionIndex: number, value?: number) {
|
|
81
|
+
const start = sectionIndex * LIGHT_BLOCKS_PER_SECTION
|
|
82
|
+
const end = start + LIGHT_BLOCKS_PER_SECTION
|
|
83
|
+
if (value !== undefined) {
|
|
84
|
+
dest.fill(value, start, end)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (src.length < end) return
|
|
88
|
+
dest.set(src.subarray(start, end), start)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergePadding(hasData: boolean, incoming?: Uint8Array): Uint8Array {
|
|
92
|
+
if (hasData && incoming && incoming.length === LIGHT_BLOCKS_PER_SECTION) {
|
|
93
|
+
return new Uint8Array(incoming)
|
|
94
|
+
}
|
|
95
|
+
return new Uint8Array(LIGHT_BLOCKS_PER_SECTION)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mergeChannel(
|
|
99
|
+
destValues: Uint8Array,
|
|
100
|
+
destPresent: Uint32Array,
|
|
101
|
+
srcValues: Uint8Array,
|
|
102
|
+
dataMask: Uint32Array,
|
|
103
|
+
emptyMask: Uint32Array,
|
|
104
|
+
numSections: number,
|
|
105
|
+
destBelow: Uint8Array | undefined,
|
|
106
|
+
destAbove: Uint8Array | undefined,
|
|
107
|
+
incomingBelow: Uint8Array | undefined,
|
|
108
|
+
incomingAbove: Uint8Array | undefined
|
|
109
|
+
): { below?: Uint8Array; above?: Uint8Array } {
|
|
110
|
+
let below = destBelow
|
|
111
|
+
let above = destAbove
|
|
112
|
+
const totalBits = numSections + 2
|
|
113
|
+
for (let bit = 0; bit < totalBits; bit++) {
|
|
114
|
+
const hasData = maskBitGet(dataMask, bit)
|
|
115
|
+
const hasEmpty = maskBitGet(emptyMask, bit)
|
|
116
|
+
if (!hasData && !hasEmpty) continue
|
|
117
|
+
|
|
118
|
+
if (bit === 0) {
|
|
119
|
+
below = mergePadding(hasData, incomingBelow)
|
|
120
|
+
maskBitSet(destPresent, 0)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (bit === numSections + 1) {
|
|
124
|
+
above = mergePadding(hasData, incomingAbove)
|
|
125
|
+
maskBitSet(destPresent, bit)
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sectionIndex = bit - 1
|
|
130
|
+
if (hasData) copySection(destValues, srcValues, sectionIndex)
|
|
131
|
+
else copySection(destValues, srcValues, sectionIndex, 0)
|
|
132
|
+
maskBitSet(destPresent, bit)
|
|
133
|
+
}
|
|
134
|
+
return { below, above }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function mergeUpdateLight(cache: UpdateLightColumnCache | undefined, parsed: ParsedUpdateLight): UpdateLightColumnCache {
|
|
138
|
+
const next = cache ? cloneLightCache(cache) : createEmptyLightCache(parsed.numSections)
|
|
139
|
+
next.trustEdges = parsed.trustEdges
|
|
140
|
+
next.numSections = parsed.numSections
|
|
141
|
+
|
|
142
|
+
const skyPad = mergeChannel(
|
|
143
|
+
next.skyLight,
|
|
144
|
+
next.skyPresent,
|
|
145
|
+
parsed.skyLight,
|
|
146
|
+
parsed.skyLightMask,
|
|
147
|
+
parsed.emptySkyLightMask,
|
|
148
|
+
parsed.numSections,
|
|
149
|
+
next.skyBelow,
|
|
150
|
+
next.skyAbove,
|
|
151
|
+
parsed.skyBelow,
|
|
152
|
+
parsed.skyAbove
|
|
153
|
+
)
|
|
154
|
+
next.skyBelow = skyPad.below
|
|
155
|
+
next.skyAbove = skyPad.above
|
|
156
|
+
|
|
157
|
+
const blockPad = mergeChannel(
|
|
158
|
+
next.blockLight,
|
|
159
|
+
next.blockPresent,
|
|
160
|
+
parsed.blockLight,
|
|
161
|
+
parsed.blockLightMask,
|
|
162
|
+
parsed.emptyBlockLightMask,
|
|
163
|
+
parsed.numSections,
|
|
164
|
+
next.blockBelow,
|
|
165
|
+
next.blockAbove,
|
|
166
|
+
parsed.blockBelow,
|
|
167
|
+
parsed.blockAbove
|
|
168
|
+
)
|
|
169
|
+
next.blockBelow = blockPad.below
|
|
170
|
+
next.blockAbove = blockPad.above
|
|
171
|
+
|
|
172
|
+
return next
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function cloneLightCache(cache: UpdateLightColumnCache): UpdateLightColumnCache {
|
|
176
|
+
return {
|
|
177
|
+
numSections: cache.numSections,
|
|
178
|
+
trustEdges: cache.trustEdges,
|
|
179
|
+
skyLight: new Uint8Array(cache.skyLight),
|
|
180
|
+
blockLight: new Uint8Array(cache.blockLight),
|
|
181
|
+
skyPresent: new Uint32Array(cache.skyPresent),
|
|
182
|
+
blockPresent: new Uint32Array(cache.blockPresent),
|
|
183
|
+
skyBelow: cache.skyBelow ? new Uint8Array(cache.skyBelow) : undefined,
|
|
184
|
+
skyAbove: cache.skyAbove ? new Uint8Array(cache.skyAbove) : undefined,
|
|
185
|
+
blockBelow: cache.blockBelow ? new Uint8Array(cache.blockBelow) : undefined,
|
|
186
|
+
blockAbove: cache.blockAbove ? new Uint8Array(cache.blockAbove) : undefined
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function displayLightColumn(cache: UpdateLightColumnCache, channel: 'sky' | 'block', fill = channel === 'sky' ? 15 : 0): Uint8Array {
|
|
191
|
+
const out = new Uint8Array(cache.numSections * LIGHT_BLOCKS_PER_SECTION)
|
|
192
|
+
overlayDisplayLight(cache, channel, out, fill)
|
|
193
|
+
return out
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Overlay the current display into a reusable conversion buffer. */
|
|
197
|
+
export function overlayDisplayLight(cache: UpdateLightColumnCache, channel: 'sky' | 'block', out: Uint8Array, fill = channel === 'sky' ? 15 : 0): void {
|
|
198
|
+
const src = channel === 'sky' ? cache.skyLight : cache.blockLight
|
|
199
|
+
const present = channel === 'sky' ? cache.skyPresent : cache.blockPresent
|
|
200
|
+
for (let s = 0; s < cache.numSections; s++) {
|
|
201
|
+
const start = s * LIGHT_BLOCKS_PER_SECTION
|
|
202
|
+
const end = start + LIGHT_BLOCKS_PER_SECTION
|
|
203
|
+
if (maskBitGet(present, worldSectionMaskBit(s))) {
|
|
204
|
+
out.set(src.subarray(start, end), start)
|
|
205
|
+
} else {
|
|
206
|
+
out.fill(fill, start, end)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function parsedUpdateLightFromWasm(parsed: any, numSections: number): ParsedUpdateLight {
|
|
212
|
+
return {
|
|
213
|
+
x: parsed.x as number,
|
|
214
|
+
z: parsed.z as number,
|
|
215
|
+
trustEdges: Boolean(parsed.trustEdges),
|
|
216
|
+
numSections,
|
|
217
|
+
skyLight: parsed.skyLight as Uint8Array,
|
|
218
|
+
blockLight: parsed.blockLight as Uint8Array,
|
|
219
|
+
skyLightMask: (parsed.skyLightMask as Uint32Array) ?? new Uint32Array(2),
|
|
220
|
+
emptySkyLightMask: (parsed.emptySkyLightMask as Uint32Array) ?? new Uint32Array(2),
|
|
221
|
+
blockLightMask: (parsed.blockLightMask as Uint32Array) ?? new Uint32Array(2),
|
|
222
|
+
emptyBlockLightMask: (parsed.emptyBlockLightMask as Uint32Array) ?? new Uint32Array(2),
|
|
223
|
+
skyBelow: parsed.skyBelow as Uint8Array | undefined,
|
|
224
|
+
skyAbove: parsed.skyAbove as Uint8Array | undefined,
|
|
225
|
+
blockBelow: parsed.blockBelow as Uint8Array | undefined,
|
|
226
|
+
blockAbove: parsed.blockAbove as Uint8Array | undefined
|
|
227
|
+
}
|
|
228
|
+
}
|