minecraft-renderer 0.1.39 → 0.1.41

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 +94 -94
  4. package/dist/minecraft-renderer.js +57 -57
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +66 -66
  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 +5 -1
  11. package/src/graphicsBackend/preloadWorkers.ts +187 -0
  12. package/src/lib/worldrendererCommon.ts +26 -2
  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 +210 -0
  32. package/src/wasm-mesher/runtime-build/wasm_mesher.js +881 -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 +24 -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 +1247 -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.39",
3
+ "version": "0.1.41",
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,
@@ -35,6 +35,10 @@ export const defaultWorldRendererConfig = {
35
35
  * iOS Safari and other low-RAM environments). Trades performance for
36
36
  * lower per-worker RAM. */
37
37
  disableMesherConversionCache: false,
38
+ /** Whether to dedicate the last worker exclusively to block-update
39
+ * remeshing (change worker). When true, initial chunk meshing is
40
+ * distributed only across workers[0 .. n-2]. */
41
+ dedicatedChangeWorker: false,
38
42
 
39
43
  // Rendering engine settings
40
44
  /** Face shading: vanilla Minecraft vs higher-contrast client look */
@@ -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'
@@ -997,6 +997,30 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
997
997
  getWorkerNumber(pos: Vec3, updateAction = false) {
998
998
  const CHUNK_SIZE = 16
999
999
  const sectionHeight = this.getSectionHeight()
1000
+ const dedicated = this.worldRendererConfig.dedicatedChangeWorker
1001
+
1002
+ if (dedicated && this.workers.length > 1) {
1003
+ // WASM column meshing must keep all vertical sections of a chunk
1004
+ // column on one worker — skip dedicated change worker to avoid
1005
+ // concurrent column meshing across different workers.
1006
+ if (this.worldRendererConfig.wasmMesher) {
1007
+ return mod(Math.floor(pos.x / CHUNK_SIZE) + Math.floor(pos.z / CHUNK_SIZE), this.workers.length)
1008
+ }
1009
+ if (updateAction) {
1010
+ const key = `${Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE},${Math.floor(pos.y / sectionHeight) * sectionHeight},${Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE}`
1011
+ const busy = this.sectionsWaiting.get(key) && !this.finishedSections[key]
1012
+ if (busy) {
1013
+ // Section is already being meshed by a general worker — route
1014
+ // the update to the same worker to avoid concurrent meshing.
1015
+ const generalWorkers = this.workers.length - 1
1016
+ return mod(Math.floor(pos.x / CHUNK_SIZE) + Math.floor(pos.y / sectionHeight) + Math.floor(pos.z / CHUNK_SIZE), generalWorkers)
1017
+ }
1018
+ return this.workers.length - 1
1019
+ }
1020
+ const generalWorkers = this.workers.length - 1
1021
+ return mod(Math.floor(pos.x / CHUNK_SIZE) + Math.floor(pos.y / sectionHeight) + Math.floor(pos.z / CHUNK_SIZE), generalWorkers)
1022
+ }
1023
+
1000
1024
  if (this.worldRendererConfig.wasmMesher) {
1001
1025
  // WASM column meshing must keep all vertical sections of a chunk column
1002
1026
  // on one worker. Hash by x/z only and bypass the change-worker shortcut
@@ -1097,7 +1121,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1097
1121
  // Dispatch sections to workers based on position
1098
1122
  // This guarantees uniformity accross workers and that a given section
1099
1123
  // is always dispatched to the same worker
1100
- const hash = this.getWorkerNumber(pos, useChangeWorker && this.mesherLogger.active)
1124
+ const hash = this.getWorkerNumber(pos, useChangeWorker && (this.mesherLogger.active || this.worldRendererConfig.dedicatedChangeWorker))
1101
1125
  this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
1102
1126
  if (this.forceCallFromMesherReplayer) {
1103
1127
  this.workers[hash].postMessage({
@@ -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]