minecraft-renderer 0.1.55 → 0.1.57

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.55",
3
+ "version": "0.1.57",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -260,7 +260,7 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
260
260
  min: 30,
261
261
  max: 110,
262
262
  unit: '°',
263
- text: 'FOV (Field of View)'
263
+ text: 'FOV'
264
264
  },
265
265
  gpuPreference: {
266
266
  text: 'GPU preference',
@@ -14,6 +14,24 @@ describe('augmentWorkerMcData', () => {
14
14
  expect(Array.isArray(mcData.blocksArray)).toBe(true)
15
15
  })
16
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
+
17
35
  it('builds entitiesByName and items indexes from arrays', () => {
18
36
  const mcData: Record<string, unknown> = {
19
37
  entities: [{ name: 'bat', id: 1, type: 'mob' }],
@@ -1,10 +1,34 @@
1
1
  //@ts-nocheck
2
2
  type McElement = Record<string, unknown>
3
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
+
4
24
  function buildIndexFromArray<T extends McElement> (
5
25
  array: T[],
6
26
  field: keyof T
7
27
  ): Record<string | number, T> {
28
+ if (!Array.isArray(array)) {
29
+ console.warn('[augmentWorkerMcData] buildIndexFromArray expected array, got', typeof array)
30
+ return {}
31
+ }
8
32
  return array.reduce<Record<string | number, T>>((index, element) => {
9
33
  index[element[field] as string | number] = element
10
34
  return index
@@ -16,6 +40,10 @@ function buildIndexFromArrayWithRanges<T extends McElement> (
16
40
  minField: keyof T,
17
41
  maxField: keyof T
18
42
  ): Record<number, T> {
43
+ if (!Array.isArray(array)) {
44
+ console.warn('[augmentWorkerMcData] buildIndexFromArrayWithRanges expected array, got', typeof array)
45
+ return {}
46
+ }
19
47
  return array.reduce<Record<number, T>>((index, element) => {
20
48
  const min = element[minField] as number
21
49
  const max = element[maxField] as number
@@ -42,13 +70,13 @@ function getSourceArray (
42
70
  arrayKey: string,
43
71
  rawKey: string
44
72
  ): McElement[] | undefined {
45
- const fromArrayKey = mcData[arrayKey]
46
- if (Array.isArray(fromArrayKey)) {
47
- return fromArrayKey as McElement[]
73
+ const fromArrayKey = coerceDenseArray(mcData[arrayKey])
74
+ if (fromArrayKey?.length) {
75
+ return fromArrayKey
48
76
  }
49
- const raw = mcData[rawKey]
50
- if (Array.isArray(raw)) {
51
- return raw as McElement[]
77
+ const raw = coerceDenseArray(mcData[rawKey])
78
+ if (raw?.length) {
79
+ return raw
52
80
  }
53
81
  return undefined
54
82
  }
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
+ }
@@ -14,7 +14,8 @@ import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser'
14
14
  import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
15
15
  import { isWebWorker } from '../three/documentRenderer'
16
16
  import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
17
- import { getLoadedItemDefinitionsStore } from 'mc-assets'
17
+ import { getLoadedItemDefinitionsStore } from 'mc-assets/dist/stores'
18
+ import { sanitizeWorkerEventArgs } from '../lib/workerMessageSanitize'
18
19
 
19
20
  type ResourceManagerEvents = {
20
21
  assetsTexturesUpdated: () => void
@@ -22,16 +23,43 @@ type ResourceManagerEvents = {
22
23
  assetsInventoryReady: () => void
23
24
  }
24
25
 
26
+ type ItemDefinitionsStore = ReturnType<typeof getLoadedItemDefinitionsStore>
27
+
28
+ let workerItemDefinitionsStore: ItemDefinitionsStore | null = null
29
+
30
+ export function getItemsDefinitionsStoreForRender (
31
+ resources: LoadedResourcesTransferrable
32
+ ): ItemDefinitionsStore {
33
+ if (!isWebWorker) {
34
+ ensureItemsDefinitionsStore(resources)
35
+ return resources.itemsDefinitionsStore
36
+ }
37
+ if (!workerItemDefinitionsStore || typeof workerItemDefinitionsStore.get !== 'function') {
38
+ workerItemDefinitionsStore = getLoadedItemDefinitionsStore(itemDefinitionsJson)
39
+ }
40
+ return workerItemDefinitionsStore
41
+ }
42
+
43
+ export function ensureItemsDefinitionsStore (resources: LoadedResourcesTransferrable): void {
44
+ if (isWebWorker) return
45
+ const store = resources.itemsDefinitionsStore as { get?: unknown }
46
+ if (typeof store?.get === 'function') return
47
+ resources.itemsDefinitionsStore = getLoadedItemDefinitionsStore(
48
+ resources.sourceItemDefinitionsJson ?? itemDefinitionsJson
49
+ )
50
+ }
51
+
25
52
  export class LoadedResourcesTransferrable {
26
53
  // todo transfer instead!
27
54
  readonly sourceItemDefinitionsJson: any = itemDefinitionsJson
28
- readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
55
+ itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
29
56
 
30
57
  allReady = false
31
58
  // Atlas parsers
32
59
  itemsAtlasImage!: ImageBitmap
33
60
  blocksAtlasImage!: ImageBitmap
34
61
  blocksAtlasJson!: ItemsAtlasesOutputJson
62
+ itemsAtlasJson!: ItemsAtlasesOutputJson
35
63
  // User data (specific to current resourcepack/version)
36
64
  customBlockStates?: Record<string, any>
37
65
  customModels?: Record<string, any>
@@ -55,7 +83,11 @@ export class LoadedResourcesTransferrable {
55
83
 
56
84
  constructor(data?: any) {
57
85
  if (data) {
58
- Object.assign(this, data)
86
+ const safe = { ...data }
87
+ delete safe.itemsDefinitionsStore
88
+ delete safe.sourceItemDefinitionsJson
89
+ Object.assign(this, safe)
90
+ ensureItemsDefinitionsStore(this)
59
91
  }
60
92
  if (this.version) {
61
93
  const globalMc = (globalThis as { loadedData?: IndexedData, mcData?: IndexedData }).loadedData
@@ -89,6 +121,8 @@ export class LoadedResourcesTransferrable {
89
121
  }
90
122
 
91
123
  cloned.customTextures = {}
124
+ delete cloned.itemsDefinitionsStore
125
+ delete cloned.sourceItemDefinitionsJson
92
126
  return cloned as LoadedResourcesTransferrable
93
127
  }
94
128
 
@@ -117,10 +151,36 @@ const STABLE_MODELS_VERSION = '1.21.4'
117
151
  export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) {
118
152
  static restorerName = 'ResourcesManager'
119
153
 
154
+ rebuildWorkerRenderers (resources: LoadedResourcesTransferrable): void {
155
+ if (!isWebWorker) return
156
+ if (!resources.version || !resources.blockstatesModels || !resources.blocksAtlasJson) return
157
+
158
+ this.blocksAtlasParser = new AtlasParser({ latest: resources.blocksAtlasJson }, '')
159
+ if (resources.itemsAtlasJson) {
160
+ this.itemsAtlasParser = new AtlasParser({ latest: resources.itemsAtlasJson }, '')
161
+ } else if (!this.itemsAtlasParser?.atlas?.latest) {
162
+ if (!this.sourceItemsAtlases || Object.keys(this.sourceItemsAtlases).length === 0) return
163
+ this.itemsAtlasParser = new AtlasParser(this.sourceItemsAtlases, '')
164
+ }
165
+
166
+ resources.itemsRenderer = new ItemsRenderer(
167
+ resources.version,
168
+ resources.blockstatesModels,
169
+ this.itemsAtlasParser,
170
+ this.blocksAtlasParser
171
+ )
172
+ resources.worldBlockProvider = worldBlockProvider(
173
+ resources.blockstatesModels,
174
+ this.blocksAtlasParser.atlas,
175
+ STABLE_MODELS_VERSION
176
+ )
177
+ }
178
+
120
179
  static restoreTransferred(data: any, worker?: Worker) {
121
180
  const resourcesManager = new ResourcesManager()
122
- const upResources = (data) => {
123
- resourcesManager.currentResources = new LoadedResourcesTransferrable(data)
181
+ const upResources = (transferData: LoadedResourcesTransferrable) => {
182
+ resourcesManager.currentResources = new LoadedResourcesTransferrable(transferData)
183
+ resourcesManager.rebuildWorkerRenderers(resourcesManager.currentResources)
124
184
  }
125
185
  upResources(data.currentResources)
126
186
  if (worker) {
@@ -139,6 +199,17 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
139
199
  return resourcesManager
140
200
  }
141
201
 
202
+ enrichTransferSnapshot (transfer?: LoadedResourcesTransferrable): LoadedResourcesTransferrable | undefined {
203
+ if (!transfer) return transfer
204
+ if (this.itemsAtlasParser?.atlas?.latest) {
205
+ transfer.itemsAtlasJson = this.itemsAtlasParser.atlas.latest
206
+ }
207
+ if (this.blocksAtlasParser?.atlas?.latest) {
208
+ transfer.blocksAtlasJson = this.blocksAtlasParser.atlas.latest
209
+ }
210
+ return transfer
211
+ }
212
+
142
213
  prepareForTransfer(worker?: Worker) {
143
214
  if (worker) {
144
215
  // todo do it automatically
@@ -149,21 +220,24 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
149
220
  class: ResourcesManager.restorerName,
150
221
  type: 'event',
151
222
  eventName,
152
- args,
223
+ args: sanitizeWorkerEventArgs(args),
153
224
  })
154
225
  // todo handle assetsInventoryReady
155
226
  if (eventName === 'assetsTexturesUpdated' || eventName === 'assetsInventoryReady') {
227
+ const currentResources = this.enrichTransferSnapshot(
228
+ this.currentResources?.prepareForTransfer(),
229
+ )
156
230
  worker.postMessage({
157
231
  class: ResourcesManager.restorerName,
158
232
  type: 'newResources',
159
- currentResources: this.currentResources?.prepareForTransfer(),
233
+ currentResources,
160
234
  })
161
235
  }
162
236
  }) as any
163
237
  }
164
238
  return {
165
239
  __restorer: ResourcesManager.restorerName,
166
- currentResources: this.currentResources?.prepareForTransfer(),
240
+ currentResources: this.enrichTransferSnapshot(this.currentResources?.prepareForTransfer()),
167
241
  }
168
242
  }
169
243
 
@@ -316,6 +390,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
316
390
 
317
391
  this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
318
392
  resources.itemsAtlasImage = await createImageBitmap(itemsCanvas)
393
+ resources.itemsAtlasJson = this.itemsAtlasParser.atlas.latest
319
394
  }
320
395
 
321
396
  async generateGuiTextures() {
@@ -0,0 +1,61 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
4
+ import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
5
+ import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
6
+ import { AtlasParser } from 'mc-assets/dist/atlasParser'
7
+ import {
8
+ getItemsDefinitionsStoreForRender,
9
+ LoadedResourcesTransferrable,
10
+ ResourcesManager,
11
+ } from './resourcesManager'
12
+
13
+ vi.mock('../three/documentRenderer', () => ({
14
+ isWebWorker: true,
15
+ }))
16
+
17
+ describe('ResourcesManager.rebuildWorkerRenderers', () => {
18
+ it('creates ItemsRenderer with working modelsStore.get in worker context', () => {
19
+ const blocksAtlasParser = new AtlasParser(blocksAtlases as any, '')
20
+ const itemsAtlasParser = new AtlasParser(itemsAtlases as any, '')
21
+ const resources = new LoadedResourcesTransferrable({
22
+ version: '1.21.4',
23
+ texturesVersion: '1.21.4',
24
+ blockstatesModels,
25
+ blocksAtlasJson: blocksAtlasParser.atlas.latest,
26
+ itemsAtlasJson: itemsAtlasParser.atlas.latest,
27
+ allReady: true,
28
+ })
29
+
30
+ const manager = new ResourcesManager()
31
+ manager.rebuildWorkerRenderers(resources)
32
+
33
+ expect(resources.itemsRenderer).toBeDefined()
34
+ expect(resources.worldBlockProvider).toBeDefined()
35
+ const tex = resources.itemsRenderer!.getItemTexture('item/missing_texture')
36
+ expect(tex).toBeDefined()
37
+ })
38
+
39
+ it('getItemsDefinitionsStoreForRender returns store with .get in worker', () => {
40
+ const resources = new LoadedResourcesTransferrable({
41
+ version: '1.21.4',
42
+ blockstatesModels,
43
+ blocksAtlasJson: (new AtlasParser(blocksAtlases as any, '')).atlas.latest,
44
+ itemsDefinitionsStore: { data: { latest: {} }, inclusive: false },
45
+ })
46
+ const store = getItemsDefinitionsStoreForRender(resources)
47
+ expect(typeof store.get).toBe('function')
48
+ })
49
+
50
+ it('falls back to bundled items atlas when itemsAtlasJson is missing', () => {
51
+ const resources = new LoadedResourcesTransferrable({
52
+ version: '1.21.4',
53
+ blockstatesModels,
54
+ blocksAtlasJson: (new AtlasParser(blocksAtlases as any, '')).atlas.latest,
55
+ })
56
+ const manager = new ResourcesManager()
57
+ manager.rebuildWorkerRenderers(resources)
58
+ expect(resources.itemsRenderer).toBeDefined()
59
+ expect(resources.itemsRenderer!.getItemTexture('item/missing_texture')).toBeDefined()
60
+ })
61
+ })
@@ -3,7 +3,7 @@ import { BlockModel } from 'mc-assets/dist/types'
3
3
  import { ItemSpecificContextProperties } from '../playerState/types'
4
4
  import { PlayerStateRenderer } from '../playerState/playerState'
5
5
  import { GeneralInputItem, getItemModelName } from '../lib/items'
6
- import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
6
+ import { ResourcesManagerTransferred } from '../resourcesManager'
7
7
  import { renderSlot } from './renderSlot'
8
8
 
9
9
  export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): {
@@ -22,7 +22,7 @@ import { createItemMesh } from './itemMesh'
22
22
  import * as Entity from './entity/EntityMesh'
23
23
  import { getMesh } from './entity/EntityMesh'
24
24
  import { WalkingGeneralSwing } from './entity/animations'
25
- import { disposeObject, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
25
+ import { disposeObject, loadNearestFilterTexture, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
26
26
  import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
27
27
  import { WorldRendererThree } from './worldRendererThree'
28
28
  import { IndexedData } from 'minecraft-data'
@@ -213,6 +213,19 @@ const nametags = {}
213
213
 
214
214
  const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
215
215
 
216
+ function metadataAsArray (metadata: unknown): unknown[] | undefined {
217
+ if (metadata == null) return undefined
218
+ if (Array.isArray(metadata)) return metadata
219
+ if (typeof metadata === 'object') {
220
+ const record = metadata as Record<string, unknown>
221
+ const keys = Object.keys(record).filter((k) => /^\d+$/.test(k)).map(Number).sort((a, b) => a - b)
222
+ if (keys.length && keys[0] === 0 && keys.every((k, i) => k === i)) {
223
+ return keys.map((k) => record[String(k)])
224
+ }
225
+ }
226
+ return undefined
227
+ }
228
+
216
229
  function getEntityMesh(mcData: IndexedData | undefined, entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree, options: { fontFamily: string }, overrides) {
217
230
  if (entity.name) {
218
231
  try {
@@ -1086,7 +1099,7 @@ export class Entities {
1086
1099
  ? { name: entity.name }
1087
1100
  : entity.name === 'falling_block'
1088
1101
  ? { blockState: entity['objectData'] }
1089
- : entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
1102
+ : metadataAsArray(entity.metadata)?.find((m: any) => typeof m === 'object' && m?.itemCount)
1090
1103
  if (item) {
1091
1104
  const object = this.getItemMesh(item, {
1092
1105
  'minecraft:display_context': 'ground',
@@ -1177,7 +1190,8 @@ export class Entities {
1177
1190
 
1178
1191
  const meta = getGeneralEntitiesMetadata(entity, this.mcData)
1179
1192
 
1180
- const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
1193
+ const meta0 = metadataAsArray(entity.metadata)?.[0] ?? (entity.metadata as { 0?: unknown } | undefined)?.[0]
1194
+ const isInvisible = ((meta0 ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
1181
1195
  for (const child of mesh!.children ?? []) {
1182
1196
  if (child.name !== 'nametag') {
1183
1197
  child.visible = !isInvisible
@@ -1512,12 +1526,8 @@ export class Entities {
1512
1526
  }
1513
1527
 
1514
1528
  loadMap(data: any) {
1515
- const texture = new THREE.TextureLoader().load(data)
1516
- if (texture) {
1517
- texture.magFilter = THREE.NearestFilter
1518
- texture.minFilter = THREE.NearestFilter
1519
- texture.needsUpdate = true
1520
- }
1529
+ const texture = loadNearestFilterTexture(data)
1530
+ texture.needsUpdate = true
1521
1531
  return texture
1522
1532
  }
1523
1533
 
@@ -8,7 +8,7 @@ import ocelotPng from 'mc-assets/dist/other-textures/latest/entity/cat/ocelot.pn
8
8
  import arrowTexture from 'mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
9
9
  import spectralArrowTexture from 'mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
10
10
  import tippedArrowTexture from 'mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
11
- import { loadTexture } from '../threeJsUtils'
11
+ import { loadNearestFilterTexture, loadTexture } from '../threeJsUtils'
12
12
  import { WorldRendererThree } from '../worldRendererThree'
13
13
  import entities from './entities.json'
14
14
  import { externalModels } from './objModels'
@@ -505,9 +505,7 @@ export class EntityMesh {
505
505
  partRoot.position.set(x, y, z)
506
506
  }
507
507
  if (metadata?.texture) {
508
- const texture = new THREE.TextureLoader().load(metadata.texture)
509
- texture.minFilter = THREE.NearestFilter
510
- texture.magFilter = THREE.NearestFilter
508
+ const texture = loadNearestFilterTexture(metadata.texture)
511
509
  partRoot.traverse((child) => {
512
510
  if (child instanceof THREE.Mesh) {
513
511
  child.material = new THREE.MeshBasicMaterial({
@@ -547,9 +545,7 @@ export class EntityMesh {
547
545
  obj.position.set(x, y, z)
548
546
  }
549
547
  if (metadata?.texture) {
550
- const texture = new THREE.TextureLoader().load(metadata.texture)
551
- texture.minFilter = THREE.NearestFilter
552
- texture.magFilter = THREE.NearestFilter
548
+ const texture = loadNearestFilterTexture(metadata.texture)
553
549
  const material = new THREE.MeshBasicMaterial({
554
550
  map: texture,
555
551
  transparent: true,
@@ -616,9 +612,7 @@ export class EntityMesh {
616
612
  debugFlags.isHardcodedTexture = true
617
613
  }
618
614
  if (!texturePath) throw new Error(`No texture for ${type}`)
619
- const texture = new THREE.TextureLoader().load(texturePath)
620
- texture.minFilter = THREE.NearestFilter
621
- texture.magFilter = THREE.NearestFilter
615
+ const texture = loadNearestFilterTexture(texturePath)
622
616
  const material = new THREE.MeshBasicMaterial({
623
617
  map: texture,
624
618
  transparent: true,
@@ -94,6 +94,18 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
94
94
  for (const worker of worldRenderer.workers) {
95
95
  worker.postMessage(message)
96
96
  }
97
+ },
98
+ getChunksDebugState() {
99
+ const loadedSectionsChunks: Record<string, true> = {}
100
+ for (const sectionPos of Object.keys(worldRenderer.sectionObjects)) {
101
+ const [x, , z] = sectionPos.split(',').map(Number)
102
+ loadedSectionsChunks[`${x},${z}`] = true
103
+ }
104
+ return {
105
+ loadedSectionsChunks,
106
+ loadedChunks: { ...worldRenderer.loadedChunks },
107
+ finishedChunks: { ...worldRenderer.finishedChunks },
108
+ }
97
109
  }
98
110
  }
99
111
  }
@@ -1,6 +1,19 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
 
4
+ /** Canvas used for item texture extraction (main thread or worker). */
5
+ export type ItemTextureCanvas = HTMLCanvasElement | OffscreenCanvas
6
+
7
+ function createItemTextureCanvas (width: number, height: number): ItemTextureCanvas {
8
+ if (typeof document !== 'undefined') {
9
+ const canvas = document.createElement('canvas')
10
+ canvas.width = width
11
+ canvas.height = height
12
+ return canvas
13
+ }
14
+ return new OffscreenCanvas(width, height)
15
+ }
16
+
4
17
  export interface Create3DItemMeshOptions {
5
18
  depth: number
6
19
  pixelSize?: number
@@ -17,7 +30,7 @@ export interface Create3DItemMeshResult {
17
30
  * from a canvas containing the item texture
18
31
  */
19
32
  export function create3DItemMesh (
20
- canvas: HTMLCanvasElement,
33
+ canvas: ItemTextureCanvas,
21
34
  options: Create3DItemMeshOptions
22
35
  ): Create3DItemMeshResult {
23
36
  const { depth, pixelSize } = options
@@ -246,18 +259,18 @@ export interface ItemMeshResult {
246
259
  export function extractItemTextureToCanvas (
247
260
  sourceTexture: THREE.Texture,
248
261
  textureInfo: ItemTextureInfo
249
- ): HTMLCanvasElement {
262
+ ): ItemTextureCanvas {
250
263
  const { u, v, sizeX, sizeY } = textureInfo
251
264
 
252
265
  // Calculate canvas size - fix the calculation
253
266
  const canvasWidth = Math.max(1, Math.floor(sizeX * sourceTexture.image.width))
254
267
  const canvasHeight = Math.max(1, Math.floor(sizeY * sourceTexture.image.height))
255
268
 
256
- const canvas = document.createElement('canvas')
257
- canvas.width = canvasWidth
258
- canvas.height = canvasHeight
259
-
260
- const ctx = canvas.getContext('2d')!
269
+ const canvas = createItemTextureCanvas(canvasWidth, canvasHeight)
270
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null
271
+ if (!ctx) {
272
+ throw new Error('Failed to get 2d context for item texture canvas')
273
+ }
261
274
  ctx.imageSmoothingEnabled = false
262
275
 
263
276
  // Draw the item texture region to canvas