minecraft-renderer 0.1.54 → 0.1.56

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.
Files changed (34) hide show
  1. package/dist/mesher.js +54 -54
  2. package/dist/mesher.js.map +3 -3
  3. package/dist/mesherWasm.js +46 -46
  4. package/dist/minecraft-renderer.js +61 -61
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +972 -972
  7. package/package.json +1 -1
  8. package/src/lib/buildWorkerMcDataIndexes.test.ts +48 -0
  9. package/src/lib/buildWorkerMcDataIndexes.ts +121 -0
  10. package/src/lib/items.ts +4 -3
  11. package/src/lib/utils.ts +1 -1
  12. package/src/lib/workerMessageSanitize.test.ts +14 -0
  13. package/src/lib/workerMessageSanitize.ts +48 -0
  14. package/src/lib/workerProxy.restore.test.ts +29 -0
  15. package/src/lib/workerProxy.ts +110 -36
  16. package/src/lib/worldrendererCommon.ts +25 -0
  17. package/src/resourcesManager/resourcesManager.ts +97 -11
  18. package/src/resourcesManager/resourcesManager.worker.test.ts +61 -0
  19. package/src/three/appShared.ts +1 -1
  20. package/src/three/chunkMeshManager.ts +4 -3
  21. package/src/three/entities.ts +27 -19
  22. package/src/three/entity/EntityMesh.ts +4 -10
  23. package/src/three/graphicsBackendBase.ts +25 -15
  24. package/src/three/graphicsBackendOffThread.ts +27 -3
  25. package/src/three/hand.ts +3 -3
  26. package/src/three/itemMesh.ts +20 -7
  27. package/src/three/menuBackground/assetUrl.ts +15 -0
  28. package/src/three/menuBackground/classic.ts +11 -5
  29. package/src/three/menuBackground/futuristic.ts +3 -0
  30. package/src/three/threeJsMedia.ts +31 -4
  31. package/src/three/threeJsUtils.ts +11 -0
  32. package/src/three/threeWorker.ts +12 -11
  33. package/src/three/worldRendererThree.ts +7 -0
  34. package/src/worldView/worldView.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,48 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import { augmentWorkerMcData } from './buildWorkerMcDataIndexes'
4
+
5
+ describe('augmentWorkerMcData', () => {
6
+ it('is idempotent when mcData is augmented twice (menu then world)', () => {
7
+ const mcData: Record<string, unknown> = {
8
+ entities: [{ name: 'bat', id: 1, type: 'mob' }],
9
+ blocks: [{ name: 'stone', id: 3, minStateId: 48, maxStateId: 63, defaultState: 48 }],
10
+ }
11
+ augmentWorkerMcData(mcData)
12
+ expect(() => augmentWorkerMcData(mcData)).not.toThrow()
13
+ expect(mcData.__workerIndexesBuilt).toBe(true)
14
+ expect(Array.isArray(mcData.blocksArray)).toBe(true)
15
+ })
16
+
17
+ it('keeps *Array sources after indexing (esbuild mc-data plugin reads blocksArray)', () => {
18
+ const mcData: Record<string, unknown> = {
19
+ blocks: [{ name: 'stone', id: 3, minStateId: 48, maxStateId: 63, defaultState: 48 }],
20
+ }
21
+ augmentWorkerMcData(mcData)
22
+ expect(Array.isArray(mcData.blocksArray)).toBe(true)
23
+ expect((mcData.blocksArray as unknown[]).length).toBe(1)
24
+ expect(Array.isArray(mcData.blocks)).toBe(false)
25
+ })
26
+
27
+ it('coerces valtio-style dense objects into arrays', () => {
28
+ const mcData: Record<string, unknown> = {
29
+ entities: { 0: { name: 'bat', id: 1, type: 'mob' } },
30
+ }
31
+ augmentWorkerMcData(mcData)
32
+ expect((mcData.entitiesByName as Record<string, { name: string }>).bat?.name).toBe('bat')
33
+ })
34
+
35
+ it('builds entitiesByName and items indexes from arrays', () => {
36
+ const mcData: Record<string, unknown> = {
37
+ entities: [{ name: 'bat', id: 1, type: 'mob' }],
38
+ items: [{ name: 'dirt', id: 2 }],
39
+ blocks: [{ name: 'stone', id: 3, minStateId: 48, maxStateId: 63, defaultState: 48 }],
40
+ }
41
+ augmentWorkerMcData(mcData)
42
+ expect((mcData.entitiesByName as Record<string, { name: string }>).bat?.name).toBe('bat')
43
+ expect((mcData.entities as Record<number, { id: number }>)[1]?.id).toBe(1)
44
+ expect((mcData.itemsByName as Record<string, unknown>).dirt).toBeDefined()
45
+ expect((mcData.items as Record<number, unknown>)[2]).toBeDefined()
46
+ expect((mcData.blocksByStateId as Record<number, unknown>)[48]).toBeDefined()
47
+ })
48
+ })
@@ -0,0 +1,121 @@
1
+ //@ts-nocheck
2
+ type McElement = Record<string, unknown>
3
+
4
+ function coerceDenseArray (value: unknown): McElement[] | undefined {
5
+ if (Array.isArray(value)) {
6
+ return value as McElement[]
7
+ }
8
+ if (!value || typeof value !== 'object') {
9
+ return undefined
10
+ }
11
+ const record = value as Record<string, McElement>
12
+ const keys = Object.keys(record).filter((k) => /^\d+$/.test(k)).map(Number).sort((a, b) => a - b)
13
+ if (!keys.length || keys[0] !== 0) {
14
+ return undefined
15
+ }
16
+ for (let i = 0; i < keys.length; i++) {
17
+ if (keys[i] !== i) {
18
+ return undefined
19
+ }
20
+ }
21
+ return keys.map((k) => record[String(k)])
22
+ }
23
+
24
+ function buildIndexFromArray<T extends McElement> (
25
+ array: T[],
26
+ field: keyof T
27
+ ): Record<string | number, T> {
28
+ if (!Array.isArray(array)) {
29
+ console.warn('[augmentWorkerMcData] buildIndexFromArray expected array, got', typeof array)
30
+ return {}
31
+ }
32
+ return array.reduce<Record<string | number, T>>((index, element) => {
33
+ index[element[field] as string | number] = element
34
+ return index
35
+ }, {})
36
+ }
37
+
38
+ function buildIndexFromArrayWithRanges<T extends McElement> (
39
+ array: T[],
40
+ minField: keyof T,
41
+ maxField: keyof T
42
+ ): Record<number, T> {
43
+ if (!Array.isArray(array)) {
44
+ console.warn('[augmentWorkerMcData] buildIndexFromArrayWithRanges expected array, got', typeof array)
45
+ return {}
46
+ }
47
+ return array.reduce<Record<number, T>>((index, element) => {
48
+ const min = element[minField] as number
49
+ const max = element[maxField] as number
50
+ for (let i = min; i <= max; i++) {
51
+ index[i] = element
52
+ }
53
+ return index
54
+ }, {})
55
+ }
56
+
57
+ function ensureBlockStateIds (blocks: McElement[]) {
58
+ if (!blocks.length) return
59
+ if ('minStateId' in blocks[0] && 'defaultState' in blocks[0]) return
60
+ for (const block of blocks) {
61
+ const id = block.id as number
62
+ block.minStateId = id << 4
63
+ block.maxStateId = (block.minStateId as number) + 15
64
+ block.defaultState = block.minStateId
65
+ }
66
+ }
67
+
68
+ function getSourceArray (
69
+ mcData: Record<string, unknown>,
70
+ arrayKey: string,
71
+ rawKey: string
72
+ ): McElement[] | undefined {
73
+ const fromArrayKey = coerceDenseArray(mcData[arrayKey])
74
+ if (fromArrayKey?.length) {
75
+ return fromArrayKey
76
+ }
77
+ const raw = coerceDenseArray(mcData[rawKey])
78
+ if (raw?.length) {
79
+ return raw
80
+ }
81
+ return undefined
82
+ }
83
+
84
+ export function augmentWorkerMcData (mcData: Record<string, unknown>) {
85
+ if (mcData.__workerIndexesBuilt) {
86
+ return mcData
87
+ }
88
+
89
+ const blocks = getSourceArray(mcData, 'blocksArray', 'blocks')
90
+ if (blocks?.length) {
91
+ ensureBlockStateIds(blocks)
92
+ mcData.blocksArray = blocks
93
+ mcData.blocks = buildIndexFromArray(blocks, 'id')
94
+ mcData.blocksByName = buildIndexFromArray(blocks, 'name')
95
+ mcData.blocksByStateId = buildIndexFromArrayWithRanges(blocks, 'minStateId', 'maxStateId')
96
+ }
97
+
98
+ const items = getSourceArray(mcData, 'itemsArray', 'items')
99
+ if (items?.length) {
100
+ mcData.itemsArray = items
101
+ mcData.itemsByName = buildIndexFromArray(items, 'name')
102
+ mcData.items = buildIndexFromArray(items, 'id')
103
+ }
104
+
105
+ const entities = getSourceArray(mcData, 'entitiesArray', 'entities')
106
+ if (entities?.length) {
107
+ mcData.entitiesArray = entities
108
+ mcData.entitiesByName = buildIndexFromArray(entities, 'name')
109
+ mcData.entities = buildIndexFromArray(entities, 'id')
110
+ }
111
+
112
+ const biomes = getSourceArray(mcData, 'biomesArray', 'biomes')
113
+ if (biomes?.length) {
114
+ mcData.biomesArray = biomes
115
+ mcData.biomes = buildIndexFromArray(biomes, 'id')
116
+ mcData.biomesByName = buildIndexFromArray(biomes, 'name')
117
+ }
118
+
119
+ mcData.__workerIndexesBuilt = true
120
+ return mcData
121
+ }
package/src/lib/items.ts CHANGED
@@ -4,7 +4,7 @@ import nbt from 'prismarine-nbt'
4
4
  import { fromFormattedString } from '@xmcl/text-component'
5
5
  import { getItemSelector } from '../playerState/playerState'
6
6
  import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
7
- import { ResourcesManagerCommon } from '../resourcesManager'
7
+ import { getItemsDefinitionsStoreForRender, ResourcesManagerCommon } from '../resourcesManager'
8
8
  import { ItemSpecificContextProperties } from '../playerState/types'
9
9
  import { PlayerStateRenderer } from '../playerState/playerState'
10
10
 
@@ -131,9 +131,10 @@ export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpec
131
131
  const itemSelector = getItemSelector(playerState, {
132
132
  ...specificProps
133
133
  })
134
- const modelFromDef = getItemDefinition(resourcesManager.currentResources!.itemsDefinitionsStore, {
134
+ const resources = resourcesManager.currentResources!
135
+ const modelFromDef = getItemDefinition(getItemsDefinitionsStoreForRender(resources), {
135
136
  name: itemModelName,
136
- version: resourcesManager.currentResources!.version,
137
+ version: resources.version,
137
138
  properties: itemSelector
138
139
  })?.model
139
140
  const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName
package/src/lib/utils.ts CHANGED
@@ -42,7 +42,7 @@ const detectFullOffscreenCanvasSupport = () => {
42
42
  const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport()
43
43
 
44
44
  export const createCanvas = (width: number, height: number): OffscreenCanvas => {
45
- if (hasFullOffscreenCanvasSupport) {
45
+ if (hasFullOffscreenCanvasSupport || typeof document === 'undefined') {
46
46
  return new OffscreenCanvas(width, height)
47
47
  }
48
48
  const canvas = document.createElement('canvas')
@@ -0,0 +1,14 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import { sanitizeForWorkerPostMessage } from './workerMessageSanitize'
4
+
5
+ describe('sanitizeForWorkerPostMessage', () => {
6
+ it('drops functions such as debug loggers', () => {
7
+ const debug = Object.assign(() => {}, { enabled: false })
8
+ const entity = { id: 1, name: 'zombie', debug, position: { x: 1, y: 2, z: 3 } }
9
+ const sanitized = sanitizeForWorkerPostMessage(entity) as Record<string, unknown>
10
+ expect(sanitized.debug).toBeUndefined()
11
+ expect(sanitized.id).toBe(1)
12
+ expect(() => structuredClone(sanitized)).not.toThrow()
13
+ })
14
+ })
@@ -0,0 +1,48 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Strip non–structured-clone values before Worker.postMessage (e.g. mineflayer `debug` on entities).
4
+ */
5
+ export function sanitizeForWorkerPostMessage (value: unknown, depth = 0): unknown {
6
+ if (depth > 16) return undefined
7
+ if (value === null || value === undefined) return value
8
+
9
+ const t = typeof value
10
+ if (t === 'function' || t === 'symbol') return undefined
11
+ if (t === 'bigint') return value.toString()
12
+ if (t !== 'object') return value
13
+
14
+ if (value instanceof ArrayBuffer) return value
15
+ if (ArrayBuffer.isView(value)) {
16
+ const view = value as ArrayBufferView
17
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)
18
+ }
19
+ if (value instanceof Date) return value.toISOString()
20
+
21
+ if (Array.isArray(value)) {
22
+ return value
23
+ .map((entry) => sanitizeForWorkerPostMessage(entry, depth + 1))
24
+ .filter((entry) => entry !== undefined)
25
+ }
26
+
27
+ const record = value as Record<string, unknown>
28
+ if (
29
+ typeof record.x === 'number'
30
+ && typeof record.y === 'number'
31
+ && typeof record.z === 'number'
32
+ && !('w' in record)
33
+ ) {
34
+ return { x: record.x, y: record.y, z: record.z }
35
+ }
36
+
37
+ const out: Record<string, unknown> = {}
38
+ for (const key of Object.keys(record)) {
39
+ if (key === '_client' || key === '_events' || key === '_eventsCount') continue
40
+ const sanitized = sanitizeForWorkerPostMessage(record[key], depth + 1)
41
+ if (sanitized !== undefined) out[key] = sanitized
42
+ }
43
+ return out
44
+ }
45
+
46
+ export function sanitizeWorkerEventArgs (args: unknown[]): unknown[] {
47
+ return args.map((arg) => sanitizeForWorkerPostMessage(arg))
48
+ }
@@ -0,0 +1,29 @@
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
+ })
@@ -117,6 +117,10 @@ const getSyncId = () => {
117
117
  return Math.random().toString(36).slice(2, 15) + Math.random().toString(36).slice(2, 15)
118
118
  }
119
119
 
120
+ const applySyncPatch = (target: any, patch: any, worker: Worker) => {
121
+ Object.assign(target, restoreTransferred(patch, [], worker, false))
122
+ }
123
+
120
124
  const setupObjectSync = (obj: any, originalObj: any, worker: Worker, isValtio: boolean, debugKey: string) => {
121
125
  if (!obj['__syncToWorker'] && !obj['__syncFromWorker'] && !isValtio) return
122
126
 
@@ -141,13 +145,45 @@ const setupObjectSync = (obj: any, originalObj: any, worker: Worker, isValtio: b
141
145
  worker.addEventListener('message', (event: any) => {
142
146
  if (event.data.type === 'sync' && event.data.syncId === syncId) {
143
147
  currentWorkerSyncStats.fromWorker++
144
- Object.assign(originalObj, event.data.value)
148
+ applySyncPatch(originalObj, event.data.value, worker)
145
149
  }
146
150
  })
147
151
  }
148
152
  }
149
153
 
154
+ const serializeMapForTransfer = (map: Map<unknown, unknown>) => ({
155
+ __restorer: 'Map',
156
+ __mapEntries: Array.from(map.entries()),
157
+ })
158
+
159
+ const serializeSetForTransfer = (set: Set<unknown>) => ({
160
+ __restorer: 'Set',
161
+ __setValues: [...set],
162
+ })
163
+
164
+ const isSetLike = (value: unknown): value is Set<unknown> => {
165
+ return value instanceof Set || Object.prototype.toString.call(value) === '[object Set]'
166
+ }
167
+
168
+ const isMapLike = (value: unknown): value is Map<unknown, unknown> => {
169
+ return value instanceof Map || Object.prototype.toString.call(value) === '[object Map]'
170
+ }
171
+
172
+ const iterableFromPlainObject = (obj: Record<string, unknown>) => {
173
+ return Object.keys(obj)
174
+ .filter(k => !k.startsWith('__'))
175
+ .sort((a, b) => Number(a) - Number(b))
176
+ .map(k => obj[k])
177
+ }
178
+
150
179
  const cloneValtioObject = (obj: any) => {
180
+ if (isMapLike(obj)) {
181
+ return serializeMapForTransfer(obj)
182
+ }
183
+ if (isSetLike(obj)) {
184
+ return serializeSetForTransfer(obj)
185
+ }
186
+
151
187
  if (getVersion(obj) === undefined) {
152
188
  return obj
153
189
  }
@@ -179,16 +215,19 @@ export const deepPrepareForTransfer = (obj: any, worker: Worker, autoRemoveMetho
179
215
  continue
180
216
  }
181
217
 
182
- // print a warning for Date, RegExp, Map, WeakMap, WeakSet
183
- if (obj[key] instanceof Date || obj[key] instanceof RegExp || obj[key] instanceof Map || obj[key] instanceof WeakMap || obj[key] instanceof WeakSet) {
218
+ // print a warning for Date, RegExp, WeakMap, WeakSet
219
+ if (obj[key] instanceof Date || obj[key] instanceof RegExp || obj[key] instanceof WeakMap || obj[key] instanceof WeakSet) {
184
220
  console.warn(`Warning: ${key} is a ${typeof obj[key]}, which is not supported for transfer.`)
185
221
  }
186
222
 
187
223
  // default restorers main -> worker
224
+ if (isMapLike(obj[key])) {
225
+ newObj[key] = serializeMapForTransfer(obj[key])
226
+ continue
227
+ }
188
228
  // Set (only primitive values)
189
- if (obj[key] instanceof Set) {
190
- newObj[key] = [...obj[key]]
191
- newObj[key]['__restorer'] = 'Set'
229
+ if (isSetLike(obj[key])) {
230
+ newObj[key] = serializeSetForTransfer(obj[key])
192
231
  continue
193
232
  }
194
233
  if (obj[key] instanceof Vec3) {
@@ -253,7 +292,7 @@ const receiveSyncedObject = (obj: any, worker: Worker, debugKey: string) => {
253
292
  if (obj['__syncToWorker']) {
254
293
  worker.addEventListener('message', (event: any) => {
255
294
  if (event.data.type === 'sync' && event.data.syncId === syncId) {
256
- Object.assign(obj, event.data.value)
295
+ applySyncPatch(obj, event.data.value, worker)
257
296
  }
258
297
  })
259
298
  }
@@ -275,10 +314,36 @@ const receiveSyncedObject = (obj: any, worker: Worker, debugKey: string) => {
275
314
  }
276
315
 
277
316
  const defaultRestorers = [
317
+ {
318
+ restorerName: 'Map',
319
+ restoreTransferred(obj, _worker: Worker) {
320
+ if (Array.isArray(obj)) {
321
+ return new Map(obj)
322
+ }
323
+ const raw = obj.__mapEntries ?? obj.entries
324
+ if (Array.isArray(raw)) {
325
+ return new Map(raw)
326
+ }
327
+ if (raw != null && typeof raw === 'object' && typeof raw !== 'function') {
328
+ return new Map(Object.entries(raw as Record<string, unknown>))
329
+ }
330
+ return new Map()
331
+ }
332
+ },
278
333
  {
279
334
  restorerName: 'Set',
280
- restoreTransferred(obj, worker: Worker) {
281
- return new Set(obj)
335
+ restoreTransferred(obj, _worker: Worker) {
336
+ if (Array.isArray(obj)) {
337
+ return new Set(obj)
338
+ }
339
+ const raw = obj.__setValues ?? obj.values
340
+ if (Array.isArray(raw)) {
341
+ return new Set(raw)
342
+ }
343
+ if (raw != null && typeof raw === 'object' && typeof raw !== 'function') {
344
+ return new Set(iterableFromPlainObject(raw as Record<string, unknown>))
345
+ }
346
+ return new Set()
282
347
  }
283
348
  },
284
349
  {
@@ -296,41 +361,50 @@ export const addDefaultRestorer = (restorer: { restorerName: string, restoreTran
296
361
  export const restoreTransferred = (obj: any, restorersArg: any[], worker: Worker, errorHandler: ((error: Error) => void) | boolean = true) => {
297
362
  const restorers = [...defaultRestorers, ...restorersArg]
298
363
 
299
- for (const key in obj) {
300
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
301
- if (!obj[key]) continue
364
+ const restoreValue = (value: any, debugKey: string): any => {
365
+ if (value == null || typeof value !== 'object') {
366
+ return value
367
+ }
302
368
 
303
- if (typeof obj[key] === 'object' && obj[key] !== null) {
304
- restoreTransferred(obj[key], restorers, worker, errorHandler)
369
+ if (value['__restorer']) {
370
+ const restorer = restorers.find(r => {
371
+ return r.restorerName ? r.restorerName === value['__restorer'] : r.name === value['__restorer']
372
+ })
373
+ if (restorer) {
374
+ return restorer.restoreTransferred(value, worker)
305
375
  }
306
-
307
- if (obj[key]['__restorer']) {
308
- // find restorer
309
- const restorer = restorers.find(restorer => {
310
- return restorer.restorerName ? restorer.restorerName === obj[key]['__restorer'] : restorer.name === obj[key]['__restorer']
311
- })
312
- if (restorer) {
313
- obj[key] = restorer.restoreTransferred(obj[key], worker)
314
- } else {
315
- const error = new Error(`Restorer ${obj[key]['__restorer']} not found`)
316
- if (typeof errorHandler === 'function') {
317
- errorHandler(error)
318
- } else if (errorHandler) {
319
- throw error
320
- } else {
321
- console.error(error)
322
- }
323
- }
376
+ const error = new Error(`Restorer ${value['__restorer']} not found`)
377
+ if (typeof errorHandler === 'function') {
378
+ errorHandler(error)
379
+ } else if (errorHandler) {
380
+ throw error
381
+ } else {
382
+ console.error(error)
324
383
  }
384
+ return value
385
+ }
386
+
387
+ if (Array.isArray(value)) {
388
+ return value.map((item, index) => restoreValue(item, `${debugKey}[${index}]`))
389
+ }
325
390
 
326
- if (obj[key]['__valtio']) {
327
- obj[key] = proxy(obj[key])
391
+ for (const key in value) {
392
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue
393
+ const child = value[key]
394
+ if (child != null && typeof child === 'object') {
395
+ value[key] = restoreValue(child, `${debugKey}.${key}`)
328
396
  }
397
+ }
329
398
 
330
- receiveSyncedObject(obj[key], worker, key)
399
+ if (value['__valtio']) {
400
+ value = proxy(value)
331
401
  }
402
+
403
+ receiveSyncedObject(value, worker, debugKey)
404
+ return value
332
405
  }
333
- return obj
406
+
407
+ return restoreValue(obj, 'root')
334
408
  }
335
409
 
336
410
  // const workerProxy = createWorkerProxy({
@@ -1295,3 +1295,28 @@ export const meshersSendMcData = (workers: Worker[], version: string, mcDataKeys
1295
1295
  worker.postMessage({ type: 'mcData', mcData })
1296
1296
  }
1297
1297
  }
1298
+
1299
+ /** Wait for worker `mcDataApplied` after {@link meshersSendMcData}. */
1300
+ export const meshersSendMcDataAwait = (
1301
+ workers: Worker[],
1302
+ version: string,
1303
+ mcDataKeys = dynamicMcDataFiles,
1304
+ mcDataFull: IndexedData,
1305
+ timeoutMs = 10_000
1306
+ ): Promise<void> => {
1307
+ return Promise.all(workers.map(worker => new Promise<void>((resolve, reject) => {
1308
+ const timeout = setTimeout(() => {
1309
+ worker.removeEventListener('message', handler as EventListener)
1310
+ reject(new Error(`mcData transfer timeout (${timeoutMs}ms)`))
1311
+ }, timeoutMs)
1312
+ const handler = ({ data }: MessageEvent) => {
1313
+ if (data?.type === 'mcDataApplied') {
1314
+ clearTimeout(timeout)
1315
+ worker.removeEventListener('message', handler as EventListener)
1316
+ resolve()
1317
+ }
1318
+ }
1319
+ worker.addEventListener('message', handler as EventListener)
1320
+ meshersSendMcData([worker], version, mcDataKeys, mcDataFull)
1321
+ }))).then(() => undefined)
1322
+ }