minecraft-renderer 0.1.61 → 0.1.63
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/minecraft-renderer.js +58 -58
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +400 -400
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +3 -3
- package/src/graphicsBackend/types.ts +3 -3
- package/src/lib/bindAbortableListener.test.ts +65 -0
- package/src/lib/bindAbortableListener.ts +41 -0
- package/src/lib/workerProxy.ts +238 -118
- package/src/lib/workerSyncOps.test.ts +154 -0
- package/src/lib/worldrendererCommon.ts +70 -48
- package/src/three/documentRenderer.ts +1 -1
- package/src/three/entities.ts +6 -1
- package/src/three/graphicsBackendBase.ts +18 -8
- package/src/three/modules/rain.ts +1 -1
- package/src/three/shaders/cubeBlockShader.ts +2 -2
- package/src/three/worldRendererThree.ts +2 -1
- package/src/worldView/worldView.ts +39 -8
- package/src/worldView/worldViewWorkerBridge.test.ts +59 -0
- package/src/lib/workerProxy.restore.test.ts +0 -29
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
3
|
+
import { proxy, subscribe } from 'valtio'
|
|
4
|
+
import { Vec3 } from 'vec3'
|
|
5
|
+
import {
|
|
6
|
+
applySyncOps,
|
|
7
|
+
getWorkerSyncStatsForTest,
|
|
8
|
+
resetWorkerSyncStatsForTest,
|
|
9
|
+
sendWorkerSyncOps,
|
|
10
|
+
setByPath,
|
|
11
|
+
type WireSyncOp,
|
|
12
|
+
} from './workerProxy'
|
|
13
|
+
import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
|
|
14
|
+
|
|
15
|
+
const fakeWorker = () => {
|
|
16
|
+
const listeners: Array<(event: { data: any }) => void> = []
|
|
17
|
+
return {
|
|
18
|
+
postMessage: vi.fn((data: any) => {
|
|
19
|
+
for (const listener of listeners) {
|
|
20
|
+
listener({ data })
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
addEventListener: vi.fn((_type: string, listener: (event: { data: any }) => void) => {
|
|
24
|
+
listeners.push(listener)
|
|
25
|
+
}),
|
|
26
|
+
removeEventListener: vi.fn(),
|
|
27
|
+
} as unknown as Worker
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const makeRendererState = () => proxy({
|
|
31
|
+
world: {
|
|
32
|
+
chunksLoaded: {} as Record<string, true>,
|
|
33
|
+
heightmaps: {} as Record<string, Int16Array>,
|
|
34
|
+
allChunksLoaded: false,
|
|
35
|
+
mesherWork: false,
|
|
36
|
+
instabilityFactors: defaultPerformanceInstabilityFactors(),
|
|
37
|
+
intersectMedia: null as null | object,
|
|
38
|
+
},
|
|
39
|
+
renderer: '...',
|
|
40
|
+
preventEscapeMenu: false,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('workerSyncOps', () => {
|
|
44
|
+
it('set op round-trips mesherWork', () => {
|
|
45
|
+
const source = makeRendererState()
|
|
46
|
+
const target = makeRendererState()
|
|
47
|
+
const ops: WireSyncOp[] = [{ kind: 'set', path: ['world', 'mesherWork'], value: true }]
|
|
48
|
+
applySyncOps(target, ops, fakeWorker())
|
|
49
|
+
expect(target.world.mesherWork).toBe(true)
|
|
50
|
+
expect(source.world.mesherWork).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('top-level set round-trips renderer', () => {
|
|
54
|
+
const target = makeRendererState()
|
|
55
|
+
applySyncOps(target, [{ kind: 'set', path: ['renderer'], value: 'WebGL2 r123' }], fakeWorker())
|
|
56
|
+
expect(target.renderer).toBe('WebGL2 r123')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('nested set creates chunksLoaded key on receiver', () => {
|
|
60
|
+
const target = makeRendererState()
|
|
61
|
+
applySyncOps(target, [{ kind: 'set', path: ['world', 'chunksLoaded', '1,2'], value: true }], fakeWorker())
|
|
62
|
+
expect(target.world.chunksLoaded['1,2']).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('delete op removes heightmap key on receiver', () => {
|
|
66
|
+
const target = makeRendererState()
|
|
67
|
+
target.world.heightmaps['1,2'] = new Int16Array(256)
|
|
68
|
+
applySyncOps(target, [{ kind: 'delete', path: ['world', 'heightmaps', '1,2'] }], fakeWorker())
|
|
69
|
+
expect(target.world.heightmaps['1,2']).toBeUndefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('Int16Array value survives copy without neutering sender buffer', () => {
|
|
73
|
+
const source = new Int16Array([1, 2, 3])
|
|
74
|
+
const sender = makeRendererState()
|
|
75
|
+
sender.world.heightmaps['0,0'] = source
|
|
76
|
+
const receiver = makeRendererState()
|
|
77
|
+
const buf = sender.world.heightmaps['0,0']!
|
|
78
|
+
applySyncOps(receiver, [{
|
|
79
|
+
kind: 'set',
|
|
80
|
+
path: ['world', 'heightmaps', '0,0'],
|
|
81
|
+
value: new Int16Array(buf),
|
|
82
|
+
}], fakeWorker())
|
|
83
|
+
expect([...receiver.world.heightmaps['0,0']!]).toEqual([1, 2, 3])
|
|
84
|
+
expect(sender.world.heightmaps['0,0']![0]).toBe(1)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('Vec3 value survives via restorer', () => {
|
|
88
|
+
const target = makeRendererState() as any
|
|
89
|
+
const vec = new Vec3(1, 2, 3)
|
|
90
|
+
applySyncOps(target, [{
|
|
91
|
+
kind: 'set',
|
|
92
|
+
path: ['world', 'intersectMedia'],
|
|
93
|
+
value: { pos: { x: 1, y: 2, z: 3, __restorer: 'Vec3' } },
|
|
94
|
+
}], fakeWorker())
|
|
95
|
+
expect(target.world.intersectMedia.pos).toBeInstanceOf(Vec3)
|
|
96
|
+
expect(target.world.intersectMedia.pos.x).toBe(1)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('batched ops in one tick produce one message with multiple ops', async () => {
|
|
100
|
+
const worker = fakeWorker()
|
|
101
|
+
const syncId = 'test-sync'
|
|
102
|
+
const source = makeRendererState()
|
|
103
|
+
let messageCount = 0
|
|
104
|
+
;(worker.postMessage as ReturnType<typeof vi.fn>).mockImplementation((data: any) => {
|
|
105
|
+
messageCount++
|
|
106
|
+
expect(data.ops.length).toBeGreaterThanOrEqual(2)
|
|
107
|
+
})
|
|
108
|
+
await new Promise<void>((resolve) => {
|
|
109
|
+
subscribe(source, (ops) => {
|
|
110
|
+
sendWorkerSyncOps(syncId, ops, worker, 'toWorker', 'test')
|
|
111
|
+
resolve()
|
|
112
|
+
})
|
|
113
|
+
source.world.mesherWork = true
|
|
114
|
+
source.renderer = 'batch'
|
|
115
|
+
})
|
|
116
|
+
expect(messageCount).toBe(1)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('debugWorkerSyncStats', () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
resetWorkerSyncStatsForTest()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
resetWorkerSyncStatsForTest()
|
|
126
|
+
vi.useRealTimers()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('one postMessage increments toWorker by 1 regardless of op count', () => {
|
|
130
|
+
const worker = fakeWorker()
|
|
131
|
+
sendWorkerSyncOps('id', [
|
|
132
|
+
['set', ['world', 'mesherWork'], true, false],
|
|
133
|
+
['set', ['renderer'], 'x', '...'],
|
|
134
|
+
], worker, 'toWorker', 'test')
|
|
135
|
+
expect(worker.postMessage).toHaveBeenCalledTimes(1)
|
|
136
|
+
expect(getWorkerSyncStatsForTest().toWorker).toBe(1)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('applySyncOps with fromWorker counts one receive per message', () => {
|
|
140
|
+
const target = makeRendererState()
|
|
141
|
+
applySyncOps(target, [
|
|
142
|
+
{ kind: 'set', path: ['world', 'mesherWork'], value: true },
|
|
143
|
+
{ kind: 'set', path: ['renderer'], value: 'y' },
|
|
144
|
+
], fakeWorker(), 'fromWorker')
|
|
145
|
+
expect(getWorkerSyncStatsForTest().fromWorker).toBe(1)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('setByPath handles length-1 path', () => {
|
|
150
|
+
const target = { renderer: 'old' }
|
|
151
|
+
setByPath(target, ['renderer'], 'new')
|
|
152
|
+
expect(target.renderer).toBe('new')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -19,6 +19,7 @@ import { MesherLogReader } from './mesherlogReader'
|
|
|
19
19
|
import { setSkinsConfig } from './utils/skins'
|
|
20
20
|
import { calculateSkyLightSimple } from './skyLight'
|
|
21
21
|
import { WorldViewWorker } from '../worldView'
|
|
22
|
+
import { bindAbortableEmitterListener, bindAbortableListener } from './bindAbortableListener'
|
|
22
23
|
import { generateSpiralMatrix } from './spiral'
|
|
23
24
|
import { PlayerStateReactive } from '../playerState/playerState'
|
|
24
25
|
import { IndexedData } from 'minecraft-data'
|
|
@@ -170,6 +171,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
170
171
|
stopMesherMessagesProcessing = false
|
|
171
172
|
|
|
172
173
|
abortController = new AbortController()
|
|
174
|
+
private valtioUnsubs: Array<() => void> = []
|
|
173
175
|
lastRendered = 0
|
|
174
176
|
renderingActive = true
|
|
175
177
|
geometryReceiveCountPerSec = 0
|
|
@@ -257,10 +259,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
257
259
|
})()
|
|
258
260
|
])
|
|
259
261
|
|
|
260
|
-
|
|
262
|
+
const onAssetsTexturesUpdated = async () => {
|
|
261
263
|
if (!this.active) return
|
|
262
264
|
await this.updateAssetsData()
|
|
263
|
-
}
|
|
265
|
+
}
|
|
266
|
+
bindAbortableEmitterListener(
|
|
267
|
+
this.resourcesManager,
|
|
268
|
+
'assetsTexturesUpdated',
|
|
269
|
+
onAssetsTexturesUpdated,
|
|
270
|
+
this.abortController.signal
|
|
271
|
+
)
|
|
264
272
|
|
|
265
273
|
this.watchReactivePlayerState()
|
|
266
274
|
this.watchReactiveConfig()
|
|
@@ -338,18 +346,24 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
338
346
|
}
|
|
339
347
|
|
|
340
348
|
watchReactivePlayerState() {
|
|
341
|
-
this.
|
|
342
|
-
this.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
349
|
+
this.valtioUnsubs.push(
|
|
350
|
+
this.onReactivePlayerStateUpdated('backgroundColor', (value) => {
|
|
351
|
+
this.changeBackgroundColor(value)
|
|
352
|
+
})
|
|
353
|
+
)
|
|
354
|
+
this.valtioUnsubs.push(
|
|
355
|
+
this.onReactivePlayerStateUpdated('cardinalLight', (value) => {
|
|
356
|
+
this.changeCardinalLight(value)
|
|
357
|
+
})
|
|
358
|
+
)
|
|
347
359
|
}
|
|
348
360
|
|
|
349
361
|
watchReactiveConfig() {
|
|
350
|
-
this.
|
|
351
|
-
|
|
352
|
-
|
|
362
|
+
this.valtioUnsubs.push(
|
|
363
|
+
this.onReactiveConfigUpdated('fetchPlayerSkins', (value) => {
|
|
364
|
+
setSkinsConfig({ apiEnabled: value })
|
|
365
|
+
})
|
|
366
|
+
)
|
|
353
367
|
}
|
|
354
368
|
|
|
355
369
|
async processMessageQueue(source: string) {
|
|
@@ -432,7 +446,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
432
446
|
// CHUNK FINISHED
|
|
433
447
|
this.finishedChunks[chunkKey] = true
|
|
434
448
|
const CHUNK_SIZE = 16
|
|
435
|
-
|
|
449
|
+
const gridKey = `${Math.floor(chunkCoords[0] / CHUNK_SIZE)},${Math.floor(chunkCoords[2] / CHUNK_SIZE)}`
|
|
450
|
+
this.reactiveState.world.chunksLoaded[gridKey] = true
|
|
436
451
|
this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0]},${chunkCoords[2]}`)
|
|
437
452
|
this.checkAllFinished()
|
|
438
453
|
// merge highest blocks by sections into highest blocks by chunks
|
|
@@ -513,7 +528,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
513
528
|
}
|
|
514
529
|
|
|
515
530
|
if (data.type === 'heightmap') {
|
|
516
|
-
|
|
531
|
+
const heightmap = new Int16Array(data.heightmap)
|
|
532
|
+
this.reactiveState.world.heightmaps[data.key] = heightmap
|
|
517
533
|
}
|
|
518
534
|
}
|
|
519
535
|
|
|
@@ -684,7 +700,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
684
700
|
|
|
685
701
|
updateChunksStats() {
|
|
686
702
|
const loadedChunks = Object.keys(this.finishedChunks)
|
|
687
|
-
this.displayOptions.nonReactiveState.world.
|
|
703
|
+
this.displayOptions.nonReactiveState.world.chunksLoadedCount = loadedChunks.length
|
|
688
704
|
this.displayOptions.nonReactiveState.world.chunksTotalNumber = this.chunksLength
|
|
689
705
|
this.reactiveState.world.allChunksLoaded = this.allChunksFinished
|
|
690
706
|
|
|
@@ -787,7 +803,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
787
803
|
delete this.finishedSections[`${x},${y},${z}`]
|
|
788
804
|
}
|
|
789
805
|
this.highestBlocksByChunks.delete(`${x},${z}`)
|
|
790
|
-
|
|
806
|
+
const heightmapKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
|
|
807
|
+
delete this.reactiveState.world.heightmaps[heightmapKey]
|
|
791
808
|
|
|
792
809
|
this.updateChunksStats()
|
|
793
810
|
|
|
@@ -826,22 +843,23 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
826
843
|
|
|
827
844
|
connect(worldView: WorldViewWorker) {
|
|
828
845
|
const worldEmitter = worldView
|
|
846
|
+
const signal = this.abortController.signal
|
|
829
847
|
|
|
830
|
-
worldEmitter
|
|
848
|
+
bindAbortableListener(worldEmitter, 'entity', (e) => {
|
|
831
849
|
this.updateEntity(e, false)
|
|
832
|
-
})
|
|
833
|
-
worldEmitter
|
|
850
|
+
}, signal)
|
|
851
|
+
bindAbortableListener(worldEmitter, 'entityMoved', (e) => {
|
|
834
852
|
this.updateEntity(e, true)
|
|
835
|
-
})
|
|
836
|
-
worldEmitter
|
|
853
|
+
}, signal)
|
|
854
|
+
bindAbortableListener(worldEmitter, 'playerEntity', (e) => {
|
|
837
855
|
this.updatePlayerEntity?.(e)
|
|
838
|
-
})
|
|
856
|
+
}, signal)
|
|
839
857
|
|
|
840
858
|
let currentLoadChunkBatch = null as {
|
|
841
859
|
timeout
|
|
842
860
|
data
|
|
843
861
|
} | null
|
|
844
|
-
worldEmitter
|
|
862
|
+
bindAbortableListener(worldEmitter, 'loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
|
|
845
863
|
this.worldSizeParams = worldConfig
|
|
846
864
|
this.queuedChunks.add(`${x},${z}`)
|
|
847
865
|
const args = [x, z, chunk, isLightUpdate]
|
|
@@ -863,45 +881,44 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
863
881
|
}
|
|
864
882
|
}
|
|
865
883
|
currentLoadChunkBatch.data.push(args)
|
|
866
|
-
})
|
|
884
|
+
}, signal)
|
|
867
885
|
// todo remove and use other architecture instead so data flow is clear
|
|
868
|
-
worldEmitter
|
|
886
|
+
bindAbortableListener(worldEmitter, 'blockEntities', (blockEntities) => {
|
|
869
887
|
this.blockEntities = blockEntities
|
|
870
|
-
})
|
|
888
|
+
}, signal)
|
|
871
889
|
|
|
872
|
-
worldEmitter
|
|
890
|
+
bindAbortableListener(worldEmitter, 'unloadChunk', ({ x, z }) => {
|
|
873
891
|
this.removeColumn(x, z)
|
|
874
|
-
})
|
|
892
|
+
}, signal)
|
|
875
893
|
|
|
876
|
-
worldEmitter
|
|
894
|
+
bindAbortableListener(worldEmitter, 'blockUpdate', ({ pos, stateId }) => {
|
|
877
895
|
this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
|
|
878
|
-
})
|
|
896
|
+
}, signal)
|
|
879
897
|
|
|
880
|
-
worldEmitter
|
|
898
|
+
bindAbortableListener(worldEmitter, 'chunkPosUpdate', ({ pos }) => {
|
|
881
899
|
this.updateViewerPosition(pos)
|
|
882
|
-
})
|
|
900
|
+
}, signal)
|
|
883
901
|
|
|
884
|
-
worldEmitter
|
|
902
|
+
bindAbortableListener(worldEmitter, 'end', () => {
|
|
885
903
|
this.worldStop?.()
|
|
886
|
-
})
|
|
887
|
-
|
|
904
|
+
}, signal)
|
|
888
905
|
|
|
889
|
-
worldEmitter
|
|
906
|
+
bindAbortableListener(worldEmitter, 'renderDistance', (d) => {
|
|
890
907
|
this.viewDistance = d
|
|
891
908
|
this.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
|
892
909
|
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
|
893
910
|
this.onRenderDistanceChanged?.(d)
|
|
894
|
-
})
|
|
911
|
+
}, signal)
|
|
895
912
|
|
|
896
|
-
worldEmitter
|
|
913
|
+
bindAbortableListener(worldEmitter, 'markAsLoaded', ({ x, z }) => {
|
|
897
914
|
this.markAsLoaded(x, z)
|
|
898
|
-
})
|
|
915
|
+
}, signal)
|
|
899
916
|
|
|
900
|
-
worldEmitter
|
|
917
|
+
bindAbortableListener(worldEmitter, 'updateLight', ({ pos }) => {
|
|
901
918
|
this.lightUpdate(pos.x, pos.z)
|
|
902
|
-
})
|
|
919
|
+
}, signal)
|
|
903
920
|
|
|
904
|
-
worldEmitter
|
|
921
|
+
bindAbortableListener(worldEmitter, 'onWorldSwitch', () => {
|
|
905
922
|
for (const fn of this.onWorldSwitched) {
|
|
906
923
|
try {
|
|
907
924
|
fn()
|
|
@@ -912,9 +929,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
912
929
|
}, 0)
|
|
913
930
|
}
|
|
914
931
|
}
|
|
915
|
-
})
|
|
932
|
+
}, signal)
|
|
916
933
|
|
|
917
|
-
worldEmitter
|
|
934
|
+
bindAbortableListener(worldEmitter, 'time', (timeOfDay) => {
|
|
918
935
|
if (!this.worldRendererConfig.dayCycle) return
|
|
919
936
|
this.timeUpdated?.(timeOfDay)
|
|
920
937
|
|
|
@@ -925,15 +942,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
925
942
|
// if (this instanceof WorldRendererThree) {
|
|
926
943
|
// (this).rerenderAllChunks?.()
|
|
927
944
|
// }
|
|
928
|
-
})
|
|
945
|
+
}, signal)
|
|
929
946
|
|
|
930
|
-
worldEmitter
|
|
947
|
+
bindAbortableListener(worldEmitter, 'biomeUpdate', ({ biome }) => {
|
|
931
948
|
this.biomeUpdated?.(biome)
|
|
932
|
-
})
|
|
949
|
+
}, signal)
|
|
933
950
|
|
|
934
|
-
worldEmitter
|
|
951
|
+
bindAbortableListener(worldEmitter, 'biomeReset', () => {
|
|
935
952
|
this.biomeReset?.()
|
|
936
|
-
})
|
|
953
|
+
}, signal)
|
|
937
954
|
}
|
|
938
955
|
|
|
939
956
|
setBlockStateIdInner(pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
|
|
@@ -1217,6 +1234,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
1217
1234
|
}
|
|
1218
1235
|
|
|
1219
1236
|
destroy() {
|
|
1237
|
+
for (const unsub of this.valtioUnsubs) {
|
|
1238
|
+
unsub()
|
|
1239
|
+
}
|
|
1240
|
+
this.valtioUnsubs = []
|
|
1241
|
+
|
|
1220
1242
|
// Cancel all pending heightmap debounce timers
|
|
1221
1243
|
for (const timer of this.heightmapDebounceTimers.values()) {
|
|
1222
1244
|
clearTimeout(timer)
|
package/src/three/entities.ts
CHANGED
|
@@ -1080,6 +1080,7 @@ export class Entities {
|
|
|
1080
1080
|
if (entity.delete) {
|
|
1081
1081
|
if (!e) return
|
|
1082
1082
|
e.userData._posTween?.stop()
|
|
1083
|
+
e.userData._rotTween?.stop()
|
|
1083
1084
|
if (e.additionalCleanup) e.additionalCleanup()
|
|
1084
1085
|
e.traverse(c => {
|
|
1085
1086
|
if (c['additionalCleanup']) c['additionalCleanup']()
|
|
@@ -1394,7 +1395,11 @@ export class Entities {
|
|
|
1394
1395
|
}
|
|
1395
1396
|
if (typeof entity.yaw === 'number' && Number.isFinite(entity.yaw)) {
|
|
1396
1397
|
const dy = shortestYawRadians(e.rotation.y, entity.yaw)
|
|
1397
|
-
|
|
1398
|
+
// Stop previous rotation tween to prevent accumulation (mirror _posTween)
|
|
1399
|
+
e.userData._rotTween?.stop()
|
|
1400
|
+
e.userData._rotTween = new TWEEN.Tween(e.rotation)
|
|
1401
|
+
.to({ y: e.rotation.y + dy }, ANIMATION_DURATION)
|
|
1402
|
+
.start()
|
|
1398
1403
|
}
|
|
1399
1404
|
|
|
1400
1405
|
if (e?.playerObject && overrides?.rotation?.head) {
|
|
@@ -191,25 +191,35 @@ export const createGraphicsBackendBase = () => {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
const startWorld = async (displayOptionsArg: DisplayWorldOptions) => {
|
|
194
|
-
const displayOptionsRestorers = [ResourcesManager, WorldViewWorker]
|
|
195
|
-
const displayOptions: DisplayWorldOptions = isWebWorker ? restoreTransferred(displayOptionsArg, displayOptionsRestorers, globalThis as unknown as Worker) : displayOptionsArg
|
|
196
|
-
|
|
197
194
|
if (!documentRenderer) throw new Error('Document renderer not initialized')
|
|
198
195
|
|
|
199
|
-
documentRenderer.nonReactiveState = displayOptions.nonReactiveState
|
|
200
|
-
// Set resourcesManager globally for world rendering
|
|
201
|
-
; (globalThis as any).resourcesManager = displayOptions.resourcesManager
|
|
202
|
-
|
|
203
196
|
if (menuBackgroundRenderer) {
|
|
204
197
|
menuBackgroundRenderer.dispose()
|
|
205
198
|
menuBackgroundRenderer = null
|
|
206
199
|
}
|
|
207
200
|
|
|
201
|
+
if (worldRenderer) {
|
|
202
|
+
worldRenderer.destroy()
|
|
203
|
+
worldRenderer = null
|
|
204
|
+
frameTimingCollector = null
|
|
205
|
+
;(globalThis as any).world = undefined
|
|
206
|
+
;(globalThis as any).frameTimingCollector = undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const displayOptionsRestorers = [ResourcesManager, WorldViewWorker]
|
|
210
|
+
const displayOptions: DisplayWorldOptions = isWebWorker ? restoreTransferred(displayOptionsArg, displayOptionsRestorers, globalThis as unknown as Worker) : displayOptionsArg
|
|
211
|
+
|
|
212
|
+
documentRenderer.nonReactiveState = displayOptions.nonReactiveState
|
|
213
|
+
// Set resourcesManager globally for world rendering
|
|
214
|
+
; (globalThis as any).resourcesManager = displayOptions.resourcesManager
|
|
215
|
+
|
|
208
216
|
worldRenderer = new WorldRendererThree(documentRenderer.renderer, initOptions, displayOptions)
|
|
209
217
|
|
|
210
218
|
await worldRenderer.worldReadyPromise
|
|
211
219
|
|
|
212
|
-
frameTimingCollector =
|
|
220
|
+
frameTimingCollector = displayOptions.inWorldRenderingConfig.enableDebugOverlay
|
|
221
|
+
? new FrameTimingCollector(displayOptions.nonReactiveState)
|
|
222
|
+
: null
|
|
213
223
|
; (globalThis as any).frameTimingCollector = frameTimingCollector
|
|
214
224
|
|
|
215
225
|
const originalRender = documentRenderer.render
|
|
@@ -93,7 +93,7 @@ export class RainModule implements RendererModuleController {
|
|
|
93
93
|
const chunkX = Math.floor(worldX / 16)
|
|
94
94
|
const chunkZ = Math.floor(worldZ / 16)
|
|
95
95
|
if (chunkX !== prevChunkX || chunkZ !== prevChunkZ) {
|
|
96
|
-
cachedHeightmap = heightmaps
|
|
96
|
+
cachedHeightmap = heightmaps[`${chunkX},${chunkZ}`]
|
|
97
97
|
prevChunkX = chunkX
|
|
98
98
|
prevChunkZ = chunkZ
|
|
99
99
|
}
|
|
@@ -142,9 +142,9 @@ void main() {
|
|
|
142
142
|
} else if (faceId == 2u || faceId == 3u) {
|
|
143
143
|
v_uv = vec2(v, u);
|
|
144
144
|
} else if (faceId == 4u) {
|
|
145
|
-
v_uv = vec2(u,
|
|
145
|
+
v_uv = vec2(u, v);
|
|
146
146
|
} else { // faceId == 5u
|
|
147
|
-
v_uv = vec2(1.0 - u,
|
|
147
|
+
v_uv = vec2(1.0 - u, v);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// --- Position: section base (multiples of 16) + face quad + block-local 0..15 ---
|
|
@@ -167,7 +167,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
167
167
|
this.performanceMonitor = new PerformanceMonitor(this.reactiveState.world.instabilityFactors)
|
|
168
168
|
|
|
169
169
|
this.renderer = renderer
|
|
170
|
-
|
|
170
|
+
const rendererInfo = WorldRendererThree.getRendererInfo(renderer) ?? '...'
|
|
171
|
+
displayOptions.rendererState.renderer = rendererInfo
|
|
171
172
|
|
|
172
173
|
// Initialize chunk mesh manager
|
|
173
174
|
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
|
|
@@ -44,6 +44,13 @@ export const delayedIterator = async <T>(
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
type WorkerWorldViewBridge = {
|
|
48
|
+
activeWorldView: WorldViewWorker
|
|
49
|
+
handler: (event: MessageEvent) => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const workerWorldViewBridges = new WeakMap<Worker, WorkerWorldViewBridge>()
|
|
53
|
+
|
|
47
54
|
/**
|
|
48
55
|
* WorldView for worker thread communication.
|
|
49
56
|
* This is a lightweight version that receives events from the main thread.
|
|
@@ -51,19 +58,43 @@ export const delayedIterator = async <T>(
|
|
|
51
58
|
export class WorldViewWorker extends (EventEmitter as new () => TypedEmitter<WorldViewEvents>) {
|
|
52
59
|
static readonly restorerName = 'WorldViewWorker'
|
|
53
60
|
|
|
54
|
-
static restoreTransferred(
|
|
61
|
+
static restoreTransferred(_data: any, worker?: Worker): WorldViewWorker {
|
|
55
62
|
const worldView = new WorldViewWorker()
|
|
56
|
-
if (worker) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
if (!worker) {
|
|
64
|
+
return worldView
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let bridge = workerWorldViewBridges.get(worker)
|
|
68
|
+
if (!bridge) {
|
|
69
|
+
const handler = ({ data }: MessageEvent) => {
|
|
70
|
+
const state = workerWorldViewBridges.get(worker)
|
|
71
|
+
const active = state?.activeWorldView
|
|
72
|
+
if (!active || data?.class !== WorldViewWorker.restorerName) return
|
|
73
|
+
if (data.type === 'event') {
|
|
74
|
+
active.emit(data.eventName, ...data.args)
|
|
62
75
|
}
|
|
63
|
-
}
|
|
76
|
+
}
|
|
77
|
+
worker.addEventListener('message', handler as EventListener)
|
|
78
|
+
bridge = { activeWorldView: worldView, handler }
|
|
79
|
+
workerWorldViewBridges.set(worker, bridge)
|
|
80
|
+
} else {
|
|
81
|
+
bridge.activeWorldView = worldView
|
|
64
82
|
}
|
|
65
83
|
return worldView
|
|
66
84
|
}
|
|
85
|
+
|
|
86
|
+
/** @internal vitest — remove bridge listener from worker */
|
|
87
|
+
static clearWorkerBridgeForTest(worker: Worker): void {
|
|
88
|
+
const bridge = workerWorldViewBridges.get(worker)
|
|
89
|
+
if (!bridge) return
|
|
90
|
+
worker.removeEventListener('message', bridge.handler as EventListener)
|
|
91
|
+
workerWorldViewBridges.delete(worker)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @internal vitest — count of bridge listeners on worker */
|
|
95
|
+
static getWorkerBridgeListenerCountForTest(worker: Worker): number {
|
|
96
|
+
return workerWorldViewBridges.has(worker) ? 1 : 0
|
|
97
|
+
}
|
|
67
98
|
}
|
|
68
99
|
|
|
69
100
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { WorldViewWorker } from './worldView'
|
|
4
|
+
|
|
5
|
+
describe('WorldViewWorker.restoreTransferred bridge', () => {
|
|
6
|
+
it('registers only one message listener per worker across restores', () => {
|
|
7
|
+
const addSpy = vi.fn()
|
|
8
|
+
const worker = {
|
|
9
|
+
addEventListener: addSpy,
|
|
10
|
+
removeEventListener: vi.fn(),
|
|
11
|
+
} as unknown as Worker
|
|
12
|
+
|
|
13
|
+
WorldViewWorker.restoreTransferred({}, worker)
|
|
14
|
+
WorldViewWorker.restoreTransferred({}, worker)
|
|
15
|
+
WorldViewWorker.restoreTransferred({}, worker)
|
|
16
|
+
|
|
17
|
+
expect(addSpy).toHaveBeenCalledTimes(1)
|
|
18
|
+
expect(WorldViewWorker.getWorkerBridgeListenerCountForTest(worker)).toBe(1)
|
|
19
|
+
|
|
20
|
+
WorldViewWorker.clearWorkerBridgeForTest(worker)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('routes events to the latest restored worldView only', () => {
|
|
24
|
+
const listeners: Array<(event: MessageEvent) => void> = []
|
|
25
|
+
const worker = {
|
|
26
|
+
addEventListener: (_type: string, handler: (event: MessageEvent) => void) => {
|
|
27
|
+
listeners.push(handler)
|
|
28
|
+
},
|
|
29
|
+
removeEventListener: () => {},
|
|
30
|
+
} as unknown as Worker
|
|
31
|
+
|
|
32
|
+
const first = WorldViewWorker.restoreTransferred({}, worker)
|
|
33
|
+
const second = WorldViewWorker.restoreTransferred({}, worker)
|
|
34
|
+
|
|
35
|
+
let firstCalls = 0
|
|
36
|
+
let secondCalls = 0
|
|
37
|
+
first.on('loadChunk', () => {
|
|
38
|
+
firstCalls++
|
|
39
|
+
})
|
|
40
|
+
second.on('loadChunk', () => {
|
|
41
|
+
secondCalls++
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const handler = listeners[0]!
|
|
45
|
+
handler({
|
|
46
|
+
data: {
|
|
47
|
+
class: WorldViewWorker.restorerName,
|
|
48
|
+
type: 'event',
|
|
49
|
+
eventName: 'loadChunk',
|
|
50
|
+
args: [{ x: 0, z: 0, chunk: {}, worldConfig: {}, isLightUpdate: false }],
|
|
51
|
+
},
|
|
52
|
+
} as MessageEvent)
|
|
53
|
+
|
|
54
|
+
expect(firstCalls).toBe(0)
|
|
55
|
+
expect(secondCalls).toBe(1)
|
|
56
|
+
|
|
57
|
+
WorldViewWorker.clearWorkerBridgeForTest(worker)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
//@ts-nocheck
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { restoreTransferred } from './workerProxy'
|
|
4
|
-
|
|
5
|
-
describe('restoreTransferred Set/Map', () => {
|
|
6
|
-
const worker = null as unknown as Worker
|
|
7
|
-
|
|
8
|
-
it('restores Set from __setValues', () => {
|
|
9
|
-
const out = restoreTransferred({ __restorer: 'Set', __setValues: ['a', 'b'] }, [], worker, false)
|
|
10
|
-
expect(out).toEqual(new Set(['a', 'b']))
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('does not throw when legacy values is a function (Map.values collision)', () => {
|
|
14
|
-
const map = new Map([['k', 1]])
|
|
15
|
-
const out = restoreTransferred({ __restorer: 'Set', values: map.values.bind(map) }, [], worker, false)
|
|
16
|
-
expect(out).toEqual(new Set())
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('restores Map from __mapEntries', () => {
|
|
20
|
-
const out = restoreTransferred({ __restorer: 'Map', __mapEntries: [['a', 1]] }, [], worker, false)
|
|
21
|
-
expect(out).toEqual(new Map([['a', 1]]))
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('does not throw when legacy entries is a function', () => {
|
|
25
|
-
const m = new Map()
|
|
26
|
-
const out = restoreTransferred({ __restorer: 'Map', entries: m.entries.bind(m) }, [], worker, false)
|
|
27
|
-
expect(out).toEqual(new Map())
|
|
28
|
-
})
|
|
29
|
-
})
|