minecraft-renderer 0.1.70 → 0.1.72
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/README.md +2 -2
- package/dist/minecraft-renderer.js +20 -20
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +56 -56
- package/package.json +1 -1
- package/src/graphicsBackend/rendererDefaultOptions.ts +4 -4
- package/src/lib/worldrendererCommon.reconfigure.test.ts +202 -0
- package/src/lib/worldrendererCommon.ts +138 -16
- package/src/three/entities.ts +20 -123
- package/src/three/graphicsBackendOffThread.ts +16 -1
- package/src/worldView/worldView.ts +11 -0
package/package.json
CHANGED
|
@@ -166,8 +166,8 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
|
|
|
166
166
|
},
|
|
167
167
|
rendererWorldPerformance: {
|
|
168
168
|
text: 'World performance',
|
|
169
|
-
tooltip: 'Background workers for chunk geometry.
|
|
170
|
-
|
|
169
|
+
tooltip: 'Background workers for chunk geometry. Recreates mesher workers and reloads chunks.',
|
|
170
|
+
requiresChunksReload: true,
|
|
171
171
|
possibleValues: [
|
|
172
172
|
['low-energy', 'Low Energy'],
|
|
173
173
|
['normal', 'Normal'],
|
|
@@ -193,8 +193,8 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
|
|
|
193
193
|
rendererMesher: {
|
|
194
194
|
possibleValues: [['wasm', 'WASM'], ['legacy-js', 'Legacy JS']],
|
|
195
195
|
text: 'Mesher pipeline',
|
|
196
|
-
tooltip: '
|
|
197
|
-
|
|
196
|
+
tooltip: 'Browser technology for processing world geometry before render. WASM is the fastest; if you see a dead tab icon, reloads, or other errors, switch to Legacy JS.',
|
|
197
|
+
requiresChunksReload: true,
|
|
198
198
|
},
|
|
199
199
|
rendererShaderCubeBlocks: {
|
|
200
200
|
text: '(UNSTABLE) Instanced shader cubes',
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { EventEmitter } from 'events'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
4
|
+
import { Vec3 } from 'vec3'
|
|
5
|
+
import { proxy } from 'valtio'
|
|
6
|
+
import * as worldRendererModule from './worldrendererCommon'
|
|
7
|
+
import { WorldRendererCommon } from './worldrendererCommon'
|
|
8
|
+
import { defaultWorldRendererConfig } from '../graphicsBackend/config'
|
|
9
|
+
import { getInitialPlayerState } from '../playerState/playerState'
|
|
10
|
+
import type { DisplayWorldOptions, GraphicsInitOptions } from '../graphicsBackend/types'
|
|
11
|
+
|
|
12
|
+
vi.mock('./ui/newStats', () => ({
|
|
13
|
+
addNewStat: vi.fn(() => ({ updateText: vi.fn(), setVisibility: vi.fn() })),
|
|
14
|
+
updateStatText: vi.fn(),
|
|
15
|
+
removeAllStats: vi.fn(),
|
|
16
|
+
updatePanesVisibility: vi.fn(),
|
|
17
|
+
MC_RENDERER_DEBUG_OVERLAY_CLASS: 'mc-renderer-debug-overlay',
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('./utils/skins', () => ({
|
|
21
|
+
setSkinsConfig: vi.fn(),
|
|
22
|
+
steveTexture: {},
|
|
23
|
+
stevePngUrl: '',
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
function ensurePromiseWithResolvers() {
|
|
27
|
+
if (!Promise.withResolvers) {
|
|
28
|
+
Promise.withResolvers = function <T>() {
|
|
29
|
+
let resolve!: (value: T | PromiseLike<T>) => void
|
|
30
|
+
let reject!: (reason?: unknown) => void
|
|
31
|
+
const promise = new Promise<T>((res, rej) => {
|
|
32
|
+
resolve = res
|
|
33
|
+
reject = rej
|
|
34
|
+
})
|
|
35
|
+
return { promise, resolve, reject }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class TestWorldRenderer extends WorldRendererCommon {
|
|
41
|
+
outputFormat = 'threeJs' as const
|
|
42
|
+
|
|
43
|
+
changeBackgroundColor() {}
|
|
44
|
+
changeCardinalLight() {}
|
|
45
|
+
handleWorkerMessage() {}
|
|
46
|
+
updateCamera() {}
|
|
47
|
+
render() {}
|
|
48
|
+
updateShowChunksBorder() {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createRenderer(workerCount = 2, worldView?: DisplayWorldOptions['worldView']) {
|
|
52
|
+
const reloadLoadedChunks = vi.fn(async () => {})
|
|
53
|
+
const rendererState = proxy({
|
|
54
|
+
world: {
|
|
55
|
+
chunksLoaded: {} as Record<string, true>,
|
|
56
|
+
heightmaps: {} as Record<string, Int16Array>,
|
|
57
|
+
allChunksLoaded: false,
|
|
58
|
+
mesherWork: false,
|
|
59
|
+
instabilityFactors: {},
|
|
60
|
+
intersectMedia: null,
|
|
61
|
+
},
|
|
62
|
+
renderer: '',
|
|
63
|
+
preventEscapeMenu: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const displayOptions: DisplayWorldOptions = {
|
|
67
|
+
version: '1.21.1',
|
|
68
|
+
worldView: (worldView ?? Object.assign(new EventEmitter(), { reloadLoadedChunks })) as DisplayWorldOptions['worldView'],
|
|
69
|
+
inWorldRenderingConfig: proxy({ ...defaultWorldRendererConfig, mesherWorkers: workerCount }),
|
|
70
|
+
playerStateReactive: getInitialPlayerState(),
|
|
71
|
+
rendererState,
|
|
72
|
+
nonReactiveState: {
|
|
73
|
+
fps: 0,
|
|
74
|
+
worstRenderTime: 0,
|
|
75
|
+
avgRenderTime: 0,
|
|
76
|
+
world: {
|
|
77
|
+
chunksLoadedCount: 0,
|
|
78
|
+
chunksTotalNumber: 0,
|
|
79
|
+
chunksFullInfo: '',
|
|
80
|
+
},
|
|
81
|
+
renderer: {
|
|
82
|
+
timeline: { live: [], frozen: [], lastSecond: [] },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
resourcesManager: {
|
|
86
|
+
currentResources: {
|
|
87
|
+
mcData: { version: {} },
|
|
88
|
+
blocksAtlasJson: {},
|
|
89
|
+
blockstatesModels: {},
|
|
90
|
+
},
|
|
91
|
+
} as DisplayWorldOptions['resourcesManager'],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const initOptions: GraphicsInitOptions = {
|
|
95
|
+
config: { sceneBackground: '#000' },
|
|
96
|
+
rendererSpecificSettings: {},
|
|
97
|
+
callbacks: {
|
|
98
|
+
displayCriticalError: vi.fn(),
|
|
99
|
+
setRendererSpecificSettings: vi.fn(),
|
|
100
|
+
fireCustomEvent: vi.fn(),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const renderer = new TestWorldRenderer(displayOptions.resourcesManager, displayOptions, initOptions)
|
|
105
|
+
renderer.active = true
|
|
106
|
+
renderer.workers = Array.from({ length: workerCount }, () => ({
|
|
107
|
+
postMessage: vi.fn(),
|
|
108
|
+
terminate: vi.fn(),
|
|
109
|
+
}))
|
|
110
|
+
renderer['syncMesherPoolSnapshot']()
|
|
111
|
+
renderer.viewDistance = 8
|
|
112
|
+
renderer.viewerChunkPosition = new Vec3(0, 64, 0)
|
|
113
|
+
renderer.worldSizeParams = { minY: 0, worldHeight: 256 }
|
|
114
|
+
return { renderer, reloadLoadedChunks }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('WorldRendererCommon.reconfigureMesherWorkers', () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
ensurePromiseWithResolvers()
|
|
120
|
+
vi.stubGlobal('location', { href: 'http://localhost/' })
|
|
121
|
+
vi.stubGlobal('Worker', class MockWorker {
|
|
122
|
+
postMessage = vi.fn()
|
|
123
|
+
terminate = vi.fn()
|
|
124
|
+
addEventListener = vi.fn()
|
|
125
|
+
onmessage: ((event: MessageEvent) => void) | null = null
|
|
126
|
+
})
|
|
127
|
+
vi.spyOn(worldRendererModule, 'meshersSendMcData').mockImplementation(() => {})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
vi.restoreAllMocks()
|
|
132
|
+
vi.unstubAllGlobals()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('recreates workers with new count and reloads chunks', async () => {
|
|
136
|
+
const { renderer, reloadLoadedChunks } = createRenderer(3)
|
|
137
|
+
const terminated = renderer.workers.map((worker) => worker.terminate)
|
|
138
|
+
renderer.worldRendererConfig.mesherWorkers = 1
|
|
139
|
+
|
|
140
|
+
await renderer.reconfigureMesherWorkers()
|
|
141
|
+
|
|
142
|
+
for (const terminate of terminated) {
|
|
143
|
+
expect(terminate).toHaveBeenCalled()
|
|
144
|
+
}
|
|
145
|
+
expect(renderer.workers).toHaveLength(1)
|
|
146
|
+
expect(reloadLoadedChunks).toHaveBeenCalledTimes(1)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('recreates workers when mesher pipeline changes', async () => {
|
|
150
|
+
const { renderer, reloadLoadedChunks } = createRenderer(2)
|
|
151
|
+
const terminated = renderer.workers.map((worker) => worker.terminate)
|
|
152
|
+
renderer.worldRendererConfig.wasmMesher = false
|
|
153
|
+
|
|
154
|
+
await renderer.reconfigureMesherWorkers()
|
|
155
|
+
|
|
156
|
+
for (const terminate of terminated) {
|
|
157
|
+
expect(terminate).toHaveBeenCalled()
|
|
158
|
+
}
|
|
159
|
+
expect(renderer.workers).toHaveLength(2)
|
|
160
|
+
expect(reloadLoadedChunks).toHaveBeenCalledTimes(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('watchMesherPoolConfig registers valtio unsubscribe functions', () => {
|
|
164
|
+
const { renderer } = createRenderer(2)
|
|
165
|
+
|
|
166
|
+
renderer['watchMesherPoolConfig']()
|
|
167
|
+
expect(renderer['valtioUnsubs']).toHaveLength(3)
|
|
168
|
+
for (const unsub of renderer['valtioUnsubs']) {
|
|
169
|
+
expect(typeof unsub).toBe('function')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
expect(() => {
|
|
173
|
+
for (const unsub of renderer['valtioUnsubs']) unsub()
|
|
174
|
+
renderer.destroy()
|
|
175
|
+
}).not.toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('config watcher enqueues reconfigure when mesherWorkers changes', async () => {
|
|
179
|
+
const enqueueSpy = vi.spyOn(TestWorldRenderer.prototype as any, 'enqueueMesherWorkersReconfigure')
|
|
180
|
+
const { renderer } = createRenderer(2)
|
|
181
|
+
|
|
182
|
+
renderer['watchMesherPoolConfig']()
|
|
183
|
+
renderer.worldRendererConfig.mesherWorkers = 4
|
|
184
|
+
|
|
185
|
+
await vi.waitFor(() => {
|
|
186
|
+
expect(enqueueSpy).toHaveBeenCalled()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
enqueueSpy.mockRestore()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('skips tail work when destroyed during bootstrap', async () => {
|
|
193
|
+
const { renderer, reloadLoadedChunks } = createRenderer(2)
|
|
194
|
+
vi.spyOn(renderer, 'updateAssetsData').mockImplementation(async () => {
|
|
195
|
+
renderer.active = false
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
await renderer.reconfigureMesherWorkers()
|
|
199
|
+
|
|
200
|
+
expect(reloadLoadedChunks).not.toHaveBeenCalled()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -127,6 +127,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
127
127
|
debugStopGeometryUpdate = false
|
|
128
128
|
|
|
129
129
|
protocolCustomBlocks = new Map<string, CustomBlockModels>()
|
|
130
|
+
private mesherPoolSnapshot = {
|
|
131
|
+
mesherWorkers: -1,
|
|
132
|
+
wasmMesher: false,
|
|
133
|
+
dedicatedChangeWorker: false,
|
|
134
|
+
}
|
|
135
|
+
private mesherReconfigureQueue: Promise<void> = Promise.resolve()
|
|
130
136
|
private heightmapDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
131
137
|
|
|
132
138
|
// Geometry throttle: first dirty per section is instant, subsequent within window are grouped
|
|
@@ -272,6 +278,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
272
278
|
|
|
273
279
|
this.watchReactivePlayerState()
|
|
274
280
|
this.watchReactiveConfig()
|
|
281
|
+
this.watchMesherPoolConfig()
|
|
275
282
|
this.worldReadyResolvers.resolve()
|
|
276
283
|
}
|
|
277
284
|
|
|
@@ -309,18 +316,127 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
309
316
|
}
|
|
310
317
|
}
|
|
311
318
|
|
|
319
|
+
private getMesherWorkerScript(): 'wasm' | 'legacy' {
|
|
320
|
+
return this.worldRendererConfig.wasmMesher ? 'wasm' : 'legacy'
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private createMesherWorker() {
|
|
324
|
+
const script = this.getMesherWorkerScript()
|
|
325
|
+
return initMesherWorker((data) => {
|
|
326
|
+
if (Array.isArray(data)) {
|
|
327
|
+
this.messageQueue.push(...data)
|
|
328
|
+
} else {
|
|
329
|
+
this.messageQueue.push(data)
|
|
330
|
+
}
|
|
331
|
+
void this.processMessageQueue('worker')
|
|
332
|
+
}, script === 'wasm' ? 'mesherWasm.js' : 'mesher.js')
|
|
333
|
+
}
|
|
334
|
+
|
|
312
335
|
initWorkers(numWorkers = this.worldRendererConfig.mesherWorkers) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
336
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
337
|
+
this.workers.push(this.createMesherWorker())
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private syncMesherPoolSnapshot() {
|
|
342
|
+
this.mesherPoolSnapshot = {
|
|
343
|
+
mesherWorkers: this.worldRendererConfig.mesherWorkers,
|
|
344
|
+
wasmMesher: this.worldRendererConfig.wasmMesher,
|
|
345
|
+
dedicatedChangeWorker: this.worldRendererConfig.dedicatedChangeWorker,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private watchMesherPoolConfig() {
|
|
350
|
+
this.syncMesherPoolSnapshot()
|
|
351
|
+
|
|
352
|
+
const tryReconfigure = () => {
|
|
353
|
+
const cfg = this.worldRendererConfig
|
|
354
|
+
const snap = this.mesherPoolSnapshot
|
|
355
|
+
if (
|
|
356
|
+
cfg.mesherWorkers === snap.mesherWorkers &&
|
|
357
|
+
cfg.wasmMesher === snap.wasmMesher &&
|
|
358
|
+
cfg.dedicatedChangeWorker === snap.dedicatedChangeWorker
|
|
359
|
+
) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
this.syncMesherPoolSnapshot()
|
|
363
|
+
this.enqueueMesherWorkersReconfigure()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const key of ['mesherWorkers', 'wasmMesher', 'dedicatedChangeWorker'] as const) {
|
|
367
|
+
this.valtioUnsubs.push(
|
|
368
|
+
this.onReactiveConfigUpdated(key, tryReconfigure, false)
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private enqueueMesherWorkersReconfigure() {
|
|
374
|
+
this.mesherReconfigureQueue = this.mesherReconfigureQueue
|
|
375
|
+
.then(() => this.reconfigureMesherWorkers())
|
|
376
|
+
.catch((err) => {
|
|
377
|
+
console.error('[Mesher] Failed to reconfigure workers:', err)
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
private clearMesherPendingState() {
|
|
381
|
+
this.sectionsWaiting.clear()
|
|
382
|
+
this.toWorkerMessagesQueue = {}
|
|
383
|
+
this.queueAwaited = false
|
|
384
|
+
this.messageQueue = []
|
|
385
|
+
this.isProcessingQueue = false
|
|
386
|
+
for (const timer of this.sectionDirtyTimers.values()) {
|
|
387
|
+
clearTimeout(timer)
|
|
388
|
+
}
|
|
389
|
+
this.sectionDirtyTimers.clear()
|
|
390
|
+
this.sectionDirtyCount.clear()
|
|
391
|
+
this.sectionDirtyPendingArgs.clear()
|
|
392
|
+
this.reactiveState.world.mesherWork = false
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private terminateAllMesherWorkers() {
|
|
396
|
+
for (const worker of this.workers) {
|
|
397
|
+
worker.terminate()
|
|
398
|
+
}
|
|
399
|
+
this.workers = []
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async bootstrapMesherWorkers() {
|
|
403
|
+
if (this.workers.length === 0) return
|
|
404
|
+
|
|
405
|
+
this.sendMesherMcData()
|
|
406
|
+
await this.updateAssetsData()
|
|
407
|
+
this.logWorkerWork('# mesher workers bootstrapped')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async reconfigureMesherWorkers() {
|
|
411
|
+
if (!this.active) return
|
|
412
|
+
|
|
413
|
+
this.clearMesherPendingState()
|
|
414
|
+
this.terminateAllMesherWorkers()
|
|
415
|
+
this.initWorkers()
|
|
416
|
+
await this.bootstrapMesherWorkers()
|
|
417
|
+
if (!this.active) return
|
|
418
|
+
|
|
419
|
+
await this.requestLoadedChunksReload()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async requestLoadedChunksReload() {
|
|
423
|
+
try {
|
|
424
|
+
const worldView = this.displayOptions.worldView as {
|
|
425
|
+
reloadLoadedChunks?: () => void | Promise<void>
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (typeof worldView.reloadLoadedChunks === 'function') {
|
|
429
|
+
await worldView.reloadLoadedChunks()
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const workerScope = globalThis as typeof globalThis & { WorkerGlobalScope?: typeof WorkerGlobalScope }
|
|
434
|
+
if (typeof workerScope.WorkerGlobalScope !== 'undefined' && globalThis instanceof workerScope.WorkerGlobalScope) {
|
|
435
|
+
// eslint-disable-next-line no-restricted-globals
|
|
436
|
+
self.postMessage({ type: 'reloadLoadedChunks' })
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error('[Mesher] Failed to reload chunks after worker reconfigure:', err)
|
|
324
440
|
}
|
|
325
441
|
}
|
|
326
442
|
|
|
@@ -331,13 +447,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
331
447
|
return subscribeKey(this.playerStateReactive, key, callback)
|
|
332
448
|
}
|
|
333
449
|
|
|
334
|
-
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(
|
|
335
|
-
|
|
450
|
+
onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(
|
|
451
|
+
key: T,
|
|
452
|
+
callback: (value: typeof this.worldRendererConfig[T]) => void,
|
|
453
|
+
initial = true
|
|
454
|
+
) {
|
|
455
|
+
if (initial) {
|
|
456
|
+
callback(this.worldRendererConfig[key])
|
|
457
|
+
}
|
|
336
458
|
if ((key as any) === '*') {
|
|
337
|
-
subscribe(this.worldRendererConfig, callback as any)
|
|
338
|
-
} else {
|
|
339
|
-
subscribeKey(this.worldRendererConfig, key, callback)
|
|
459
|
+
return subscribe(this.worldRendererConfig, callback as any)
|
|
340
460
|
}
|
|
461
|
+
return subscribeKey(this.worldRendererConfig, key, callback)
|
|
341
462
|
}
|
|
342
463
|
|
|
343
464
|
onReactiveDebugUpdated<T extends keyof typeof this.reactiveDebugParams>(key: T, callback: (value: typeof this.reactiveDebugParams[T]) => void) {
|
|
@@ -637,6 +758,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
637
758
|
|
|
638
759
|
this.initWorkers()
|
|
639
760
|
this.active = true
|
|
761
|
+
this.syncMesherPoolSnapshot()
|
|
640
762
|
|
|
641
763
|
this.sendMesherMcData()
|
|
642
764
|
}
|
package/src/three/entities.ts
CHANGED
|
@@ -271,46 +271,6 @@ export class Entities {
|
|
|
271
271
|
itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
|
|
272
272
|
pendingModelOverrides = new Map<string, { parts: EntityModelOverridePart[] }>()
|
|
273
273
|
|
|
274
|
-
private motionCache = new Map<string, { pos: THREE.Vector3, speed: number }>()
|
|
275
|
-
private readonly MOVE_ON = 0.05
|
|
276
|
-
private readonly MOVE_OFF = 0.02
|
|
277
|
-
private readonly RUN_ON = 4.8
|
|
278
|
-
private readonly RUN_OFF = 4.2
|
|
279
|
-
|
|
280
|
-
private updateAutoWalkFlags(entityKey: string, entity: SceneEntity, dt: number) {
|
|
281
|
-
if (!entity.playerObject?.animation) return
|
|
282
|
-
const anim: any = entity.playerObject.animation
|
|
283
|
-
if (!('isMoving' in anim) || !('isRunning' in anim)) return
|
|
284
|
-
if (dt <= 0) return
|
|
285
|
-
|
|
286
|
-
const cached = this.motionCache.get(entityKey)
|
|
287
|
-
if (!cached) {
|
|
288
|
-
this.motionCache.set(entityKey, { pos: entity.position.clone(), speed: 0 })
|
|
289
|
-
anim.isMoving = false
|
|
290
|
-
anim.isRunning = false
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const dx = entity.position.x - cached.pos.x
|
|
295
|
-
const dz = entity.position.z - cached.pos.z
|
|
296
|
-
cached.pos.copy(entity.position)
|
|
297
|
-
|
|
298
|
-
const instSpeed = Math.hypot(dx, dz) / Math.max(dt, 1e-6)
|
|
299
|
-
|
|
300
|
-
cached.speed = cached.speed * 0.8 + instSpeed * 0.2
|
|
301
|
-
|
|
302
|
-
const movingNow = anim.isMoving
|
|
303
|
-
? cached.speed > this.MOVE_OFF
|
|
304
|
-
: cached.speed > this.MOVE_ON
|
|
305
|
-
|
|
306
|
-
const runningNow = anim.isRunning
|
|
307
|
-
? cached.speed > this.RUN_OFF
|
|
308
|
-
: cached.speed > this.RUN_ON
|
|
309
|
-
|
|
310
|
-
anim.isMoving = movingNow
|
|
311
|
-
anim.isRunning = movingNow && runningNow
|
|
312
|
-
}
|
|
313
|
-
|
|
314
274
|
get entitiesByName(): Record<string, SceneEntity[]> {
|
|
315
275
|
const byName: Record<string, SceneEntity[]> = {}
|
|
316
276
|
for (const entity of Object.values(this.entities)) {
|
|
@@ -383,8 +343,6 @@ export class Entities {
|
|
|
383
343
|
this.entities = {}
|
|
384
344
|
this.currentSkinUrls = {}
|
|
385
345
|
|
|
386
|
-
this.motionCache.clear()
|
|
387
|
-
|
|
388
346
|
// Clean up player entity
|
|
389
347
|
if (this.playerEntity) {
|
|
390
348
|
this.worldRenderer.sceneOrigin.removeAndUntrack(this.playerEntity)
|
|
@@ -447,15 +405,13 @@ export class Entities {
|
|
|
447
405
|
this.setRendering(renderEntitiesConfig)
|
|
448
406
|
}
|
|
449
407
|
|
|
450
|
-
const
|
|
451
|
-
const dt = Math.min(dtRaw, 1 / 30)
|
|
408
|
+
const dt = Math.min(this.clock.getDelta(), 1 / 30)
|
|
452
409
|
const botPos = this.worldRenderer.viewerChunkPosition
|
|
453
410
|
const VISIBLE_DISTANCE = 10 * 10
|
|
454
411
|
|
|
455
412
|
for (const [entityIdRaw, entity] of [...Object.entries(this.entities), ['player_entity', this.playerEntity] as [string, SceneEntity | null]]) {
|
|
456
413
|
if (!entity) continue
|
|
457
414
|
|
|
458
|
-
let entityKey = entityIdRaw
|
|
459
415
|
const isPlayerEntity = entityIdRaw === 'player_entity'
|
|
460
416
|
|
|
461
417
|
if (isPlayerEntity) {
|
|
@@ -470,14 +426,10 @@ export class Entities {
|
|
|
470
426
|
this.worldRenderer.cameraWorldPos.z
|
|
471
427
|
)
|
|
472
428
|
}
|
|
473
|
-
|
|
474
|
-
entityKey = String(this.playerEntity?.originalEntity.id ?? 'player_entity')
|
|
475
429
|
}
|
|
476
430
|
|
|
477
431
|
const { playerObject } = entity
|
|
478
432
|
|
|
479
|
-
this.updateAutoWalkFlags(entityKey, entity, dtRaw)
|
|
480
|
-
|
|
481
433
|
if (playerObject?.animation) {
|
|
482
434
|
playerObject.animation.update(playerObject, dt)
|
|
483
435
|
}
|
|
@@ -778,84 +730,31 @@ export class Entities {
|
|
|
778
730
|
}
|
|
779
731
|
}
|
|
780
732
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// while standing still. Only dedupe the persistent state animations.
|
|
788
|
-
if (animation !== 'oneSwing') {
|
|
789
|
-
if (this.playerPerAnimation[key] === animation) return
|
|
790
|
-
this.playerPerAnimation[key] = animation
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (entityPlayerId === 'player_entity' && this.playerEntity?.playerObject) {
|
|
794
|
-
const { playerObject } = this.playerEntity
|
|
795
|
-
if (animation === 'oneSwing') {
|
|
796
|
-
if (playerObject.animation && (playerObject.animation as any).swingArm) {
|
|
797
|
-
(playerObject.animation as any).swingArm()
|
|
798
|
-
}
|
|
799
|
-
return
|
|
800
|
-
}
|
|
733
|
+
private applyMovementAnimation(
|
|
734
|
+
playerObject: PlayerObjectType,
|
|
735
|
+
animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking',
|
|
736
|
+
): void {
|
|
737
|
+
const anim = playerObject.animation as WalkingGeneralSwing | undefined
|
|
738
|
+
if (!anim) return
|
|
801
739
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const anim = playerObject.animation as any
|
|
805
|
-
if (anim) {
|
|
806
|
-
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
807
|
-
anim.isRunning = animation === 'running'
|
|
808
|
-
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
740
|
+
if (animation === 'oneSwing') {
|
|
741
|
+
anim.swingArm()
|
|
812
742
|
return
|
|
813
743
|
}
|
|
814
744
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
(playerObject.animation as any).swingArm()
|
|
821
|
-
}
|
|
822
|
-
return
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (playerObject.animation && (playerObject.animation as any).switchAnimationCallback !== undefined) {
|
|
826
|
-
(playerObject.animation as any).switchAnimationCallback = () => {
|
|
827
|
-
const anim = playerObject.animation as any
|
|
828
|
-
if (anim) {
|
|
829
|
-
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
830
|
-
anim.isRunning = animation === 'running'
|
|
831
|
-
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return
|
|
836
|
-
}
|
|
745
|
+
anim.switchAnimationCallback = null
|
|
746
|
+
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
747
|
+
anim.isRunning = animation === 'running'
|
|
748
|
+
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
749
|
+
}
|
|
837
750
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (playerEntityObject.animation && (playerEntityObject.animation as any).swingArm) {
|
|
843
|
-
(playerEntityObject.animation as any).swingArm()
|
|
844
|
-
}
|
|
845
|
-
return
|
|
846
|
-
}
|
|
751
|
+
playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
|
|
752
|
+
const playerObject = entityPlayerId === 'player_entity'
|
|
753
|
+
? this.playerEntity?.playerObject
|
|
754
|
+
: this.getPlayerObject(entityPlayerId) ?? this.playerEntity?.playerObject
|
|
847
755
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const anim = playerEntityObject.animation as any
|
|
851
|
-
if (anim) {
|
|
852
|
-
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
853
|
-
anim.isRunning = animation === 'running'
|
|
854
|
-
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
756
|
+
if (!playerObject) return
|
|
757
|
+
this.applyMovementAnimation(playerObject, animation)
|
|
859
758
|
}
|
|
860
759
|
|
|
861
760
|
parseEntityLabel(jsonLike) {
|
|
@@ -1340,11 +1239,9 @@ export class Entities {
|
|
|
1340
1239
|
}
|
|
1341
1240
|
}
|
|
1342
1241
|
|
|
1343
|
-
playerPerAnimation = {} as Record<number, string>
|
|
1344
1242
|
onRemoveEntity(entity: import('prismarine-entity').Entity) {
|
|
1345
1243
|
this.loadedSkinEntityIds.delete(entity.id.toString())
|
|
1346
1244
|
delete this.currentSkinUrls[entity.id.toString()]
|
|
1347
|
-
this.motionCache.delete(entity.id.toString())
|
|
1348
1245
|
}
|
|
1349
1246
|
|
|
1350
1247
|
updateMap(mapNumber: string | number, data: string) {
|
|
@@ -9,6 +9,7 @@ import type { MenuBackgroundOptions } from './menuBackground/types'
|
|
|
9
9
|
import { MENU_BACKGROUND_MC_VERSION } from './menuBackground/shared'
|
|
10
10
|
import { createGraphicsBackendBase, type ThreeJsBackendMethods } from './graphicsBackendBase'
|
|
11
11
|
import { addCanvasForWorker } from './documentRenderer'
|
|
12
|
+
import type { WorldView } from '../worldView'
|
|
12
13
|
|
|
13
14
|
function initThreeWorker(onGotMessage: (data: any) => void) {
|
|
14
15
|
// Node environment needs an absolute path, but browser needs the url of the file
|
|
@@ -31,7 +32,12 @@ function initThreeWorker(onGotMessage: (data: any) => void) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (initOptions) => {
|
|
34
|
-
const
|
|
35
|
+
const workerSideChannel = {
|
|
36
|
+
onMessage: (_data: unknown) => {},
|
|
37
|
+
}
|
|
38
|
+
const worker = initThreeWorker((data) => {
|
|
39
|
+
workerSideChannel.onMessage(data)
|
|
40
|
+
})
|
|
35
41
|
type WorkerType = ReturnType<ReturnType<typeof createGraphicsBackendBase>['workerProxy']>
|
|
36
42
|
|
|
37
43
|
const proxy = useWorkerProxy<WorkerType>(worker)
|
|
@@ -79,6 +85,15 @@ export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (init
|
|
|
79
85
|
}
|
|
80
86
|
},
|
|
81
87
|
async startWorld(options) {
|
|
88
|
+
const worldView = options.worldView as unknown as WorldView
|
|
89
|
+
workerSideChannel.onMessage = (data: any) => {
|
|
90
|
+
if (data?.type === 'reloadLoadedChunks') {
|
|
91
|
+
void worldView.reloadLoadedChunks().catch((err) => {
|
|
92
|
+
console.error('[Renderer] Failed to reload chunks after mesher reconfigure:', err)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
const workerThreeSendData = {
|
|
83
98
|
...dynamicMcDataFiles,
|
|
84
99
|
items: 'itemsArray',
|
|
@@ -320,6 +320,17 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Re-fetch and re-emit every loaded chunk (e.g. after mesher workers are recreated).
|
|
325
|
+
*/
|
|
326
|
+
async reloadLoadedChunks(): Promise<void> {
|
|
327
|
+
const coords = Object.keys(this.loadedChunks)
|
|
328
|
+
for (const key of coords) {
|
|
329
|
+
const [x, z] = key.split(',').map(Number)
|
|
330
|
+
await this.loadChunk({ x, z }, false, 'mesher-reconfigure')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
323
334
|
/**
|
|
324
335
|
* Unload all chunks.
|
|
325
336
|
*/
|