minecraft-renderer 0.1.54 → 0.1.55

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.54",
3
+ "version": "0.1.55",
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,30 @@
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('builds entitiesByName and items indexes from arrays', () => {
18
+ const mcData: Record<string, unknown> = {
19
+ entities: [{ name: 'bat', id: 1, type: 'mob' }],
20
+ items: [{ name: 'dirt', id: 2 }],
21
+ blocks: [{ name: 'stone', id: 3, minStateId: 48, maxStateId: 63, defaultState: 48 }],
22
+ }
23
+ augmentWorkerMcData(mcData)
24
+ expect((mcData.entitiesByName as Record<string, { name: string }>).bat?.name).toBe('bat')
25
+ expect((mcData.entities as Record<number, { id: number }>)[1]?.id).toBe(1)
26
+ expect((mcData.itemsByName as Record<string, unknown>).dirt).toBeDefined()
27
+ expect((mcData.items as Record<number, unknown>)[2]).toBeDefined()
28
+ expect((mcData.blocksByStateId as Record<number, unknown>)[48]).toBeDefined()
29
+ })
30
+ })
@@ -0,0 +1,93 @@
1
+ //@ts-nocheck
2
+ type McElement = Record<string, unknown>
3
+
4
+ function buildIndexFromArray<T extends McElement> (
5
+ array: T[],
6
+ field: keyof T
7
+ ): Record<string | number, T> {
8
+ return array.reduce<Record<string | number, T>>((index, element) => {
9
+ index[element[field] as string | number] = element
10
+ return index
11
+ }, {})
12
+ }
13
+
14
+ function buildIndexFromArrayWithRanges<T extends McElement> (
15
+ array: T[],
16
+ minField: keyof T,
17
+ maxField: keyof T
18
+ ): Record<number, T> {
19
+ return array.reduce<Record<number, T>>((index, element) => {
20
+ const min = element[minField] as number
21
+ const max = element[maxField] as number
22
+ for (let i = min; i <= max; i++) {
23
+ index[i] = element
24
+ }
25
+ return index
26
+ }, {})
27
+ }
28
+
29
+ function ensureBlockStateIds (blocks: McElement[]) {
30
+ if (!blocks.length) return
31
+ if ('minStateId' in blocks[0] && 'defaultState' in blocks[0]) return
32
+ for (const block of blocks) {
33
+ const id = block.id as number
34
+ block.minStateId = id << 4
35
+ block.maxStateId = (block.minStateId as number) + 15
36
+ block.defaultState = block.minStateId
37
+ }
38
+ }
39
+
40
+ function getSourceArray (
41
+ mcData: Record<string, unknown>,
42
+ arrayKey: string,
43
+ rawKey: string
44
+ ): McElement[] | undefined {
45
+ const fromArrayKey = mcData[arrayKey]
46
+ if (Array.isArray(fromArrayKey)) {
47
+ return fromArrayKey as McElement[]
48
+ }
49
+ const raw = mcData[rawKey]
50
+ if (Array.isArray(raw)) {
51
+ return raw as McElement[]
52
+ }
53
+ return undefined
54
+ }
55
+
56
+ export function augmentWorkerMcData (mcData: Record<string, unknown>) {
57
+ if (mcData.__workerIndexesBuilt) {
58
+ return mcData
59
+ }
60
+
61
+ const blocks = getSourceArray(mcData, 'blocksArray', 'blocks')
62
+ if (blocks?.length) {
63
+ ensureBlockStateIds(blocks)
64
+ mcData.blocksArray = blocks
65
+ mcData.blocks = buildIndexFromArray(blocks, 'id')
66
+ mcData.blocksByName = buildIndexFromArray(blocks, 'name')
67
+ mcData.blocksByStateId = buildIndexFromArrayWithRanges(blocks, 'minStateId', 'maxStateId')
68
+ }
69
+
70
+ const items = getSourceArray(mcData, 'itemsArray', 'items')
71
+ if (items?.length) {
72
+ mcData.itemsArray = items
73
+ mcData.itemsByName = buildIndexFromArray(items, 'name')
74
+ mcData.items = buildIndexFromArray(items, 'id')
75
+ }
76
+
77
+ const entities = getSourceArray(mcData, 'entitiesArray', 'entities')
78
+ if (entities?.length) {
79
+ mcData.entitiesArray = entities
80
+ mcData.entitiesByName = buildIndexFromArray(entities, 'name')
81
+ mcData.entities = buildIndexFromArray(entities, 'id')
82
+ }
83
+
84
+ const biomes = getSourceArray(mcData, 'biomesArray', 'biomes')
85
+ if (biomes?.length) {
86
+ mcData.biomesArray = biomes
87
+ mcData.biomes = buildIndexFromArray(biomes, 'id')
88
+ mcData.biomesByName = buildIndexFromArray(biomes, 'name')
89
+ }
90
+
91
+ mcData.__workerIndexesBuilt = true
92
+ return mcData
93
+ }
@@ -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
+ }
@@ -12,6 +12,7 @@ import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
12
12
  import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
13
13
  import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser'
14
14
  import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
15
+ import { isWebWorker } from '../three/documentRenderer'
15
16
  import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
16
17
  import { getLoadedItemDefinitionsStore } from 'mc-assets'
17
18
 
@@ -55,7 +56,15 @@ export class LoadedResourcesTransferrable {
55
56
  constructor(data?: any) {
56
57
  if (data) {
57
58
  Object.assign(this, data)
58
- this.mcData = MinecraftData(this.version)
59
+ }
60
+ if (this.version) {
61
+ const globalMc = (globalThis as { loadedData?: IndexedData, mcData?: IndexedData }).loadedData
62
+ ?? (globalThis as { mcData?: IndexedData }).mcData
63
+ if (isWebWorker && globalMc?.entitiesByName) {
64
+ this.mcData = globalMc
65
+ } else {
66
+ this.mcData = MinecraftData(this.version)
67
+ }
59
68
  // this.itemsRenderer = new ItemsRenderer(
60
69
  // this.version,
61
70
  // this.blockstatesModels,
@@ -70,8 +79,10 @@ export class LoadedResourcesTransferrable {
70
79
  for (const key in this) {
71
80
  if (!Object.prototype.hasOwnProperty.call(this, key)) continue
72
81
  if (typeof this[key] === 'function') continue
73
- if (key === 'itemsRenderer' || key === 'worldBlockProvider' || key === 'mcData') {
74
- // Skip these fields
82
+ if (
83
+ key === 'itemsRenderer' || key === 'worldBlockProvider' || key === 'mcData'
84
+ || key === 'itemsDefinitionsStore' || key === 'sourceItemDefinitionsJson'
85
+ ) {
75
86
  continue
76
87
  }
77
88
  cloned[key] = this[key as keyof this]
@@ -544,6 +544,7 @@ export class ChunkMeshManager {
544
544
  this.pendingNearReveal.set(chunkKey, Date.now())
545
545
  this.armNearRevealTimer(chunkKey)
546
546
  this.armExpectedGraceTimer(chunkKey)
547
+ this.tryRevealPending()
547
548
  return
548
549
  }
549
550
  this.flushChunkDisplay(chunkKey)
@@ -598,9 +599,8 @@ export class ChunkMeshManager {
598
599
  * `chunkKey` is not yet `finishedChunks=true`.
599
600
  *
600
601
  * Two regimes by `ageMs` (time spent in `pendingNearReveal`):
601
- * - Within `EXPECTED_NEAR_GRACE_MS`: walks every expected position in
602
- * the view-distance circle; missing-and-not-finished counts as a
603
- * blocker (catches "far worker beats near worker").
602
+ * - Within `EXPECTED_NEAR_GRACE_MS`: nearer columns in the circle that are
603
+ * loaded but not finished block (far worker beats near worker).
604
604
  * - After grace: only actually-loaded-but-not-finished columns block,
605
605
  * so a never-arriving column does not freeze the view.
606
606
  */
@@ -633,6 +633,7 @@ export class ChunkMeshManager {
633
633
  const oz = (playerCz + dCz) << 4
634
634
  const otherKey = `${ox},${oz}`
635
635
  if (otherKey === chunkKey) continue
636
+ if (!loadedChunks[otherKey]) continue
636
637
  if (!finishedChunks[otherKey]) return true
637
638
  }
638
639
  }
@@ -706,7 +706,7 @@ export class Entities {
706
706
  uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
707
707
  currentSkinUrls = {} as Record<string, string>
708
708
 
709
- private isCanvasBlank(canvas: HTMLCanvasElement): boolean {
709
+ private isCanvasBlank(canvas: HTMLCanvasElement | OffscreenCanvas): boolean {
710
710
  return !canvas.getContext('2d')
711
711
  ?.getImageData(0, 0, canvas.width, canvas.height).data
712
712
  .some(channel => channel !== 0)
@@ -802,12 +802,10 @@ export class Entities {
802
802
  let skinTexture: THREE.Texture
803
803
  let skinCanvas: OffscreenCanvas
804
804
  if (skinUrl === stevePngUrl) {
805
- skinTexture = await steveTexture
806
- const canvas = createCanvas(64, 64)
807
- const ctx = canvas.getContext('2d')
808
- if (!ctx) throw new Error('Failed to get context')
809
- ctx.drawImage(skinTexture.image, 0, 0)
810
- skinCanvas = canvas
805
+ const steveSkin = await loadSkinImage(stevePngUrl)
806
+ playerCustomSkinImage = steveSkin.image
807
+ skinTexture = new THREE.CanvasTexture(steveSkin.canvas)
808
+ skinCanvas = steveSkin.canvas
811
809
  } else {
812
810
  const { canvas, image } = await loadSkinImage(skinUrl)
813
811
  playerCustomSkinImage = image
@@ -822,12 +820,12 @@ export class Entities {
822
820
  playerObject.skin.modelType = inferModelType(skinCanvas)
823
821
  playerObject.skin['isCustom'] = skinUrl !== stevePngUrl
824
822
 
825
- let earsCanvas: HTMLCanvasElement | undefined
823
+ let earsCanvas: OffscreenCanvas | undefined
826
824
  if (!playerCustomSkinImage) {
827
825
  renderEars = false
828
826
  } else if (renderEars) {
829
- earsCanvas = document.createElement('canvas')
830
- loadEarsToCanvasFromSkin(earsCanvas, playerCustomSkinImage)
827
+ earsCanvas = createCanvas(64, 64)
828
+ loadEarsToCanvasFromSkin(earsCanvas as unknown as HTMLCanvasElement, playerCustomSkinImage)
831
829
  renderEars = !this.isCanvasBlank(earsCanvas)
832
830
  }
833
831
  if (renderEars) {
@@ -149,23 +149,33 @@ export const createGraphicsBackendBase = () => {
149
149
 
150
150
  const startMenuBackground = async (menuBackgroundStartOptions?: MenuBackgroundOptions) => {
151
151
  if (!documentRenderer) throw new Error('Document renderer not initialized')
152
- if (worldRenderer) return
153
152
 
154
- if (!menuBackgroundRenderer) {
155
- const mergedOptions: MenuBackgroundOptions = {
156
- ...initOptions.config.menuBackground,
157
- ...menuBackgroundStartOptions
158
- }
159
- menuBackgroundRenderer = new MenuBackgroundRenderer(
160
- documentRenderer,
161
- { ...initOptions },
162
- mergedOptions,
163
- !!process.env.SINGLE_FILE_BUILD_MODE
164
- )
165
- callModsMethod('menuBackgroundCreated', menuBackgroundRenderer)
166
- await menuBackgroundRenderer.start(mergedOptions)
167
- callModsMethod('menuBackgroundReady', menuBackgroundRenderer)
153
+ if (worldRenderer) {
154
+ worldRenderer.destroy()
155
+ worldRenderer = null
156
+ frameTimingCollector = null
157
+ ;(globalThis as any).world = undefined
158
+ ;(globalThis as any).frameTimingCollector = undefined
159
+ }
160
+
161
+ if (menuBackgroundRenderer) {
162
+ menuBackgroundRenderer.dispose()
163
+ menuBackgroundRenderer = null
164
+ }
165
+
166
+ const mergedOptions: MenuBackgroundOptions = {
167
+ ...initOptions.config.menuBackground,
168
+ ...menuBackgroundStartOptions
168
169
  }
170
+ menuBackgroundRenderer = new MenuBackgroundRenderer(
171
+ documentRenderer,
172
+ { ...initOptions },
173
+ mergedOptions,
174
+ !!process.env.SINGLE_FILE_BUILD_MODE
175
+ )
176
+ callModsMethod('menuBackgroundCreated', menuBackgroundRenderer)
177
+ await menuBackgroundRenderer.start(mergedOptions)
178
+ callModsMethod('menuBackgroundReady', menuBackgroundRenderer)
169
179
  }
170
180
 
171
181
  const startWorld = async (displayOptionsArg: DisplayWorldOptions) => {
@@ -2,9 +2,11 @@
2
2
  import * as THREE from 'three'
3
3
  import { GraphicsBackend, GraphicsBackendLoader } from '../graphicsBackend'
4
4
  import { useWorkerProxy, deepPrepareForTransfer, findProblemTransfer } from '../lib/workerProxy'
5
- import { meshersSendMcData } from '../lib/worldrendererCommon'
5
+ import { meshersSendMcDataAwait } from '../lib/worldrendererCommon'
6
6
  import { dynamicMcDataFiles } from '../lib/buildSharedConfig.mjs'
7
7
  import { addNewStat } from '../lib/ui/newStats'
8
+ import type { MenuBackgroundOptions } from './menuBackground/types'
9
+ import { MENU_BACKGROUND_MC_VERSION } from './menuBackground/shared'
8
10
  import { createGraphicsBackendBase, type ThreeJsBackendMethods } from './graphicsBackendBase'
9
11
  import { addCanvasForWorker } from './documentRenderer'
10
12
 
@@ -58,14 +60,36 @@ export const createGraphicsBackendOffThread: GraphicsBackendLoader = async (init
58
60
  const backend: GraphicsBackend = {
59
61
  id: 'threejs',
60
62
  displayName: `three.js ${THREE.REVISION}`,
61
- async startMenuBackground() { },
63
+ async startMenuBackground(menuBackgroundStartOptions?: MenuBackgroundOptions) {
64
+ const mcData = menuBackgroundStartOptions?.resourcesManager?.currentResources?.mcData
65
+ if (mcData) {
66
+ const workerThreeSendData = {
67
+ ...dynamicMcDataFiles,
68
+ items: 'itemsArray',
69
+ entities: 'entitiesArray',
70
+ }
71
+ await meshersSendMcDataAwait([worker], MENU_BACKGROUND_MC_VERSION, workerThreeSendData, mcData)
72
+ }
73
+ const prepared = deepPrepareForTransfer(menuBackgroundStartOptions ?? {}, worker)
74
+ try {
75
+ await proxy.startMenuBackground(structuredClone(prepared))
76
+ } catch (err) {
77
+ findProblemTransfer(prepared)
78
+ throw err
79
+ }
80
+ },
62
81
  async startWorld(options) {
63
82
  const workerThreeSendData = {
64
83
  ...dynamicMcDataFiles,
65
84
  items: 'itemsArray',
66
85
  entities: 'entitiesArray',
67
86
  }
68
- meshersSendMcData([worker], options.version, workerThreeSendData, options.resourcesManager.currentResources.mcData)
87
+ await meshersSendMcDataAwait(
88
+ [worker],
89
+ options.version,
90
+ workerThreeSendData,
91
+ options.resourcesManager.currentResources.mcData
92
+ )
69
93
  console.log('mc data sent to three worker')
70
94
 
71
95
  options.inWorldRenderingConfig['__syncToWorker'] = true
package/src/three/hand.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
- import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins'
4
- import { steveTexture } from './entities'
3
+ import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
5
4
 
6
5
 
7
6
  export const getMyHand = async (image?: string, userName?: string) => {
8
7
  let newMap: THREE.Texture
9
8
  if (!image && !userName) {
10
- newMap = await steveTexture
9
+ const { canvas } = await loadSkinImage(stevePngUrl)
10
+ newMap = new THREE.CanvasTexture(canvas)
11
11
  } else {
12
12
  if (!image) {
13
13
  image = await loadSkinFromUsername(userName!, 'skin')
@@ -0,0 +1,15 @@
1
+ //@ts-nocheck
2
+ export function menuBackgroundAssetUrl (...segments: string[]): string {
3
+ const relative = segments.filter(s => s.length > 0).join('/')
4
+ const base =
5
+ typeof globalThis.location !== 'undefined' && globalThis.location.href
6
+ ? globalThis.location.href
7
+ : typeof import.meta !== 'undefined' && import.meta.url
8
+ ? import.meta.url
9
+ : `/${relative}`
10
+ try {
11
+ return new URL(relative, base).href
12
+ } catch {
13
+ return `/${relative}`
14
+ }
15
+ }