minecraft-renderer 0.1.0

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 (187) hide show
  1. package/README.md +297 -0
  2. package/dist/index.html +83 -0
  3. package/dist/static/image/arrow.6f27b59f.png +0 -0
  4. package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
  5. package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
  6. package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
  7. package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
  8. package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
  9. package/dist/static/js/365.f05233ab.js +8462 -0
  10. package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
  11. package/dist/static/js/async/738.efa27644.js +1 -0
  12. package/dist/static/js/index.092ec5be.js +56 -0
  13. package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
  14. package/dist/static/js/lib-react.5c9129e0.js +2 -0
  15. package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
  16. package/package.json +104 -0
  17. package/src/assets/destroy_stage_0.png +0 -0
  18. package/src/assets/destroy_stage_1.png +0 -0
  19. package/src/assets/destroy_stage_2.png +0 -0
  20. package/src/assets/destroy_stage_3.png +0 -0
  21. package/src/assets/destroy_stage_4.png +0 -0
  22. package/src/assets/destroy_stage_5.png +0 -0
  23. package/src/assets/destroy_stage_6.png +0 -0
  24. package/src/assets/destroy_stage_7.png +0 -0
  25. package/src/assets/destroy_stage_8.png +0 -0
  26. package/src/assets/destroy_stage_9.png +0 -0
  27. package/src/examples/README.md +146 -0
  28. package/src/examples/appViewerExample.ts +205 -0
  29. package/src/examples/initialMenuStart.ts +161 -0
  30. package/src/graphicsBackend/appViewer.ts +297 -0
  31. package/src/graphicsBackend/config.ts +119 -0
  32. package/src/graphicsBackend/index.ts +10 -0
  33. package/src/graphicsBackend/playerState.ts +61 -0
  34. package/src/graphicsBackend/types.ts +143 -0
  35. package/src/index.ts +97 -0
  36. package/src/lib/DebugGui.ts +190 -0
  37. package/src/lib/animationController.ts +85 -0
  38. package/src/lib/buildSharedConfig.mjs +1 -0
  39. package/src/lib/cameraBobbing.ts +94 -0
  40. package/src/lib/canvas2DOverlay.example.ts +361 -0
  41. package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
  42. package/src/lib/canvas2DOverlay.ts +381 -0
  43. package/src/lib/cleanupDecorator.ts +29 -0
  44. package/src/lib/createPlayerObject.ts +55 -0
  45. package/src/lib/frameTimingCollector.ts +164 -0
  46. package/src/lib/guiRenderer.ts +283 -0
  47. package/src/lib/items.ts +140 -0
  48. package/src/lib/mesherlogReader.ts +131 -0
  49. package/src/lib/moreBlockDataGenerated.json +714 -0
  50. package/src/lib/preflatMap.json +1741 -0
  51. package/src/lib/simpleUtils.ts +40 -0
  52. package/src/lib/smoothSwitcher.ts +168 -0
  53. package/src/lib/spiral.ts +29 -0
  54. package/src/lib/ui/newStats.ts +120 -0
  55. package/src/lib/utils/proxy.ts +23 -0
  56. package/src/lib/utils/skins.ts +63 -0
  57. package/src/lib/utils.ts +76 -0
  58. package/src/lib/workerProxy.ts +342 -0
  59. package/src/lib/worldrendererCommon.ts +1088 -0
  60. package/src/mesher/mesher.ts +253 -0
  61. package/src/mesher/models.ts +769 -0
  62. package/src/mesher/modelsGeometryCommon.ts +142 -0
  63. package/src/mesher/shared.ts +80 -0
  64. package/src/mesher/standaloneRenderer.ts +270 -0
  65. package/src/mesher/test/a.ts +3 -0
  66. package/src/mesher/test/mesherTester.ts +76 -0
  67. package/src/mesher/test/playground.ts +19 -0
  68. package/src/mesher/test/test-perf.ts +74 -0
  69. package/src/mesher/test/tests.test.ts +56 -0
  70. package/src/mesher/world.ts +294 -0
  71. package/src/mesher/worldConstants.ts +1 -0
  72. package/src/modules/index.ts +11 -0
  73. package/src/modules/starfield.ts +313 -0
  74. package/src/modules/types.ts +110 -0
  75. package/src/playerState/playerState.ts +78 -0
  76. package/src/playerState/types.ts +36 -0
  77. package/src/playground/allEntitiesDebug.ts +170 -0
  78. package/src/playground/baseScene.ts +587 -0
  79. package/src/playground/mobileControls.tsx +268 -0
  80. package/src/playground/playground.html +83 -0
  81. package/src/playground/playground.ts +11 -0
  82. package/src/playground/playgroundUi.tsx +140 -0
  83. package/src/playground/reactUtils.ts +71 -0
  84. package/src/playground/scenes/allEntities.ts +13 -0
  85. package/src/playground/scenes/entities.ts +37 -0
  86. package/src/playground/scenes/floorRandom.ts +33 -0
  87. package/src/playground/scenes/frequentUpdates.ts +148 -0
  88. package/src/playground/scenes/geometryExport.ts +142 -0
  89. package/src/playground/scenes/index.ts +12 -0
  90. package/src/playground/scenes/lightingStarfield.ts +40 -0
  91. package/src/playground/scenes/main.ts +313 -0
  92. package/src/playground/scenes/railsCobweb.ts +14 -0
  93. package/src/playground/scenes/rotationIssue.ts +7 -0
  94. package/src/playground/scenes/slabsOptimization.ts +16 -0
  95. package/src/playground/scenes/transparencyIssue.ts +11 -0
  96. package/src/playground/shared.ts +79 -0
  97. package/src/resourcesManager/index.ts +5 -0
  98. package/src/resourcesManager/resourcesManager.ts +314 -0
  99. package/src/shims/minecraftData.ts +41 -0
  100. package/src/sign-renderer/index.html +21 -0
  101. package/src/sign-renderer/index.ts +216 -0
  102. package/src/sign-renderer/noop.js +1 -0
  103. package/src/sign-renderer/playground.ts +38 -0
  104. package/src/sign-renderer/tests.test.ts +69 -0
  105. package/src/sign-renderer/vite.config.ts +10 -0
  106. package/src/three/appShared.ts +75 -0
  107. package/src/three/bannerRenderer.ts +275 -0
  108. package/src/three/cameraShake.ts +120 -0
  109. package/src/three/cinimaticScript.ts +350 -0
  110. package/src/three/documentRenderer.ts +491 -0
  111. package/src/three/entities.ts +1580 -0
  112. package/src/three/entity/EntityMesh.ts +707 -0
  113. package/src/three/entity/animations.js +171 -0
  114. package/src/three/entity/armorModels.json +204 -0
  115. package/src/three/entity/armorModels.ts +36 -0
  116. package/src/three/entity/entities.json +6230 -0
  117. package/src/three/entity/exportedModels.js +38 -0
  118. package/src/three/entity/externalTextures.json +1 -0
  119. package/src/three/entity/models/allay.obj +325 -0
  120. package/src/three/entity/models/arrow.obj +60 -0
  121. package/src/three/entity/models/axolotl.obj +509 -0
  122. package/src/three/entity/models/blaze.obj +601 -0
  123. package/src/three/entity/models/boat.obj +417 -0
  124. package/src/three/entity/models/camel.obj +1061 -0
  125. package/src/three/entity/models/cat.obj +509 -0
  126. package/src/three/entity/models/chicken.obj +371 -0
  127. package/src/three/entity/models/cod.obj +371 -0
  128. package/src/three/entity/models/creeper.obj +279 -0
  129. package/src/three/entity/models/dolphin.obj +371 -0
  130. package/src/three/entity/models/ender_dragon.obj +2993 -0
  131. package/src/three/entity/models/enderman.obj +325 -0
  132. package/src/three/entity/models/endermite.obj +187 -0
  133. package/src/three/entity/models/fox.obj +463 -0
  134. package/src/three/entity/models/frog.obj +739 -0
  135. package/src/three/entity/models/ghast.obj +463 -0
  136. package/src/three/entity/models/goat.obj +601 -0
  137. package/src/three/entity/models/guardian.obj +1015 -0
  138. package/src/three/entity/models/horse.obj +1061 -0
  139. package/src/three/entity/models/llama.obj +509 -0
  140. package/src/three/entity/models/minecart.obj +233 -0
  141. package/src/three/entity/models/parrot.obj +509 -0
  142. package/src/three/entity/models/piglin.obj +739 -0
  143. package/src/three/entity/models/pillager.obj +371 -0
  144. package/src/three/entity/models/rabbit.obj +555 -0
  145. package/src/three/entity/models/sheep.obj +555 -0
  146. package/src/three/entity/models/shulker.obj +141 -0
  147. package/src/three/entity/models/sniffer.obj +693 -0
  148. package/src/three/entity/models/spider.obj +509 -0
  149. package/src/three/entity/models/tadpole.obj +95 -0
  150. package/src/three/entity/models/turtle.obj +371 -0
  151. package/src/three/entity/models/vex.obj +325 -0
  152. package/src/three/entity/models/villager.obj +509 -0
  153. package/src/three/entity/models/warden.obj +463 -0
  154. package/src/three/entity/models/witch.obj +647 -0
  155. package/src/three/entity/models/wolf.obj +509 -0
  156. package/src/three/entity/models/zombie_villager.obj +463 -0
  157. package/src/three/entity/objModels.js +1 -0
  158. package/src/three/fireworks.ts +661 -0
  159. package/src/three/fireworksRenderer.ts +434 -0
  160. package/src/three/globals.d.ts +7 -0
  161. package/src/three/graphicsBackend.ts +274 -0
  162. package/src/three/graphicsBackendOffThread.ts +107 -0
  163. package/src/three/hand.ts +89 -0
  164. package/src/three/holdingBlock.ts +926 -0
  165. package/src/three/index.ts +20 -0
  166. package/src/three/itemMesh.ts +427 -0
  167. package/src/three/modules.d.ts +14 -0
  168. package/src/three/panorama.ts +308 -0
  169. package/src/three/panoramaShared.ts +1 -0
  170. package/src/three/renderSlot.ts +82 -0
  171. package/src/three/skyboxRenderer.ts +406 -0
  172. package/src/three/starField.ts +13 -0
  173. package/src/three/threeJsMedia.ts +731 -0
  174. package/src/three/threeJsMethods.ts +15 -0
  175. package/src/three/threeJsParticles.ts +160 -0
  176. package/src/three/threeJsSound.ts +95 -0
  177. package/src/three/threeJsUtils.ts +90 -0
  178. package/src/three/waypointSprite.ts +435 -0
  179. package/src/three/waypoints.ts +163 -0
  180. package/src/three/world/cursorBlock.ts +172 -0
  181. package/src/three/world/vr.ts +257 -0
  182. package/src/three/worldGeometryExport.ts +259 -0
  183. package/src/three/worldGeometryHandler.ts +279 -0
  184. package/src/three/worldRendererThree.ts +1381 -0
  185. package/src/worldView/index.ts +6 -0
  186. package/src/worldView/types.ts +66 -0
  187. package/src/worldView/worldView.ts +424 -0
@@ -0,0 +1,1088 @@
1
+ /* eslint-disable guard-for-in */
2
+ import { EventEmitter } from 'events'
3
+ import { Vec3 } from 'vec3'
4
+ import mcDataRaw from 'minecraft-data/data.js' // note: using alias
5
+ import TypedEmitter from 'typed-emitter'
6
+ import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
7
+ import { subscribeKey } from 'valtio/utils'
8
+ import { proxy } from 'valtio'
9
+ import type { ResourcesManagerTransferred } from '../resourcesManager/resourcesManager'
10
+ import { dynamicMcDataFiles } from './buildSharedConfig.mjs'
11
+ import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem } from '../graphicsBackend/types'
12
+ import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from '../mesher/shared'
13
+ import { chunkPos } from './simpleUtils'
14
+ import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
15
+ import { getPlayerStateUtils } from '../graphicsBackend/playerState'
16
+ // TODO: Fix PlayerStateRenderer and PlayerStateUtils imports
17
+ type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils>
18
+ import { MesherLogReader } from './mesherlogReader'
19
+ import { setSkinsConfig } from './utils/skins'
20
+ import { generateSpiralMatrix, WorldViewWorker } from '../worldView'
21
+ import { PlayerStateReactive } from '@/playerState/playerState'
22
+
23
+ function mod(x, n) {
24
+ return ((x % n) + n) % n
25
+ }
26
+
27
+ const toMajorVersion = version => {
28
+ const [a, b] = (String(version)).split('.')
29
+ return `${a}.${b}`
30
+ }
31
+
32
+ export const defaultWorldRendererConfig = {
33
+ paused: false,
34
+ // Debug settings
35
+ showChunkBorders: false,
36
+ enableDebugOverlay: false,
37
+ debugModelVariant: undefined as undefined | number[],
38
+
39
+ // Performance settings
40
+ mesherWorkers: 4,
41
+ addChunksBatchWaitTime: 200,
42
+ _experimentalSmoothChunkLoading: true,
43
+ _renderByChunks: false,
44
+
45
+ // Rendering engine settings
46
+ dayCycle: true,
47
+ smoothLighting: true,
48
+ enableLighting: true,
49
+ starfield: true,
50
+ defaultSkybox: true,
51
+ renderEntities: true,
52
+ extraBlockRenderers: true,
53
+ foreground: true,
54
+ fov: 75,
55
+ volume: 1,
56
+
57
+ // Camera visual related settings
58
+ showHand: false,
59
+ viewBobbing: false,
60
+ renderEars: true,
61
+ highlightBlockColor: 'blue',
62
+
63
+ // Player models
64
+ fetchPlayerSkins: true,
65
+ skinTexturesProxy: undefined as string | undefined,
66
+
67
+ // VR settings
68
+ vrSupport: true,
69
+ vrPageGameRendering: true,
70
+
71
+ // World settings
72
+ clipWorldBelowY: undefined as number | undefined,
73
+ isPlayground: false,
74
+ instantCameraUpdate: false
75
+ }
76
+
77
+ export type WorldRendererConfig = typeof defaultWorldRendererConfig
78
+
79
+ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
80
+ worldReadyResolvers = Promise.withResolvers<void>()
81
+ worldReadyPromise = this.worldReadyResolvers.promise
82
+ timeOfTheDay = 0
83
+ worldSizeParams = { minY: 0, worldHeight: 256 }
84
+ reactiveDebugParams = proxy({
85
+ stopRendering: false,
86
+ chunksRenderAboveOverride: undefined as number | undefined,
87
+ chunksRenderAboveEnabled: false,
88
+ chunksRenderBelowOverride: undefined as number | undefined,
89
+ chunksRenderBelowEnabled: false,
90
+ chunksRenderDistanceOverride: undefined as number | undefined,
91
+ chunksRenderDistanceEnabled: false,
92
+ disableEntities: false,
93
+ // disableParticles: false
94
+ })
95
+
96
+ active = false
97
+
98
+ // #region CHUNK & SECTIONS TRACKING
99
+ loadedChunks = {} as Record<string, boolean> // data is added for these chunks and they might be still processing
100
+
101
+ finishedChunks = {} as Record<string, boolean> // these chunks are fully loaded into the world (scene)
102
+
103
+ finishedSections = {} as Record<string, boolean> // these sections are fully loaded into the world (scene)
104
+
105
+ // loading sections (chunks)
106
+ sectionsWaiting = new Map<string, number>()
107
+
108
+ queuedChunks = new Set<string>()
109
+ queuedFunctions = [] as Array<() => void>
110
+ // #endregion
111
+
112
+ renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{
113
+ dirty(pos: Vec3, value: boolean): void
114
+ update(/* pos: Vec3, value: boolean */): void
115
+ chunkFinished(key: string): void
116
+ heightmap(key: string, heightmap: Uint8Array): void
117
+ }>
118
+ customTexturesDataUrl = undefined as string | undefined
119
+ workers: any[] = []
120
+ viewerChunkPosition?: Vec3
121
+ lastCamUpdate = 0
122
+ droppedFpsPercentage = 0
123
+ initialChunkLoadWasStartedIn: number | undefined
124
+ initialChunksLoad = true
125
+ enableChunksLoadDelay = false
126
+ texturesVersion?: string
127
+ viewDistance = -1
128
+ chunksLength = 0
129
+ allChunksFinished = false
130
+ messageQueue: any[] = []
131
+ isProcessingQueue = false
132
+ ONMESSAGE_TIME_LIMIT = 30 // ms
133
+
134
+ handleResize = () => { }
135
+ highestBlocksByChunks = new Map<string, { [chunkKey: string]: HighestBlockInfo }>()
136
+ blockEntities = {}
137
+
138
+ workersProcessAverageTime = 0
139
+ workersProcessAverageTimeCount = 0
140
+ maxWorkersProcessTime = 0
141
+ geometryReceiveCount = {} as Record<number, number>
142
+ allLoadedIn: undefined | number
143
+ onWorldSwitched = [] as Array<() => void>
144
+ renderTimeMax = 0
145
+ renderTimeAvg = 0
146
+ renderTimeAvgCount = 0
147
+ edgeChunks = {} as Record<string, boolean>
148
+ lastAddChunk = null as null | {
149
+ timeout: any
150
+ x: number
151
+ z: number
152
+ }
153
+ neighborChunkUpdates = true
154
+ lastChunkDistance = 0
155
+ debugStopGeometryUpdate = false
156
+
157
+ protocolCustomBlocks = new Map<string, CustomBlockModels>()
158
+
159
+ blockStateModelInfo = new Map<string, BlockStateModelInfo>()
160
+
161
+ abstract outputFormat: 'threeJs' | 'webgpu'
162
+ worldBlockProvider!: WorldBlockProvider
163
+ soundSystem: SoundSystem | undefined
164
+
165
+ abstract changeBackgroundColor(color: [number, number, number]): void
166
+
167
+ worldRendererConfig: WorldRendererConfig
168
+ playerStateReactive: PlayerStateReactive
169
+ playerStateUtils: PlayerStateUtils
170
+ reactiveState: RendererReactiveState
171
+ mesherLogReader: MesherLogReader | undefined
172
+ forceCallFromMesherReplayer = false
173
+ stopMesherMessagesProcessing = false
174
+
175
+ abortController = new AbortController()
176
+ lastRendered = 0
177
+ renderingActive = true
178
+ geometryReceiveCountPerSec = 0
179
+ mesherLogger = {
180
+ contents: [] as string[],
181
+ active: new URL(location.href).searchParams.get('mesherlog') === 'true'
182
+ }
183
+ currentRenderedFrames = 0
184
+ fpsAverage = 0
185
+ lastFps = 0
186
+ fpsWorst = undefined as number | undefined
187
+ fpsSamples = 0
188
+ backendInfoReport = '-'
189
+ chunksFullInfo = '-'
190
+ workerCustomHandleTime = 0
191
+
192
+ get version() {
193
+ return this.displayOptions.version
194
+ }
195
+
196
+ get displayAdvancedStats() {
197
+ return (this.initOptions.config.statsVisible ?? 0) > 1
198
+ }
199
+
200
+ constructor(public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
201
+ this.snapshotInitialValues()
202
+ this.worldRendererConfig = displayOptions.inWorldRenderingConfig
203
+ this.playerStateReactive = displayOptions.playerStateReactive!
204
+ this.playerStateUtils = getPlayerStateUtils(this.playerStateReactive)
205
+ this.reactiveState = displayOptions.rendererState!
206
+ // this.mesherLogReader = new MesherLogReader(this)
207
+ this.renderUpdateEmitter.on('update', () => {
208
+ const loadedChunks = Object.keys(this.finishedChunks).length
209
+ updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`)
210
+ })
211
+
212
+ addNewStat('downloaded-chunks', 100, 140, 20)
213
+
214
+ this.connect(this.displayOptions.worldView as any)
215
+
216
+ const chunksUpdateInterval = setInterval(() => {
217
+ this.geometryReceiveCountPerSec = Object.values(this.geometryReceiveCount).reduce((acc, curr) => acc + curr, 0)
218
+ this.geometryReceiveCount = {}
219
+ updatePanesVisibility(this.displayAdvancedStats)
220
+ this.updateChunksStats()
221
+ }, 500)
222
+ const fpsUpdateInterval = setInterval(() => {
223
+ this.fpsUpdate()
224
+ }, 1000)
225
+ this.abortController.signal.addEventListener('abort', () => {
226
+ clearInterval(chunksUpdateInterval)
227
+ clearInterval(fpsUpdateInterval)
228
+ })
229
+ }
230
+
231
+ fpsUpdate() {
232
+ this.fpsSamples++
233
+ this.fpsAverage = (this.fpsAverage * (this.fpsSamples - 1) + this.currentRenderedFrames) / this.fpsSamples
234
+ if (this.fpsWorst === undefined) {
235
+ this.fpsWorst = this.currentRenderedFrames
236
+ } else {
237
+ this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames)
238
+ }
239
+ this.lastFps = this.currentRenderedFrames
240
+ this.displayOptions.nonReactiveState.fps = this.currentRenderedFrames
241
+ this.displayOptions.nonReactiveState.worstRenderTime = this.renderTimeMax
242
+ this.displayOptions.nonReactiveState.avgRenderTime = this.renderTimeAvg
243
+ this.currentRenderedFrames = 0
244
+ }
245
+
246
+ logWorkerWork(message: string | (() => string)) {
247
+ if (!this.mesherLogger.active) return
248
+ this.mesherLogger.contents.push(typeof message === 'function' ? message() : message)
249
+ }
250
+
251
+ async init() {
252
+ if (this.active) throw new Error('WorldRendererCommon is already initialized')
253
+
254
+ await Promise.all([
255
+ this.resetWorkers(),
256
+ (async () => {
257
+ if (this.resourcesManager.currentResources?.allReady) {
258
+ await this.updateAssetsData()
259
+ }
260
+ })()
261
+ ])
262
+
263
+ this.resourcesManager.on('assetsTexturesUpdated', async () => {
264
+ if (!this.active) return
265
+ await this.updateAssetsData()
266
+ })
267
+
268
+ this.watchReactivePlayerState()
269
+ this.watchReactiveConfig()
270
+ this.worldReadyResolvers.resolve()
271
+ }
272
+
273
+ snapshotInitialValues() { }
274
+
275
+ wasChunkSentToWorker(chunkKey: string) {
276
+ return this.loadedChunks[chunkKey]
277
+ }
278
+
279
+ async getHighestBlocks(chunkKey: string) {
280
+ return this.highestBlocksByChunks.get(chunkKey)
281
+ }
282
+
283
+ updateCustomBlock(chunkKey: string, blockPos: string, model: string) {
284
+ this.protocolCustomBlocks.set(chunkKey, {
285
+ ...this.protocolCustomBlocks.get(chunkKey),
286
+ [blockPos]: model
287
+ })
288
+ this.logWorkerWork(() => `-> updateCustomBlock ${chunkKey} ${blockPos} ${model} ${this.wasChunkSentToWorker(chunkKey)}`)
289
+ if (this.wasChunkSentToWorker(chunkKey)) {
290
+ const [x, y, z] = blockPos.split(',').map(Number)
291
+ this.setBlockStateId(new Vec3(x, y, z), undefined)
292
+ }
293
+ }
294
+
295
+ async getBlockInfo(blockPos: { x: number, y: number, z: number }, stateId: number) {
296
+ const chunkKey = `${Math.floor(blockPos.x / 16) * 16},${Math.floor(blockPos.z / 16) * 16}`
297
+ const customBlockName = this.protocolCustomBlocks.get(chunkKey)?.[`${blockPos.x},${blockPos.y},${blockPos.z}`]
298
+ const cacheKey = getBlockAssetsCacheKey(stateId, customBlockName)
299
+ const modelInfo = this.blockStateModelInfo.get(cacheKey)
300
+ return {
301
+ customBlockName,
302
+ modelInfo
303
+ }
304
+ }
305
+
306
+ initWorkers(numWorkers = this.worldRendererConfig.mesherWorkers) {
307
+ // init workers
308
+ for (let i = 0; i < numWorkers + 1; i++) {
309
+ const worker = initMesherWorker((data) => {
310
+ if (Array.isArray(data)) {
311
+ this.messageQueue.push(...data)
312
+ } else {
313
+ this.messageQueue.push(data)
314
+ }
315
+ void this.processMessageQueue('worker')
316
+ })
317
+ this.workers.push(worker)
318
+ }
319
+ }
320
+
321
+ onReactivePlayerStateUpdated<T extends keyof PlayerStateReactive>(key: T, callback: (value: PlayerStateReactive[T]) => void, initial = true) {
322
+ if (initial) {
323
+ callback(this.playerStateReactive[key])
324
+ }
325
+ subscribeKey(this.playerStateReactive, key, callback)
326
+ }
327
+
328
+ onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {
329
+ callback(this.worldRendererConfig[key])
330
+ subscribeKey(this.worldRendererConfig, key, callback)
331
+ }
332
+
333
+ onReactiveDebugUpdated<T extends keyof typeof this.reactiveDebugParams>(key: T, callback: (value: typeof this.reactiveDebugParams[T]) => void) {
334
+ callback(this.reactiveDebugParams[key])
335
+ subscribeKey(this.reactiveDebugParams, key, callback)
336
+ }
337
+
338
+ watchReactivePlayerState() {
339
+ this.onReactivePlayerStateUpdated('backgroundColor', (value) => {
340
+ this.changeBackgroundColor(value)
341
+ })
342
+ }
343
+
344
+ watchReactiveConfig() {
345
+ this.onReactiveConfigUpdated('fetchPlayerSkins', (value) => {
346
+ setSkinsConfig({ apiEnabled: value })
347
+ })
348
+ }
349
+
350
+ async processMessageQueue(source: string) {
351
+ if (this.isProcessingQueue || this.messageQueue.length === 0) return
352
+ this.logWorkerWork(`# ${source} processing queue`)
353
+ if (this.lastRendered && performance.now() - this.lastRendered > this.ONMESSAGE_TIME_LIMIT && this.worldRendererConfig._experimentalSmoothChunkLoading && this.renderingActive) {
354
+ const start = performance.now()
355
+ await new Promise(resolve => {
356
+ requestAnimationFrame(resolve)
357
+ })
358
+ this.logWorkerWork(`# processing got delayed by ${performance.now() - start}ms`)
359
+ }
360
+ this.isProcessingQueue = true
361
+
362
+ const startTime = performance.now()
363
+ let processedCount = 0
364
+
365
+ while (this.messageQueue.length > 0) {
366
+ const processingStopped = this.stopMesherMessagesProcessing
367
+ if (!processingStopped) {
368
+ const data = this.messageQueue.shift()!
369
+ this.handleMessage(data)
370
+ processedCount++
371
+ }
372
+
373
+ // Check if we've exceeded the time limit
374
+ if (processingStopped || (performance.now() - startTime > this.ONMESSAGE_TIME_LIMIT && this.renderingActive && this.worldRendererConfig._experimentalSmoothChunkLoading)) {
375
+ // If we have more messages and exceeded time limit, schedule next batch
376
+ if (this.messageQueue.length > 0) {
377
+ requestAnimationFrame(async () => {
378
+ this.isProcessingQueue = false
379
+ void this.processMessageQueue('queue-delay')
380
+ })
381
+ return
382
+ }
383
+ break
384
+ }
385
+ }
386
+
387
+ this.isProcessingQueue = false
388
+ }
389
+
390
+ handleMessage(rawData: any) {
391
+ const data = rawData as MesherMainEvent
392
+ if (!this.active) return
393
+ this.mesherLogReader?.workerMessageReceived(data.type, data)
394
+ if (data.type !== 'geometry' || !this.debugStopGeometryUpdate) {
395
+ const start = performance.now()
396
+ this.handleWorkerMessage(data as WorkerReceive)
397
+ this.workerCustomHandleTime += performance.now() - start
398
+ }
399
+ if (data.type === 'geometry') {
400
+ this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`)
401
+ this.geometryReceiveCount[data.workerIndex] ??= 0
402
+ this.geometryReceiveCount[data.workerIndex]++
403
+ const chunkCoords = data.key.split(',').map(Number)
404
+ this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
405
+ }
406
+ if (data.type === 'sectionFinished') { // on after load & unload section
407
+ this.logWorkerWork(`<- ${data.workerIndex} sectionFinished ${data.key} ${JSON.stringify({ processTime: data.processTime })}`)
408
+ if (!this.sectionsWaiting.has(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
409
+ this.sectionsWaiting.set(data.key, this.sectionsWaiting.get(data.key)! - 1)
410
+ if (this.sectionsWaiting.get(data.key) === 0) {
411
+ this.sectionsWaiting.delete(data.key)
412
+ this.finishedSections[data.key] = true
413
+ }
414
+
415
+ const chunkCoords = data.key.split(',').map(Number)
416
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
417
+ if (this.loadedChunks[chunkKey]) { // ensure chunk data was added, not a neighbor chunk update
418
+ let loaded = true
419
+ for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) {
420
+ if (!this.finishedSections[`${chunkCoords[0]},${y},${chunkCoords[2]}`]) {
421
+ loaded = false
422
+ break
423
+ }
424
+ }
425
+ if (loaded) {
426
+ // CHUNK FINISHED
427
+ this.finishedChunks[chunkKey] = true
428
+ this.reactiveState.world.chunksLoaded.add(`${Math.floor(chunkCoords[0] / 16)},${Math.floor(chunkCoords[2] / 16)}`)
429
+ this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0]},${chunkCoords[2]}`)
430
+ this.checkAllFinished()
431
+ // merge highest blocks by sections into highest blocks by chunks
432
+ // for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) {
433
+ // const sectionKey = `${chunkCoords[0]},${y},${chunkCoords[2]}`
434
+ // for (let x = 0; x < 16; x++) {
435
+ // for (let z = 0; z < 16; z++) {
436
+ // const posInsideKey = `${chunkCoords[0] + x},${chunkCoords[2] + z}`
437
+ // let block = null as HighestBlockInfo | null
438
+ // const highestBlock = this.highestBlocksBySections[sectionKey]?.[posInsideKey]
439
+ // if (!highestBlock) continue
440
+ // if (!block || highestBlock.y > block.y) {
441
+ // block = highestBlock
442
+ // }
443
+ // if (block) {
444
+ // this.highestBlocksByChunks[chunkKey] ??= {}
445
+ // this.highestBlocksByChunks[chunkKey][posInsideKey] = block
446
+ // }
447
+ // }
448
+ // }
449
+ // delete this.highestBlocksBySections[sectionKey]
450
+ // }
451
+ }
452
+ }
453
+
454
+ this.renderUpdateEmitter.emit('update')
455
+ if (data.processTime) {
456
+ this.workersProcessAverageTimeCount++
457
+ this.workersProcessAverageTime = ((this.workersProcessAverageTime * (this.workersProcessAverageTimeCount - 1)) + data.processTime) / this.workersProcessAverageTimeCount
458
+ this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime)
459
+ }
460
+ }
461
+
462
+ if (data.type === 'blockStateModelInfo') {
463
+ for (const [cacheKey, info] of Object.entries(data.info)) {
464
+ this.blockStateModelInfo.set(cacheKey, info)
465
+ }
466
+ }
467
+
468
+ if (data.type === 'heightmap') {
469
+ this.reactiveState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap))
470
+ }
471
+ }
472
+
473
+ downloadMesherLog() {
474
+ const a = document.createElement('a')
475
+ a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(this.mesherLogger.contents.join('\n'))
476
+ a.download = 'mesher.log'
477
+ a.click()
478
+ }
479
+
480
+ checkAllFinished() {
481
+ if (this.sectionsWaiting.size === 0) {
482
+ this.reactiveState.world.mesherWork = false
483
+ }
484
+ // todo check exact surrounding chunks
485
+ const allFinished = Object.keys(this.finishedChunks).length >= this.chunksLength
486
+ if (allFinished) {
487
+ this.allChunksLoaded?.()
488
+ this.allChunksFinished = true
489
+ this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
490
+ }
491
+ this.updateChunksStats()
492
+ }
493
+
494
+ changeHandSwingingState(isAnimationPlaying: boolean, isLeftHand: boolean): void { }
495
+
496
+ abstract handleWorkerMessage(data: WorkerReceive): void
497
+
498
+ abstract updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void
499
+
500
+ abstract render(): void
501
+
502
+ /**
503
+ * Optionally update data that are depedendent on the viewer position
504
+ */
505
+ updatePosDataChunk?(key: string): void
506
+
507
+ allChunksLoaded?(): void
508
+
509
+ timeUpdated?(newTime: number): void
510
+
511
+ biomeUpdated?(biome: any): void
512
+
513
+ biomeReset?(): void
514
+
515
+ updateViewerPosition(pos: Vec3) {
516
+ this.viewerChunkPosition = pos
517
+ for (const [key, value] of Object.entries(this.loadedChunks)) {
518
+ if (!value) continue
519
+ this.updatePosDataChunk?.(key)
520
+ }
521
+ }
522
+
523
+ sendWorkers(message: WorkerSend) {
524
+ for (const worker of this.workers) {
525
+ worker.postMessage(message)
526
+ }
527
+ }
528
+
529
+ getDistance(posAbsolute: Vec3) {
530
+ const [botX, botZ] = chunkPos(this.viewerChunkPosition!)
531
+ const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16))
532
+ const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16))
533
+ return [dx, dz] as [number, number]
534
+ }
535
+
536
+ abstract updateShowChunksBorder(value: boolean): void
537
+
538
+ resetWorld() {
539
+ // destroy workers
540
+ for (const worker of this.workers) {
541
+ worker.terminate()
542
+ }
543
+ this.workers = []
544
+ }
545
+
546
+ async resetWorkers() {
547
+ this.resetWorld()
548
+
549
+ // for workers in single file build
550
+ if (typeof document !== 'undefined' && document?.readyState === 'loading') {
551
+ await new Promise(resolve => {
552
+ document.addEventListener('DOMContentLoaded', resolve)
553
+ })
554
+ }
555
+
556
+ this.initWorkers()
557
+ this.active = true
558
+
559
+ this.sendMesherMcData()
560
+ }
561
+
562
+ getMesherConfig(): MesherConfig {
563
+ let skyLight = 15
564
+ const timeOfDay = this.timeOfTheDay
565
+ if (timeOfDay < 0 || timeOfDay > 24_000) {
566
+ //
567
+ } else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
568
+ skyLight = 15
569
+ } else if (timeOfDay > 6000 && timeOfDay < 12_000) {
570
+ skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
571
+ } else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
572
+ skyLight = ((timeOfDay - 12_000) / 6000) * 15
573
+ }
574
+
575
+ skyLight = Math.floor(skyLight)
576
+ return {
577
+ version: this.version,
578
+ enableLighting: this.worldRendererConfig.enableLighting,
579
+ skyLight,
580
+ smoothLighting: this.worldRendererConfig.smoothLighting,
581
+ outputFormat: this.outputFormat,
582
+ // textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
583
+ debugModelVariant: this.worldRendererConfig.debugModelVariant,
584
+ clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
585
+ disableBlockEntityTextures: !this.worldRendererConfig.extraBlockRenderers,
586
+ worldMinY: this.worldMinYRender,
587
+ worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight,
588
+ }
589
+ }
590
+
591
+ sendMesherMcData() {
592
+ const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)]
593
+ const mcData = {
594
+ version: JSON.parse(JSON.stringify(allMcData.version))
595
+ }
596
+ for (const key of dynamicMcDataFiles) {
597
+ mcData[key] = allMcData[key]
598
+ }
599
+
600
+ for (const worker of this.workers) {
601
+ worker.postMessage({ type: 'mcData', mcData, config: this.getMesherConfig() })
602
+ }
603
+ this.logWorkerWork('# mcData sent')
604
+ }
605
+
606
+ async updateAssetsData() {
607
+ const resources = this.resourcesManager.currentResources
608
+
609
+ if (this.workers.length === 0) throw new Error('workers not initialized yet')
610
+ for (const [i, worker] of this.workers.entries()) {
611
+ const { blockstatesModels } = resources
612
+
613
+ worker.postMessage({
614
+ type: 'mesherData',
615
+ workerIndex: i,
616
+ blocksAtlas: {
617
+ latest: resources.blocksAtlasJson
618
+ },
619
+ blockstatesModels,
620
+ config: this.getMesherConfig(),
621
+ })
622
+ }
623
+
624
+ this.logWorkerWork('# mesherData sent')
625
+ console.log('textures loaded')
626
+ }
627
+
628
+ get worldMinYRender() {
629
+ return Math.floor(Math.max(this.worldSizeParams.minY, this.worldRendererConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
630
+ }
631
+
632
+ updateChunksStats() {
633
+ const loadedChunks = Object.keys(this.finishedChunks)
634
+ this.displayOptions.nonReactiveState.world.chunksLoaded = new Set(loadedChunks)
635
+ this.displayOptions.nonReactiveState.world.chunksTotalNumber = this.chunksLength
636
+ this.reactiveState.world.allChunksLoaded = this.allChunksFinished
637
+
638
+ const text = `Q: ${this.messageQueue.length} ${Object.keys(this.loadedChunks).length}/${Object.keys(this.finishedChunks).length}/${this.chunksLength} chunks (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.geometryReceiveCountPerSec}ss/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`
639
+ this.chunksFullInfo = text
640
+ updateStatText('downloaded-chunks', text)
641
+ }
642
+
643
+ addColumn(x: number, z: number, chunk: any, isLightUpdate: boolean) {
644
+ if (!this.active) return
645
+ if (this.workers.length === 0) throw new Error('workers not initialized yet')
646
+ this.initialChunksLoad = false
647
+ this.initialChunkLoadWasStartedIn ??= Date.now()
648
+ this.loadedChunks[`${x},${z}`] = true
649
+ this.updateChunksStats()
650
+
651
+ const chunkKey = `${x},${z}`
652
+ const customBlockModels = this.protocolCustomBlocks.get(chunkKey)
653
+
654
+ for (const worker of this.workers) {
655
+ worker.postMessage({
656
+ type: 'chunk',
657
+ x,
658
+ z,
659
+ chunk,
660
+ customBlockModels: customBlockModels || undefined
661
+ })
662
+ }
663
+ // this.workers[0].postMessage({
664
+ // type: 'getHeightmap',
665
+ // x,
666
+ // z,
667
+ // })
668
+ this.logWorkerWork(() => `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`)
669
+ this.mesherLogReader?.chunkReceived(x, z, chunk.length)
670
+ for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) {
671
+ const loc = new Vec3(x, y, z)
672
+ this.setSectionDirty(loc)
673
+ if (this.neighborChunkUpdates && (!isLightUpdate || this.worldRendererConfig.smoothLighting)) {
674
+ this.setSectionDirty(loc.offset(-16, 0, 0))
675
+ this.setSectionDirty(loc.offset(16, 0, 0))
676
+ this.setSectionDirty(loc.offset(0, 0, -16))
677
+ this.setSectionDirty(loc.offset(0, 0, 16))
678
+ }
679
+ }
680
+ }
681
+
682
+ markAsLoaded(x, z) {
683
+ this.loadedChunks[`${x},${z}`] = true
684
+ this.finishedChunks[`${x},${z}`] = true
685
+ this.logWorkerWork(`-> markAsLoaded ${JSON.stringify({ x, z })}`)
686
+ this.checkAllFinished()
687
+ }
688
+
689
+ removeColumn(x, z) {
690
+ delete this.loadedChunks[`${x},${z}`]
691
+ for (const worker of this.workers) {
692
+ worker.postMessage({ type: 'unloadChunk', x, z })
693
+ }
694
+ this.logWorkerWork(`-> unloadChunk ${JSON.stringify({ x, z })}`)
695
+ delete this.finishedChunks[`${x},${z}`]
696
+ this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
697
+ if (Object.keys(this.finishedChunks).length === 0) {
698
+ this.allLoadedIn = undefined
699
+ this.initialChunkLoadWasStartedIn = undefined
700
+ }
701
+ for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
702
+ this.setSectionDirty(new Vec3(x, y, z), false)
703
+ delete this.finishedSections[`${x},${y},${z}`]
704
+ }
705
+ this.highestBlocksByChunks.delete(`${x},${z}`)
706
+
707
+ this.updateChunksStats()
708
+
709
+ if (Object.keys(this.loadedChunks).length === 0) {
710
+ this.mesherLogger.contents = []
711
+ this.logWorkerWork('# all chunks unloaded. New log started')
712
+ void this.mesherLogReader?.maybeStartReplay()
713
+ }
714
+ }
715
+
716
+ setBlockStateId(pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
717
+ const set = async () => {
718
+ const sectionX = Math.floor(pos.x / 16) * 16
719
+ const sectionZ = Math.floor(pos.z / 16) * 16
720
+ if (this.queuedChunks.has(`${sectionX},${sectionZ}`)) {
721
+ await new Promise<void>(resolve => {
722
+ this.queuedFunctions.push(() => {
723
+ resolve()
724
+ })
725
+ })
726
+ }
727
+ if (!this.loadedChunks[`${sectionX},${sectionZ}`]) {
728
+ // console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
729
+ }
730
+ this.setBlockStateIdInner(pos, stateId, needAoRecalculation)
731
+ }
732
+ void set()
733
+ }
734
+
735
+ updateEntity(e: any, isUpdate = false) { }
736
+
737
+ abstract updatePlayerEntity?(e: any): void
738
+
739
+ lightUpdate(chunkX: number, chunkZ: number) { }
740
+
741
+ connect(worldView: WorldViewWorker) {
742
+ const worldEmitter = worldView
743
+
744
+ worldEmitter.on('entity', (e) => {
745
+ this.updateEntity(e, false)
746
+ })
747
+ worldEmitter.on('entityMoved', (e) => {
748
+ this.updateEntity(e, true)
749
+ })
750
+ worldEmitter.on('playerEntity', (e) => {
751
+ this.updatePlayerEntity?.(e)
752
+ })
753
+
754
+ let currentLoadChunkBatch = null as {
755
+ timeout
756
+ data
757
+ } | null
758
+ worldEmitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
759
+ this.worldSizeParams = worldConfig
760
+ this.queuedChunks.add(`${x},${z}`)
761
+ const args = [x, z, chunk, isLightUpdate]
762
+ if (!currentLoadChunkBatch) {
763
+ // add a setting to use debounce instead
764
+ currentLoadChunkBatch = {
765
+ data: [],
766
+ timeout: setTimeout(() => {
767
+ for (const args of currentLoadChunkBatch!.data) {
768
+ this.queuedChunks.delete(`${args[0]},${args[1]}`)
769
+ this.addColumn(...args as Parameters<typeof this.addColumn>)
770
+ }
771
+ for (const fn of this.queuedFunctions) {
772
+ fn()
773
+ }
774
+ this.queuedFunctions = []
775
+ currentLoadChunkBatch = null
776
+ }, this.worldRendererConfig.addChunksBatchWaitTime)
777
+ }
778
+ }
779
+ currentLoadChunkBatch.data.push(args)
780
+ })
781
+ // todo remove and use other architecture instead so data flow is clear
782
+ worldEmitter.on('blockEntities', (blockEntities) => {
783
+ this.blockEntities = blockEntities
784
+ })
785
+
786
+ worldEmitter.on('unloadChunk', ({ x, z }) => {
787
+ this.removeColumn(x, z)
788
+ })
789
+
790
+ worldEmitter.on('blockUpdate', ({ pos, stateId }) => {
791
+ this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
792
+ })
793
+
794
+ worldEmitter.on('chunkPosUpdate', ({ pos }) => {
795
+ this.updateViewerPosition(pos)
796
+ })
797
+
798
+ worldEmitter.on('end', () => {
799
+ this.worldStop?.()
800
+ })
801
+
802
+
803
+ worldEmitter.on('renderDistance', (d) => {
804
+ this.viewDistance = d
805
+ this.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
806
+ this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
807
+ })
808
+
809
+ worldEmitter.on('markAsLoaded', ({ x, z }) => {
810
+ this.markAsLoaded(x, z)
811
+ })
812
+
813
+ worldEmitter.on('updateLight', ({ pos }) => {
814
+ this.lightUpdate(pos.x, pos.z)
815
+ })
816
+
817
+ worldEmitter.on('onWorldSwitch', () => {
818
+ for (const fn of this.onWorldSwitched) {
819
+ try {
820
+ fn()
821
+ } catch (e) {
822
+ setTimeout(() => {
823
+ console.log('[Renderer Backend] Error in onWorldSwitched:')
824
+ throw e
825
+ }, 0)
826
+ }
827
+ }
828
+ })
829
+
830
+ worldEmitter.on('time', (timeOfDay) => {
831
+ if (!this.worldRendererConfig.dayCycle) return
832
+ this.timeUpdated?.(timeOfDay)
833
+
834
+ this.timeOfTheDay = timeOfDay
835
+
836
+ // if (this.worldRendererConfig.skyLight === skyLight) return
837
+ // this.worldRendererConfig.skyLight = skyLight
838
+ // if (this instanceof WorldRendererThree) {
839
+ // (this).rerenderAllChunks?.()
840
+ // }
841
+ })
842
+
843
+ worldEmitter.on('biomeUpdate', ({ biome }) => {
844
+ this.biomeUpdated?.(biome)
845
+ })
846
+
847
+ worldEmitter.on('biomeReset', () => {
848
+ this.biomeReset?.()
849
+ })
850
+ }
851
+
852
+ setBlockStateIdInner(pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
853
+ const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
854
+ const blockPosKey = `${pos.x},${pos.y},${pos.z}`
855
+ const customBlockModels = this.protocolCustomBlocks.get(chunkKey) || {}
856
+
857
+ for (const worker of this.workers) {
858
+ worker.postMessage({
859
+ type: 'blockUpdate',
860
+ pos,
861
+ stateId,
862
+ customBlockModels
863
+ })
864
+ }
865
+ this.logWorkerWork(`-> blockUpdate ${JSON.stringify({ pos, stateId, customBlockModels })}`)
866
+ this.setSectionDirty(pos, true, true)
867
+ if (this.neighborChunkUpdates) {
868
+ if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, true)
869
+ if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, true)
870
+ if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, true)
871
+ if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, true)
872
+ if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, true)
873
+ if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, true)
874
+
875
+ if (needAoRecalculation) {
876
+ // top view neighbors
877
+ if ((pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, -16), true, true)
878
+ if ((pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(16, 0, -16), true, true)
879
+ if ((pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-16, 0, 16), true, true)
880
+ if ((pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 16), true, true)
881
+
882
+ // side view neighbors (but ignore updates above)
883
+ // z view neighbors
884
+ if ((pos.x & 15) === 0 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(-16, -16, 0), true, true)
885
+ if ((pos.x & 15) === 15 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(16, -16, 0), true, true)
886
+
887
+ // x view neighbors
888
+ if ((pos.z & 15) === 0 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, -16), true, true)
889
+ if ((pos.z & 15) === 15 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 16), true, true)
890
+
891
+ // x & z neighbors
892
+ if ((pos.y & 15) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-16, -16, -16), true, true)
893
+ if ((pos.y & 15) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(16, -16, -16), true, true)
894
+ if ((pos.y & 15) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-16, -16, 16), true, true)
895
+ if ((pos.y & 15) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(16, -16, 16), true, true)
896
+ }
897
+ }
898
+ }
899
+
900
+ abstract worldStop?()
901
+
902
+ queueAwaited = false
903
+ toWorkerMessagesQueue = {} as { [workerIndex: string]: any[] }
904
+
905
+ getWorkerNumber(pos: Vec3, updateAction = false) {
906
+ if (updateAction) {
907
+ const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
908
+ const cantUseChangeWorker = this.sectionsWaiting.get(key) && !this.finishedSections[key]
909
+ if (!cantUseChangeWorker) return 0
910
+ }
911
+
912
+ const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1)
913
+ return hash + 1
914
+ }
915
+
916
+ async debugGetWorkerCustomBlockModel(pos: Vec3) {
917
+ const data = [] as Array<Promise<string>>
918
+ for (const worker of this.workers) {
919
+ data.push(new Promise((resolve) => {
920
+ worker.addEventListener('message', (e) => {
921
+ if (e.data.type === 'customBlockModel') {
922
+ resolve(e.data.customBlockModel)
923
+ }
924
+ })
925
+ }))
926
+ worker.postMessage({
927
+ type: 'getCustomBlockModel',
928
+ pos
929
+ })
930
+ }
931
+ return Promise.all(data)
932
+ }
933
+
934
+ setSectionDirty(pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
935
+ if (!this.forceCallFromMesherReplayer && this.mesherLogReader) return
936
+
937
+ if (this.viewDistance === -1) throw new Error('viewDistance not set')
938
+ this.reactiveState.world.mesherWork = true
939
+ const distance = this.getDistance(pos)
940
+ // todo shouldnt we check loadedChunks instead?
941
+ if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
942
+ const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
943
+ // if (this.sectionsOutstanding.has(key)) return
944
+ this.renderUpdateEmitter.emit('dirty', pos, value)
945
+ // Dispatch sections to workers based on position
946
+ // This guarantees uniformity accross workers and that a given section
947
+ // is always dispatched to the same worker
948
+ const hash = this.getWorkerNumber(pos, useChangeWorker && this.mesherLogger.active)
949
+ this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
950
+ if (this.forceCallFromMesherReplayer) {
951
+ this.workers[hash].postMessage({
952
+ type: 'dirty',
953
+ x: pos.x,
954
+ y: pos.y,
955
+ z: pos.z,
956
+ value,
957
+ config: this.getMesherConfig(),
958
+ })
959
+ } else {
960
+ this.toWorkerMessagesQueue[hash] ??= []
961
+ this.toWorkerMessagesQueue[hash].push({
962
+ // this.workers[hash].postMessage({
963
+ type: 'dirty',
964
+ x: pos.x,
965
+ y: pos.y,
966
+ z: pos.z,
967
+ value,
968
+ config: this.getMesherConfig(),
969
+ })
970
+ this.dispatchMessages()
971
+ }
972
+ }
973
+
974
+ dispatchMessages() {
975
+ if (this.queueAwaited) return
976
+ this.queueAwaited = true
977
+ setTimeout(() => {
978
+ // group messages and send as one
979
+ for (const workerIndex in this.toWorkerMessagesQueue) {
980
+ const worker = this.workers[Number(workerIndex)]
981
+ worker.postMessage(this.toWorkerMessagesQueue[workerIndex])
982
+ for (const message of this.toWorkerMessagesQueue[workerIndex]) {
983
+ this.logWorkerWork(`-> ${workerIndex} dispatchMessages ${message.type} ${JSON.stringify({ x: message.x, y: message.y, z: message.z, value: message.value })}`)
984
+ }
985
+ }
986
+ this.toWorkerMessagesQueue = {}
987
+ this.queueAwaited = false
988
+ })
989
+ }
990
+
991
+ // Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
992
+ // of sections not rendered are 0
993
+ async waitForChunksToRender() {
994
+ return new Promise<void>((resolve, reject) => {
995
+ if ([...this.sectionsWaiting].length === 0) {
996
+ resolve()
997
+ return
998
+ }
999
+
1000
+ const updateHandler = () => {
1001
+ if (this.sectionsWaiting.size === 0) {
1002
+ this.renderUpdateEmitter.removeListener('update', updateHandler)
1003
+ resolve()
1004
+ }
1005
+ }
1006
+ this.renderUpdateEmitter.on('update', updateHandler)
1007
+ })
1008
+ }
1009
+
1010
+ async waitForChunkToLoad(pos: Vec3) {
1011
+ return new Promise<void>((resolve, reject) => {
1012
+ const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
1013
+ if (this.loadedChunks[key]) {
1014
+ resolve()
1015
+ return
1016
+ }
1017
+ const updateHandler = () => {
1018
+ if (this.loadedChunks[key]) {
1019
+ this.renderUpdateEmitter.removeListener('update', updateHandler)
1020
+ resolve()
1021
+ }
1022
+ }
1023
+ this.renderUpdateEmitter.on('update', updateHandler)
1024
+ })
1025
+ }
1026
+
1027
+ destroy() {
1028
+ // Stop all workers
1029
+ for (const worker of this.workers) {
1030
+ worker.terminate()
1031
+ }
1032
+ this.workers = []
1033
+
1034
+ // Stop and destroy sound system
1035
+ if (this.soundSystem) {
1036
+ this.soundSystem.destroy()
1037
+ this.soundSystem = undefined
1038
+ }
1039
+
1040
+ this.active = false
1041
+
1042
+ // Clear chunk and section tracking (previously handled by @worldCleanup decorator)
1043
+ this.loadedChunks = {}
1044
+ this.finishedChunks = {}
1045
+ this.finishedSections = {}
1046
+ this.sectionsWaiting.clear()
1047
+ this.queuedChunks.clear()
1048
+ this.blockStateModelInfo.clear()
1049
+
1050
+ this.renderUpdateEmitter.removeAllListeners()
1051
+ this.abortController.abort()
1052
+ removeAllStats()
1053
+ }
1054
+ }
1055
+
1056
+ export const initMesherWorker = (onGotMessage: (data: any) => void) => {
1057
+ // Node environment needs an absolute path, but browser needs the url of the file
1058
+ const workerName = 'mesher.js'
1059
+
1060
+ let worker: any
1061
+ if (process.env.SINGLE_FILE_BUILD) {
1062
+ const workerCode = document.getElementById('mesher-worker-code')!.textContent!
1063
+ const blob = new Blob([workerCode], { type: 'text/javascript' })
1064
+ worker = new Worker(window.URL.createObjectURL(blob))
1065
+ } else {
1066
+ worker = new Worker(workerName)
1067
+ }
1068
+
1069
+ worker.onmessage = ({ data }) => {
1070
+ onGotMessage(data)
1071
+ }
1072
+ if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
1073
+ return worker
1074
+ }
1075
+
1076
+ export const meshersSendMcData = (workers: Worker[], version: string, mcDataKeys: string[] = dynamicMcDataFiles) => {
1077
+ const allMcData = mcDataRaw.pc[version] ?? mcDataRaw.pc[toMajorVersion(version)]
1078
+ const mcData = {
1079
+ version: JSON.parse(JSON.stringify(allMcData.version))
1080
+ }
1081
+ for (const key of mcDataKeys) {
1082
+ mcData[key] = allMcData[key]
1083
+ }
1084
+
1085
+ for (const worker of workers) {
1086
+ worker.postMessage({ type: 'mcData', mcData })
1087
+ }
1088
+ }