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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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. Reload to apply.',
170
- requiresRestart: true,
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: 'WASM is faster. Use JS if WASM is not working. Requires reload.',
197
- requiresRestart: true
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
- // init workers
314
- for (let i = 0; i < numWorkers + 0; i++) {
315
- const worker = initMesherWorker((data) => {
316
- if (Array.isArray(data)) {
317
- this.messageQueue.push(...data)
318
- } else {
319
- this.messageQueue.push(data)
320
- }
321
- void this.processMessageQueue('worker')
322
- }, this.worldRendererConfig.wasmMesher ? 'mesherWasm.js' : 'mesher.js')
323
- this.workers.push(worker)
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>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {
335
- callback(this.worldRendererConfig[key])
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
  }
@@ -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 dtRaw = this.clock.getDelta()
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
- playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
782
- // TODO CLEANUP!
783
- // Handle special player entity ID for bot entity in third person
784
- const key = String(entityPlayerId)
785
- // `oneSwing` is a one-shot event, not a persistent state: two swings in a row
786
- // are both 'oneSwing', so deduping by name would swallow every repeat swing
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
- if (playerObject.animation && (playerObject.animation as any).switchAnimationCallback !== undefined) {
803
- (playerObject.animation as any).switchAnimationCallback = () => {
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
- // Handle regular entities
816
- const playerObject = this.getPlayerObject(entityPlayerId)
817
- if (playerObject) {
818
- if (animation === 'oneSwing') {
819
- if (playerObject.animation && (playerObject.animation as any).swingArm) {
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
- // Handle player entity (for third person view) - fallback for backwards compatibility
839
- if (this.playerEntity?.playerObject) {
840
- const { playerObject: playerEntityObject } = this.playerEntity
841
- if (animation === 'oneSwing') {
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
- if (playerEntityObject.animation && (playerEntityObject.animation as any).switchAnimationCallback !== undefined) {
849
- (playerEntityObject.animation as any).switchAnimationCallback = () => {
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 worker = initThreeWorker(() => { })
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
  */