minecraft-renderer 0.1.40 → 0.1.42

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 (57) hide show
  1. package/dist/mesher.js +8 -8
  2. package/dist/mesher.js.map +4 -4
  3. package/dist/mesherWasm.js +93 -93
  4. package/dist/minecraft-renderer.js +57 -57
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +19 -19
  7. package/package.json +3 -4
  8. package/src/bundler/bundlePrepare.ts +56 -0
  9. package/src/graphicsBackend/appViewer.ts +10 -0
  10. package/src/graphicsBackend/config.ts +1 -1
  11. package/src/graphicsBackend/preloadWorkers.ts +187 -0
  12. package/src/lib/worldrendererCommon.ts +1 -1
  13. package/src/{mesher → mesher-legacy}/mesher.ts +14 -4
  14. package/src/{mesher → mesher-legacy}/test/mesherTester.ts +2 -2
  15. package/src/{mesher → mesher-legacy}/test/run/test-js.ts +1 -1
  16. package/src/{mesher → mesher-legacy}/test/test-perf.ts +1 -1
  17. package/src/{mesher → mesher-legacy}/test/tests.test.ts +1 -1
  18. package/src/{mesher → mesher-shared}/shared.ts +2 -0
  19. package/src/playground/allEntitiesDebug.ts +1 -1
  20. package/src/three/chunkMeshManager.ts +1 -1
  21. package/src/three/entities.ts +19 -6
  22. package/src/three/entity/EntityMesh.ts +123 -140
  23. package/src/three/graphicsBackendBase.ts +13 -0
  24. package/src/three/holdingBlock.ts +1 -1
  25. package/src/three/holdingBlockLegacy.ts +1 -1
  26. package/src/three/modules/sciFiWorldReveal.ts +1 -1
  27. package/src/three/worldRendererThree.ts +2 -2
  28. package/src/wasm-mesher/README.md +90 -0
  29. package/src/{wasm-lib → wasm-mesher/bridge}/convertChunk.ts +2 -2
  30. package/src/{wasm-lib → wasm-mesher/bridge}/render-from-wasm.ts +4 -4
  31. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +251 -0
  32. package/src/wasm-mesher/runtime-build/wasm_mesher.js +1061 -0
  33. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  34. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +26 -0
  35. package/src/{mesher/test → wasm-mesher/tests}/heightmapParity.test.ts +4 -4
  36. package/src/{mesher/test → wasm-mesher/tests}/mesherWasmConversionCache.test.ts +2 -2
  37. package/src/{mesher/test → wasm-mesher/tests}/splitColumnWasmOutput.test.ts +1 -1
  38. package/src/wasm-mesher/worker/mesherWasm.ts +1425 -0
  39. package/src/{mesher → wasm-mesher/worker}/mesherWasmConversionCache.ts +1 -1
  40. package/src/worldView/types.ts +90 -0
  41. package/src/mesher/mesherWasm.ts +0 -696
  42. package/wasm/wasm_mesher.d.ts +0 -46
  43. package/wasm/wasm_mesher.js +0 -443
  44. package/wasm/wasm_mesher_bg.wasm +0 -0
  45. package/wasm/wasm_mesher_bg.wasm.d.ts +0 -9
  46. /package/src/{mesher → mesher-legacy}/test/a.ts +0 -0
  47. /package/src/{mesher → mesher-legacy}/test/playground.ts +0 -0
  48. /package/src/{mesher → mesher-legacy}/test/run/chunk.ts +0 -0
  49. /package/src/{mesher → mesher-legacy}/test/snapshotUtils.ts +0 -0
  50. /package/src/{mesher → mesher-shared}/blockEntityMetadata.ts +0 -0
  51. /package/src/{mesher → mesher-shared}/computeHeightmap.ts +0 -0
  52. /package/src/{mesher → mesher-shared}/models.ts +0 -0
  53. /package/src/{mesher → mesher-shared}/modelsGeometryCommon.ts +0 -0
  54. /package/src/{mesher → mesher-shared}/standaloneRenderer.ts +0 -0
  55. /package/src/{mesher → mesher-shared}/world.ts +0 -0
  56. /package/src/{mesher → mesher-shared}/worldConstants.ts +0 -0
  57. /package/src/{mesher → wasm-mesher/worker}/mesherWasmRequestTracker.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,6 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "src",
12
- "wasm",
13
12
  "logo.webp"
14
13
  ],
15
14
  "browserslist": {
@@ -121,8 +120,8 @@
121
120
  "watch:lib": "node scripts/buildLib.mjs -w",
122
121
  "build:mesher": "node scripts/buildMesherWorker.mjs",
123
122
  "build:wasm": "cd wasm-mesher && ./build.sh web",
124
- "test:wasm": "cd wasm-mesher && wasm-pack build --target nodejs --out-dir pkg --dev && node build.mjs && node test-chunk.cjs && node test-section-boundary.cjs",
125
- "test:wasm:boundary": "cd wasm-mesher && wasm-pack build --target nodejs --out-dir pkg --dev && node build.mjs && node test-section-boundary.cjs",
123
+ "test:wasm": "cd wasm-mesher && wasm-pack build --target nodejs --out-dir pkg --dev && node build.mjs && node tests/test-chunk.cjs && node tests/test-section-boundary.cjs",
124
+ "test:wasm:boundary": "cd wasm-mesher && wasm-pack build --target nodejs --out-dir pkg --dev && node build.mjs && node tests/test-section-boundary.cjs",
126
125
  "watch:mesher": "pnpm build:mesher -w",
127
126
  "build:threeworker": "node scripts/buildThreeWorker.mjs",
128
127
  "watch:threeworker": "pnpm build:threeworker -w",
@@ -0,0 +1,56 @@
1
+ //@ts-nocheck
2
+ import { cp, mkdir, stat } from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+
6
+ /** Worker-related basenames inside `minecraft-renderer`/`dist`; skipped if missing. */
7
+ export const MESHER_DIST_FILES = [
8
+ 'mesher.js',
9
+ 'mesher.js.map',
10
+ 'mesherWasm.js',
11
+ 'mesherWasm.js.map',
12
+ ] as const
13
+
14
+ export type BundlePrepareMesherOptions = {
15
+ cwd?: string
16
+ packageName?: string
17
+ outDir?: string
18
+ mesherDistDir?: string
19
+ files?: readonly string[]
20
+ }
21
+
22
+ function resolveSrcDist (opts: BundlePrepareMesherOptions | undefined, cwd: string): string {
23
+ if (opts?.mesherDistDir) return path.resolve(cwd, opts.mesherDistDir)
24
+ const pkg = opts?.packageName ?? 'minecraft-renderer'
25
+ const req = createRequire(path.join(cwd, 'package.json'))
26
+ return path.join(path.dirname(req.resolve(`${pkg}/package.json`)), 'dist')
27
+ }
28
+
29
+ export async function bundlePrepareMesherWorkers (opts?: BundlePrepareMesherOptions): Promise<string[]> {
30
+ const cwd = opts?.cwd ?? process.cwd()
31
+ const outDir = path.resolve(cwd, opts?.outDir ?? 'dist')
32
+ const srcDist = resolveSrcDist(opts, cwd)
33
+ const names = opts?.files ?? MESHER_DIST_FILES
34
+
35
+ await stat(srcDist).catch(() => {
36
+ throw new Error(`[bundlePrepareMesherWorkers] missing dist: ${srcDist}`)
37
+ })
38
+ await mkdir(outDir, { recursive: true })
39
+
40
+ const copied: string[] = []
41
+ for (const name of names) {
42
+ const from = path.join(srcDist, name)
43
+ let st
44
+ try {
45
+ st = await stat(from)
46
+ } catch {
47
+ continue
48
+ }
49
+ if (!st.isFile()) continue
50
+ const to = path.join(outDir, name)
51
+ await mkdir(path.dirname(to), { recursive: true })
52
+ await cp(from, to)
53
+ copied.push(path.relative(process.cwd(), to) || to)
54
+ }
55
+ return copied
56
+ }
@@ -26,6 +26,7 @@ import { getInitialPlayerState } from './playerState'
26
26
  import { defaultWorldRendererConfig, defaultGraphicsBackendConfig, getDefaultRendererState, WorldRendererConfig } from './config'
27
27
  import { PlayerStateReactive } from '../playerState/playerState'
28
28
  import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
29
+ import { preloadMesherWorkerScript } from './preloadWorkers'
29
30
 
30
31
  export interface AppViewerOptions {
31
32
  config?: Partial<GraphicsBackendConfig>
@@ -101,6 +102,15 @@ export class AppViewer {
101
102
  this.resolveWorldReady = resolve
102
103
  }
103
104
 
105
+ /**
106
+ * Preload mesher worker script (HTTP validate + ephemeral Worker + `mc-web-ping` / `mc-web-pong`).
107
+ * Chooses `/mesherWasm.js` vs `/mesher.js` from `inWorldRenderingConfig.wasmMesher`.
108
+ */
109
+ preloadWorkers (): Promise<void> {
110
+ const script = this.inWorldRenderingConfig.wasmMesher ? 'mesherWasm.js' : 'mesher.js'
111
+ return preloadMesherWorkerScript({ script })
112
+ }
113
+
104
114
  /**
105
115
  * Load a graphics backend.
106
116
  */
@@ -25,7 +25,7 @@ export const defaultWorldRendererConfig = {
25
25
  futuristicReveal: false,
26
26
 
27
27
  // Performance settings
28
- wasmMesher: false,
28
+ wasmMesher: true,
29
29
  mesherWorkers: 1,
30
30
  addChunksBatchWaitTime: 200,
31
31
  _experimentalSmoothChunkLoading: true,
@@ -0,0 +1,187 @@
1
+ //@ts-nocheck
2
+ /** Structured reason so logs / support can tell fetch vs worker vs ping failures apart. */
3
+ export type MesherWorkerPreloadFailure =
4
+ | { phase: 'fetch'; code: 'timeout' | 'network' | 'bad-status'; status?: number; detail?: string }
5
+ | { phase: 'fetch'; code: 'invalid-body'; hint: 'empty' | 'html' }
6
+ | { phase: 'worker'; code: 'construct-failed'; message: string }
7
+ | { phase: 'worker'; code: 'script-error'; message: string }
8
+ | { phase: 'ping'; code: 'timeout' | 'messageerror' | 'post-failed'; detail?: string }
9
+
10
+ export class MesherWorkerPreloadError extends Error {
11
+ readonly failure: MesherWorkerPreloadFailure
12
+
13
+ constructor(message: string, failure: MesherWorkerPreloadFailure) {
14
+ super(message)
15
+ this.name = 'MesherWorkerPreloadError'
16
+ this.failure = failure
17
+ console.error('[mesher preload]', failure, message)
18
+ }
19
+ }
20
+
21
+ function isMcWebPong(data: unknown): boolean {
22
+ if (!data || typeof data !== 'object') return false
23
+ if ((data as { type?: string }).type === 'mc-web-pong') return true
24
+ if (Array.isArray(data)) {
25
+ return data.some(d => d && typeof d === 'object' && (d as { type?: string }).type === 'mc-web-pong')
26
+ }
27
+ return false
28
+ }
29
+
30
+ const DEFAULT_FETCH_MS = 45_000
31
+ const DEFAULT_PING_MS = 10_000
32
+
33
+ /**
34
+ * Validates a mesher worker script over HTTP (not HTML/error page), instantiates a Worker, and waits for `mc-web-pong`.
35
+ * Use `mesher.js` for the legacy mesher bundle and `mesherWasm.js` for the WASM mesher bundle.
36
+ * Single-file builds skip (blob worker).
37
+ */
38
+ export async function preloadMesherWorkerScript(opts?: {
39
+ fetchTimeoutMs?: number
40
+ pingTimeoutMs?: number
41
+ /** Worker script basename relative to `document.baseURI`. Defaults to `mesher.js`. */
42
+ script?: string
43
+ }): Promise<void> {
44
+ if (process.env.SINGLE_FILE_BUILD) return
45
+
46
+ const fetchTimeoutMs = opts?.fetchTimeoutMs ?? DEFAULT_FETCH_MS
47
+ const pingTimeoutMs = opts?.pingTimeoutMs ?? DEFAULT_PING_MS
48
+ const scriptBasename = opts?.script ?? 'mesher.js'
49
+ const scriptUrl = new URL(scriptBasename, document.baseURI).href
50
+
51
+ let res: Response
52
+ try {
53
+ const ctrl = new AbortController()
54
+ const t = window.setTimeout(() => ctrl.abort(), fetchTimeoutMs)
55
+ try {
56
+ res = await fetch(scriptUrl, {
57
+ credentials: 'same-origin',
58
+ cache: 'force-cache',
59
+ signal: ctrl.signal,
60
+ })
61
+ } finally {
62
+ clearTimeout(t)
63
+ }
64
+ } catch (e: unknown) {
65
+ const err = e as { name?: string; message?: string }
66
+ if (err?.name === 'AbortError') {
67
+ throw new MesherWorkerPreloadError(
68
+ `Mesher script fetch timed out after ${fetchTimeoutMs}ms (${scriptUrl}).`,
69
+ { phase: 'fetch', code: 'timeout' }
70
+ )
71
+ }
72
+ throw new MesherWorkerPreloadError(
73
+ `Mesher script fetch failed (network): ${err?.message ?? e}. URL: ${scriptUrl}`,
74
+ { phase: 'fetch', code: 'network', detail: String(err?.message ?? e) }
75
+ )
76
+ }
77
+
78
+ if (!res.ok) {
79
+ throw new MesherWorkerPreloadError(
80
+ `Mesher script HTTP ${res.status} ${res.statusText}: ${scriptUrl}`,
81
+ { phase: 'fetch', code: 'bad-status', status: res.status }
82
+ )
83
+ }
84
+
85
+ const contentType = res.headers.get('content-type') ?? ''
86
+ const buf = await res.arrayBuffer()
87
+ if (buf.byteLength === 0) {
88
+ throw new MesherWorkerPreloadError(
89
+ `Mesher script response was empty: ${scriptUrl}`,
90
+ { phase: 'fetch', code: 'invalid-body', hint: 'empty' }
91
+ )
92
+ }
93
+
94
+ const headSize = Math.min(1024, buf.byteLength)
95
+ const head = new TextDecoder().decode(buf.slice(0, headSize)).trimStart()
96
+ if (head.startsWith('<!DOCTYPE') || head.startsWith('<html') || head.startsWith('<HTML')) {
97
+ throw new MesherWorkerPreloadError(
98
+ `Mesher URL returned HTML (wrong path, redirect, or SPA fallback), not JavaScript: ${scriptUrl}`,
99
+ { phase: 'fetch', code: 'invalid-body', hint: 'html' }
100
+ )
101
+ }
102
+
103
+ if (contentType.length > 0 && !/javascript|ecmascript/i.test(contentType)) {
104
+ console.warn('[mesher preload] Unexpected Content-Type for mesher worker script:', contentType, scriptUrl)
105
+ }
106
+
107
+ let worker: Worker | undefined
108
+ try {
109
+ worker = new Worker(scriptUrl)
110
+ } catch (e: unknown) {
111
+ const msg = e instanceof Error ? e.message : String(e)
112
+ throw new MesherWorkerPreloadError(
113
+ `Could not construct Worker for mesher (${scriptUrl}): ${msg}`,
114
+ { phase: 'worker', code: 'construct-failed', message: msg }
115
+ )
116
+ }
117
+
118
+ await new Promise<void>((resolve, reject) => {
119
+ let settled = false
120
+ const pingTimer = window.setTimeout(() => {
121
+ if (settled) return
122
+ settled = true
123
+ cleanup()
124
+ reject(new MesherWorkerPreloadError(
125
+ `Mesher worker did not reply with mc-web-pong within ${pingTimeoutMs}ms (wrong script, SW stale cache, worker blocked, or COEP/CORP). URL: ${scriptUrl}`,
126
+ { phase: 'ping', code: 'timeout' }
127
+ ))
128
+ }, pingTimeoutMs)
129
+
130
+ const cleanup = () => {
131
+ clearTimeout(pingTimer)
132
+ const w = worker
133
+ worker = undefined
134
+ if (!w) return
135
+ w.removeEventListener('message', onMessage)
136
+ w.removeEventListener('error', onError)
137
+ w.removeEventListener('messageerror', onMessageError)
138
+ w.terminate()
139
+ }
140
+
141
+ const done = () => {
142
+ if (settled) return
143
+ settled = true
144
+ cleanup()
145
+ resolve()
146
+ }
147
+
148
+ const fail = (err: Error) => {
149
+ if (settled) return
150
+ settled = true
151
+ cleanup()
152
+ reject(err)
153
+ }
154
+
155
+ function onMessage(ev: MessageEvent) {
156
+ if (isMcWebPong(ev.data)) done()
157
+ }
158
+
159
+ function onError(ev: ErrorEvent) {
160
+ fail(new MesherWorkerPreloadError(
161
+ `Mesher worker script failed to load or threw during startup: ${ev.message || 'unknown'} @ ${scriptUrl}`,
162
+ { phase: 'worker', code: 'script-error', message: ev.message }
163
+ ))
164
+ }
165
+
166
+ function onMessageError() {
167
+ fail(new MesherWorkerPreloadError(
168
+ `Mesher worker message channel error (structured clone / deserialization). URL: ${scriptUrl}`,
169
+ { phase: 'ping', code: 'messageerror' }
170
+ ))
171
+ }
172
+
173
+ worker!.addEventListener('message', onMessage)
174
+ worker!.addEventListener('error', onError)
175
+ worker!.addEventListener('messageerror', onMessageError)
176
+
177
+ try {
178
+ worker!.postMessage({ type: 'mc-web-ping', t: performance.now(), workerIndex: 0 })
179
+ } catch (e: unknown) {
180
+ const detail = e instanceof Error ? e.message : String(e)
181
+ fail(new MesherWorkerPreloadError(
182
+ `Failed to post mc-web-ping to mesher worker: ${detail}`,
183
+ { phase: 'ping', code: 'post-failed', detail }
184
+ ))
185
+ }
186
+ })
187
+ }
@@ -9,7 +9,7 @@ import { proxy, subscribe } from 'valtio'
9
9
  import type { ResourcesManagerTransferred } from '../resourcesManager/resourcesManager'
10
10
  import { dynamicMcDataFiles } from './buildSharedConfig.mjs'
11
11
  import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem } from '../graphicsBackend/types'
12
- import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, SECTION_HEIGHT } from '../mesher/shared'
12
+ import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, SECTION_HEIGHT } from '../mesher-shared/shared'
13
13
  import { chunkPos } from './simpleUtils'
14
14
  import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
15
15
  import { getPlayerStateUtils } from '../graphicsBackend/playerState'
@@ -1,9 +1,9 @@
1
1
  //@ts-nocheck
2
2
  import { Vec3 } from 'vec3'
3
- import { World } from './world'
4
- import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
5
- import { BlockStateModelInfo } from './shared'
6
- import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from './computeHeightmap'
3
+ import { World } from '../mesher-shared/world'
4
+ import { getSectionGeometry, setBlockStatesData as setMesherData } from '../mesher-shared/models'
5
+ import { BlockStateModelInfo } from '../mesher-shared/shared'
6
+ import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from '../mesher-shared/computeHeightmap'
7
7
 
8
8
  globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
9
9
 
@@ -169,6 +169,16 @@ const handleMessage = data => {
169
169
 
170
170
  break
171
171
  }
172
+ case 'mc-web-ping': {
173
+ const replyWorkerIndex = typeof data.workerIndex === 'number' ? data.workerIndex : workerIndex
174
+ global.postMessage({
175
+ type: 'mc-web-pong',
176
+ workerIndex: replyWorkerIndex,
177
+ t: data.t,
178
+ recvAt: typeof performance !== 'undefined' ? performance.now() : undefined,
179
+ })
180
+ break
181
+ }
172
182
  // No default
173
183
  }
174
184
  }
@@ -3,8 +3,8 @@ import ChunkLoader, { PCChunk } from 'prismarine-chunk'
3
3
  import { Vec3 } from 'vec3'
4
4
  import MinecraftData from 'minecraft-data'
5
5
  import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
6
- import { World as MesherWorld } from '../world'
7
- import { setBlockStatesData, getSectionGeometry } from '../models'
6
+ import { World as MesherWorld } from '../../mesher-shared/world'
7
+ import { setBlockStatesData, getSectionGeometry } from '../../mesher-shared/models'
8
8
 
9
9
  interface Options {
10
10
  chunkOverride?: PCChunk
@@ -3,7 +3,7 @@ import ChunkLoader from 'prismarine-chunk'
3
3
  import { setup } from '../mesherTester'
4
4
  import { compareOrWriteSnapshot } from '../snapshotUtils'
5
5
  import { getChunk, VERSION } from './chunk'
6
- import { mesherGeometryToExportFormat } from '../../../wasm-lib/render-from-wasm'
6
+ import { mesherGeometryToExportFormat } from '../../../wasm-mesher/bridge/render-from-wasm'
7
7
  import fs from 'fs'
8
8
  import { join } from 'path'
9
9
 
@@ -3,7 +3,7 @@ import PrismarineWorld from 'prismarine-world'
3
3
  import PrismarineChunk from 'prismarine-chunk'
4
4
  import { Vec3 } from 'vec3'
5
5
  import MinecraftData from 'minecraft-data'
6
- import { defaultMesherConfig } from '../shared'
6
+ import { defaultMesherConfig } from '../../mesher-shared/shared'
7
7
  import { setup } from './mesherTester.js'
8
8
  import { generateSpiralMatrix } from '../../lib/spiral'
9
9
 
@@ -1,7 +1,7 @@
1
1
  //@ts-nocheck
2
2
  import { test, expect } from 'vitest'
3
3
  import { versions } from 'minecraft-data'
4
- import { INVISIBLE_BLOCKS } from '../worldConstants'
4
+ import { INVISIBLE_BLOCKS } from '../../mesher-shared/worldConstants'
5
5
  import { setup } from './mesherTester'
6
6
 
7
7
  const lastVersion = versions.pc.map(x => x.minecraftVersion).filter(version => !version.includes('w'))[0]
@@ -87,6 +87,8 @@ export interface MesherMainEvents {
87
87
  };
88
88
  blockStateModelInfo: { type: 'blockStateModelInfo'; info: Record<string, BlockStateModelInfo> };
89
89
  heightmap: { type: 'heightmap'; key: string; heightmap: Int16Array };
90
+ /** Reply to `{ type: 'mc-web-ping', t?, workerIndex? }` from the main thread (not batched in worker). */
91
+ mcWebPong: { type: 'mc-web-pong'; workerIndex: number; t?: number; recvAt?: number };
90
92
  }
91
93
 
92
94
  export type MesherMainEvent = MesherMainEvents[keyof MesherMainEvents]
@@ -34,7 +34,7 @@ export const displayEntitiesDebugList = (mcData: IndexedData) => {
34
34
  const results: Array<{
35
35
  entity: string;
36
36
  supported: boolean;
37
- type?: 'obj' | 'bedrock' | 'special';
37
+ type?: 'obj' | 'bedrock' | 'gltf' | 'special';
38
38
  mappedFrom?: string;
39
39
  textureMap?: boolean;
40
40
  errors?: string[];
@@ -3,7 +3,7 @@ import PrismarineChatLoader from 'prismarine-chat'
3
3
  import * as THREE from 'three'
4
4
  import * as nbt from 'prismarine-nbt'
5
5
  import { Vec3 } from 'vec3'
6
- import { MesherGeometryOutput } from '../mesher/shared'
6
+ import { MesherGeometryOutput } from '../mesher-shared/shared'
7
7
  import { chunkPos } from '../lib/simpleUtils'
8
8
  import { renderSign } from '../sign-renderer'
9
9
  import { getMesh } from './entity/EntityMesh'
@@ -28,6 +28,12 @@ import { WorldRendererThree } from './worldRendererThree'
28
28
  import { IndexedData } from 'minecraft-data'
29
29
  import { ItemSpecificContextProperties } from '../playerState/types'
30
30
 
31
+ export type EntityModelOverridePart = {
32
+ modelPath: string | ArrayBuffer
33
+ modelType: Entity.EntityModelType
34
+ metadata?: any
35
+ }
36
+
31
37
  // Type for entity metadata - simplified version
32
38
  type EntityMetadataVersions = {
33
39
  [key: string]: any
@@ -250,7 +256,7 @@ export class Entities {
250
256
  currentlyRendering = true
251
257
  cachedMapsImages = {} as Record<number, string>
252
258
  itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
253
- pendingModelOverrides = new Map<string, { modelPath: string, modelType: Entity.EntityModelType, metadata: any }>()
259
+ pendingModelOverrides = new Map<string, { parts: EntityModelOverridePart[] }>()
254
260
 
255
261
  private motionCache = new Map<string, { pos: THREE.Vector3, speed: number }>()
256
262
  private _wasThirdPerson = false
@@ -1380,8 +1386,8 @@ export class Entities {
1380
1386
  beforeEntityAdded(entity: import('prismarine-entity').Entity) {
1381
1387
  const override = this.pendingModelOverrides.get(entity.id.toString())
1382
1388
  if (override) {
1383
- const { modelPath, modelType, metadata } = override
1384
- entity['customModel'] = { modelPath, modelType, metadata }
1389
+ const { parts } = override
1390
+ entity['customModel'] = parts.length === 1 ? parts[0]! : { parts }
1385
1391
  this.pendingModelOverrides.delete(entity.id.toString())
1386
1392
  }
1387
1393
  }
@@ -1580,9 +1586,16 @@ export class Entities {
1580
1586
  return intersects[0]?.object
1581
1587
  }
1582
1588
 
1583
- updateEntityModel(entityId: string, modelPath: string, modelType: Entity.EntityModelType, metadata?: any) {
1584
- // Store override data for future entities
1585
- this.pendingModelOverrides.set(entityId, { modelPath, modelType, metadata })
1589
+ updateEntityModel(
1590
+ entityId: string,
1591
+ modelPathOrParts: string | EntityModelOverridePart[],
1592
+ modelType?: Entity.EntityModelType,
1593
+ metadata?: any
1594
+ ) {
1595
+ const parts: EntityModelOverridePart[] = Array.isArray(modelPathOrParts)
1596
+ ? modelPathOrParts
1597
+ : [{ modelPath: modelPathOrParts, modelType: modelType!, metadata }]
1598
+ this.pendingModelOverrides.set(entityId, { parts })
1586
1599
 
1587
1600
  // Force entity recreation if it exists
1588
1601
  const entity = this.entities[entityId]