minecraft-renderer 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,6 +28,7 @@ import { PlayerStateReactive } from '../playerState/playerState'
28
28
  import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
29
29
  import { preloadMesherWorkerScript } from './preloadWorkers'
30
30
  import type { MenuBackgroundOptions } from '../three/menuBackground/types'
31
+ import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
31
32
 
32
33
  export interface AppViewerOptions {
33
34
  config?: Partial<GraphicsBackendConfig>
@@ -81,6 +82,9 @@ export class AppViewer {
81
82
  // Timing
82
83
  lastCamUpdate = 0
83
84
 
85
+ /** Bound by `subscribeRendererOptions` / `bindRendererOptions` — source of truth for renderer-owned settings. */
86
+ private getRendererOptions?: () => RendererStorageOptions
87
+
84
88
  constructor(options: AppViewerOptions = {}, public resourcesManager: ResourcesManager = new ResourcesManager()) {
85
89
  this.config = {
86
90
  ...defaultGraphicsBackendConfig,
@@ -117,6 +121,11 @@ export class AppViewer {
117
121
  return preloadMesherWorkerScript({ script })
118
122
  }
119
123
 
124
+ /** Wire app options storage (valtio proxy) for backend init (WebGL gpuPreference, etc.). */
125
+ bindRendererOptions(getOptions: () => RendererStorageOptions): void {
126
+ this.getRendererOptions = getOptions
127
+ }
128
+
120
129
  /**
121
130
  * Load a graphics backend.
122
131
  */
@@ -132,6 +141,7 @@ export class AppViewer {
132
141
 
133
142
  const loaderOptions: GraphicsInitOptions = {
134
143
  config: this.config,
144
+ getRendererOptions: this.getRendererOptions,
135
145
  callbacks: {
136
146
  displayCriticalError: (error) => {
137
147
  console.error('[AppViewer] Critical error:', error)
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { proxy } from 'valtio'
7
+ import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
7
8
  import type {
8
9
  GraphicsBackendConfig,
9
10
  RendererReactiveState,
@@ -88,7 +89,6 @@ export type WorldRendererConfig = typeof defaultWorldRendererConfig
88
89
  */
89
90
  export const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
90
91
  fpsLimit: undefined,
91
- powerPreference: undefined,
92
92
  sceneBackground: 'lightblue',
93
93
  timeoutRendering: false
94
94
  }
@@ -117,6 +117,7 @@ export const getDefaultRendererState = (): {
117
117
  heightmaps: new Map<string, Int16Array>(),
118
118
  allChunksLoaded: false,
119
119
  mesherWork: false,
120
+ instabilityFactors: defaultPerformanceInstabilityFactors(),
120
121
  intersectMedia: null
121
122
  },
122
123
  renderer: '...',
@@ -9,3 +9,5 @@ export * from './types'
9
9
  export * from './config'
10
10
  export * from './playerState'
11
11
  export * from './appViewer'
12
+ export * from './rendererOptionsSync'
13
+ export * from '../performanceMonitor'
@@ -0,0 +1,243 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Maps app options storage → AppViewer runtime (in-world config, graphics config, menu background).
4
+ * Call `subscribeRendererOptions` once after viewer init; keep volume sync in the app.
5
+ */
6
+
7
+ import { subscribe } from 'valtio/vanilla'
8
+ import type { AppViewer } from './appViewer'
9
+ import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
10
+ import type { MenuBackgroundOptions } from '../three/menuBackground/types'
11
+ import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
12
+ import { menuBackgroundSpeedToMultiplier } from '../three/menuBackground/config'
13
+ import type { FuturisticCameraId, FuturisticSceneId, MinecraftBlockGroupId } from '../three/menuBackground/futuristic'
14
+ import { setSkinsConfig } from '../lib/utils/skins'
15
+
16
+ export type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
17
+
18
+ export interface ApplyRendererOptionsContext {
19
+ isSafari?: boolean
20
+ isCypress?: boolean
21
+ windowFocused?: boolean
22
+ }
23
+
24
+ export interface RendererOptionsSubscribeHooks {
25
+ isSafari?: boolean
26
+ isCypress?: boolean
27
+ getWindowFocused?: () => boolean
28
+ onRegisterFocusHandlers?: (handlers: { onFocus: () => void, onBlur: () => void }) => void
29
+ }
30
+
31
+ export interface RendererWorldViewLike {
32
+ keepChunksDistance: number
33
+ }
34
+
35
+ export function menuBackgroundOptionsFromStorage(o: Pick<
36
+ RendererStorageOptions,
37
+ | 'menuBackgroundMode'
38
+ | 'menuBackgroundMinecraftTextures'
39
+ | 'menuBackgroundFuturisticScene'
40
+ | 'menuBackgroundFuturisticCamera'
41
+ | 'menuBackgroundFuturisticBlockGroup'
42
+ | 'menuBackgroundFuturisticCameraSpeed'
43
+ | 'menuBackgroundFuturisticBlockSpeed'
44
+ >): MenuBackgroundOptions {
45
+ return {
46
+ mode: o.menuBackgroundMode as MenuBackgroundOptions['mode'],
47
+ useMinecraftTextures: o.menuBackgroundMinecraftTextures,
48
+ futuristicScene: o.menuBackgroundFuturisticScene as FuturisticSceneId,
49
+ futuristicCamera: o.menuBackgroundFuturisticCamera as FuturisticCameraId,
50
+ futuristicBlockGroup: o.menuBackgroundFuturisticBlockGroup as MinecraftBlockGroupId,
51
+ futuristicCameraSpeed: menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticCameraSpeed),
52
+ futuristicBlockSpeed: menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticBlockSpeed),
53
+ }
54
+ }
55
+
56
+ export function applyMenuBackgroundLiveOptions(
57
+ menu: MenuBackgroundRenderer,
58
+ o: Pick<
59
+ RendererStorageOptions,
60
+ | 'menuBackgroundFuturisticScene'
61
+ | 'menuBackgroundFuturisticCamera'
62
+ | 'menuBackgroundFuturisticBlockGroup'
63
+ | 'menuBackgroundFuturisticCameraSpeed'
64
+ | 'menuBackgroundFuturisticBlockSpeed'
65
+ >
66
+ ): void {
67
+ const futuristic = menu.futuristic
68
+ if (!futuristic) return
69
+ futuristic.setScene?.(o.menuBackgroundFuturisticScene)
70
+ futuristic.setCamera?.(o.menuBackgroundFuturisticCamera)
71
+ void futuristic.setBlockGroup?.(o.menuBackgroundFuturisticBlockGroup)
72
+ futuristic.setCameraSpeed?.(menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticCameraSpeed))
73
+ futuristic.setBlockSpeed?.(menuBackgroundSpeedToMultiplier(o.menuBackgroundFuturisticBlockSpeed))
74
+ }
75
+
76
+ function resolveWasmMesherActive(o: RendererStorageOptions): boolean {
77
+ return o.rendererMesher !== 'legacy-js'
78
+ }
79
+
80
+ function applyMesherWorkersPreset(
81
+ appViewer: AppViewer,
82
+ o: RendererStorageOptions,
83
+ wasmActive: boolean
84
+ ): void {
85
+ const cfg = appViewer.inWorldRenderingConfig
86
+ const override = o.rendererMeshersCountOverride
87
+ const applyMesherWorkers = (workers: number) => {
88
+ cfg.mesherWorkers = override ?? workers
89
+ }
90
+ switch (o.rendererWorldPerformance) {
91
+ case 'low-energy':
92
+ applyMesherWorkers(1)
93
+ cfg.dedicatedChangeWorker = false
94
+ break
95
+ case 'normal':
96
+ applyMesherWorkers(2)
97
+ cfg.dedicatedChangeWorker = !wasmActive
98
+ break
99
+ case 'maximum':
100
+ applyMesherWorkers(Math.max(3, Math.min(navigator.hardwareConcurrency ?? 0, 8)))
101
+ cfg.dedicatedChangeWorker = !wasmActive
102
+ break
103
+ }
104
+ }
105
+
106
+ function applyFpsLimit(
107
+ appViewer: AppViewer,
108
+ o: RendererStorageOptions,
109
+ windowFocused: boolean
110
+ ): void {
111
+ const backgroundFpsLimit = o.backgroundRendering
112
+ const normalFpsLimit = o.frameLimit
113
+
114
+ if (windowFocused) {
115
+ appViewer.config.fpsLimit = normalFpsLimit || undefined
116
+ } else if (backgroundFpsLimit === '5fps') {
117
+ appViewer.config.fpsLimit = 5
118
+ } else if (backgroundFpsLimit === '20fps') {
119
+ appViewer.config.fpsLimit = 20
120
+ } else {
121
+ appViewer.config.fpsLimit = undefined
122
+ }
123
+ }
124
+
125
+ function applyStatsVisible(
126
+ appViewer: AppViewer,
127
+ o: RendererStorageOptions,
128
+ ctx: ApplyRendererOptionsContext
129
+ ): void {
130
+ const { renderDebug } = o
131
+ if (renderDebug === 'none' || ctx.isCypress) {
132
+ appViewer.config.statsVisible = 0
133
+ } else if (renderDebug === 'basic') {
134
+ appViewer.config.statsVisible = 1
135
+ } else if (renderDebug === 'advanced') {
136
+ appViewer.config.statsVisible = 2
137
+ }
138
+ }
139
+
140
+ // ensure no object assigns to the config
141
+ export function applyRendererOptions(
142
+ appViewer: AppViewer,
143
+ o: RendererStorageOptions,
144
+ ctx: ApplyRendererOptionsContext = {}
145
+ ): void {
146
+ const cfg = appViewer.inWorldRenderingConfig
147
+ const wasmActive = resolveWasmMesherActive(o)
148
+
149
+ cfg.showChunkBorders = o.showChunkBorders
150
+ cfg.futuristicReveal = o.rendererFuturisticReveal
151
+ applyMesherWorkersPreset(appViewer, o, wasmActive)
152
+ cfg.renderEntities = o.renderEntities
153
+ applyStatsVisible(appViewer, o, ctx)
154
+ applyFpsLimit(appViewer, o, ctx.windowFocused !== false)
155
+
156
+ cfg.vrSupport = o.vrSupport
157
+ cfg.vrPageGameRendering = o.vrPageGameRendering
158
+ cfg.enableDebugOverlay = o.rendererPerfDebugOverlay
159
+
160
+ cfg.clipWorldBelowY = o.clipWorldBelowY
161
+ cfg.extraBlockRenderers = !o.disableBlockEntityTextures
162
+ cfg.fetchPlayerSkins = o.loadPlayerSkins
163
+ cfg.highlightBlockColor = o.highlightBlockColor
164
+ cfg.wasmMesher = wasmActive
165
+ cfg.disableMesherConversionCache = !!ctx.isSafari
166
+
167
+ setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
168
+
169
+ cfg.smoothLighting = o.smoothLighting
170
+ cfg.shadingTheme = o.vanillaLook ? 'vanilla' : 'high-contrast'
171
+ cfg.starfield = o.starfieldRendering
172
+ cfg.defaultSkybox = o.defaultSkybox
173
+ }
174
+
175
+ /** World-view + hand/camera options (call when WorldView is ready). */
176
+ export function applyRendererWorldViewOptions(
177
+ appViewer: AppViewer,
178
+ worldView: RendererWorldViewLike,
179
+ o: Pick<
180
+ RendererStorageOptions,
181
+ 'keepChunksDistance' | 'renderEars' | 'showHand' | 'viewBobbing' | 'dayCycleAndLighting'
182
+ >
183
+ ): void {
184
+ worldView.keepChunksDistance = o.keepChunksDistance
185
+ const cfg = appViewer.inWorldRenderingConfig
186
+ cfg.renderEars = o.renderEars
187
+ cfg.showHand = o.showHand
188
+ cfg.viewBobbing = o.viewBobbing
189
+ cfg.dayCycle = o.dayCycleAndLighting
190
+ }
191
+
192
+ /**
193
+ * Subscribe to options changes and sync renderer runtime.
194
+ * Returns unsubscribe. Volume is intentionally excluded — wire it in the app.
195
+ */
196
+ export function subscribeRendererOptions<T extends RendererStorageOptions>(
197
+ appViewer: AppViewer,
198
+ optionsProxy: T,
199
+ hooks: RendererOptionsSubscribeHooks = {}
200
+ ): () => void {
201
+ appViewer.bindRendererOptions(() => optionsProxy as RendererStorageOptions)
202
+
203
+ let windowFocused = hooks.getWindowFocused?.() ?? true
204
+
205
+ const run = () => {
206
+ const snapshot = optionsProxy as RendererStorageOptions
207
+ applyRendererOptions(appViewer, snapshot, {
208
+ isSafari: hooks.isSafari,
209
+ isCypress: hooks.isCypress,
210
+ windowFocused,
211
+ })
212
+
213
+ if (appViewer.currentDisplay === 'menu') {
214
+ const menu = appViewer.backend?.getMenuBackground?.()
215
+ if (menu) applyMenuBackgroundLiveOptions(menu, snapshot)
216
+ }
217
+ }
218
+
219
+ run()
220
+
221
+ hooks.onRegisterFocusHandlers?.({
222
+ onFocus: () => {
223
+ windowFocused = true
224
+ run()
225
+ },
226
+ onBlur: () => {
227
+ windowFocused = false
228
+ run()
229
+ },
230
+ })
231
+
232
+ return subscribe(optionsProxy, run)
233
+ }
234
+
235
+ /** Call when mineflayer bot is created (lighting depends on protocol features). */
236
+ export function applyRendererEnableLighting(
237
+ appViewer: AppViewer,
238
+ newVersionsLighting: boolean,
239
+ blockStateIdSupported: boolean
240
+ ): void {
241
+ appViewer.inWorldRenderingConfig.enableLighting =
242
+ !blockStateIdSupported || newVersionsLighting
243
+ }
@@ -23,11 +23,13 @@ export interface SoundSystem {
23
23
  }
24
24
 
25
25
  import type { MenuBackgroundOptions } from '../three/menuBackground/types'
26
+ import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
27
+ import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
28
+ import type { PerformanceInstabilityFactors } from '../performanceMonitor'
26
29
 
27
30
  /** Graphics backend configuration */
28
31
  export interface GraphicsBackendConfig {
29
32
  fpsLimit?: number
30
- powerPreference?: 'high-performance' | 'low-power'
31
33
  statsVisible?: number
32
34
  sceneBackground: string
33
35
  timeoutRendering?: boolean
@@ -76,6 +78,8 @@ export interface RendererReactiveState {
76
78
  heightmaps: Map<string, Int16Array>
77
79
  allChunksLoaded: boolean
78
80
  mesherWork: boolean
81
+ /** Low-FPS / render instability factors (see `performanceMonitor`). */
82
+ instabilityFactors: PerformanceInstabilityFactors
79
83
  intersectMedia: any | null
80
84
  }
81
85
  renderer: string
@@ -93,6 +97,8 @@ export interface RendererReactiveState {
93
97
  /** Graphics initialization options */
94
98
  export interface GraphicsInitOptions<S = any> {
95
99
  config: GraphicsBackendConfig
100
+ /** Live app options (e.g. valtio proxy); used for WebGL `gpuPreference` at context creation. */
101
+ getRendererOptions?: () => RendererStorageOptions
96
102
  rendererSpecificSettings: S
97
103
  callbacks: {
98
104
  displayCriticalError: (error: Error) => void
@@ -124,6 +130,8 @@ export interface GraphicsBackend {
124
130
  soundSystem?: any
125
131
  backendMethods?: any
126
132
  getDebugOverlay?(): { entitiesString?: string, left?: Record<string, string>, right?: Record<string, string> }
133
+ /** Active main-menu background, when `currentDisplay === 'menu'`. */
134
+ getMenuBackground?(): MenuBackgroundRenderer | undefined
127
135
  }
128
136
 
129
137
  /** Graphics backend loader function type */
@@ -0,0 +1,79 @@
1
+ //@ts-nocheck
2
+ import { Vec3 } from 'vec3'
3
+ import type { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
4
+ import legacyJson from '../lib/preflatMap.json'
5
+ import type { World, WorldBlock as Block } from './world'
6
+
7
+ const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
8
+
9
+ /**
10
+ * Block name + properties for model lookup. Only runs neighbor/preflat work when
11
+ * `world.preflat` (legacy); modern block-state worlds use `fromStateId` only.
12
+ */
13
+ export function resolveBlockPropertiesForMeshing(
14
+ world: World | undefined,
15
+ cursor: Vec3,
16
+ blockProvider: WorldBlockProvider,
17
+ blockStateId: number,
18
+ PrismarineBlockCtor: { fromStateId: (id: number, biome: number) => Block }
19
+ ): { name: string, properties: Record<string, unknown> } {
20
+ if (world?.preflat) {
21
+ const block = world.getBlock(cursor, blockProvider, {})
22
+ if (block) {
23
+ let properties: Record<string, unknown> = { ...block.getProperties() }
24
+ const patch = preflatBlockCalculation(block, world, cursor)
25
+ if (patch) properties = { ...properties, ...patch }
26
+ return { name: block.name, properties }
27
+ }
28
+ }
29
+ const fromState = PrismarineBlockCtor.fromStateId(blockStateId, 1)
30
+ return { name: fromState.name, properties: fromState.getProperties() }
31
+ }
32
+
33
+ export function preflatBlockCalculation(block: Block, world: World, position: Vec3) {
34
+ const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
35
+ if (!type) return
36
+ switch (type) {
37
+ case 'directional': {
38
+ const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire')
39
+ const neighbors = [
40
+ world.getBlock(position.offset(0, 0, 1)),
41
+ world.getBlock(position.offset(0, 0, -1)),
42
+ world.getBlock(position.offset(1, 0, 0)),
43
+ world.getBlock(position.offset(-1, 0, 0))
44
+ ]
45
+ const props = {}
46
+ let changed = false
47
+ for (const [i, neighbor] of neighbors.entries()) {
48
+ const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
49
+ if (isConnectedToSolid || neighbor?.name === block.name) {
50
+ props[['south', 'north', 'east', 'west'][i]] = 'true'
51
+ changed = true
52
+ }
53
+ }
54
+ return changed ? props : undefined
55
+ }
56
+ case 'block_snowy': {
57
+ const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
58
+ if (aboveIsSnow) {
59
+ return {
60
+ snowy: `${aboveIsSnow}`
61
+ }
62
+ } else {
63
+ return
64
+ }
65
+ }
66
+ case 'door': {
67
+ const { half } = block.getProperties()
68
+ if (half === 'upper') {
69
+ const lower = world.getBlock(position.offset(0, -1, 0))
70
+ if (lower?.name === block.name) {
71
+ return {
72
+ ...lower.getProperties(),
73
+ half: 'upper'
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,26 @@
1
+ //@ts-nocheck
2
+ /** Shared geometry export shapes (worker bridge + main-thread viewer). */
3
+
4
+ export interface ExportedSection {
5
+ key: string
6
+ position: { x: number, y: number, z: number }
7
+ geometry: {
8
+ positions: number[]
9
+ normals: number[]
10
+ colors: number[]
11
+ uvs: number[]
12
+ indices: number[]
13
+ }
14
+ shaderCubes?: unknown
15
+ }
16
+
17
+ export interface ExportedWorldGeometry {
18
+ version: string
19
+ exportedAt: string
20
+ camera: {
21
+ position: { x: number, y: number, z: number }
22
+ rotation: { pitch: number, yaw: number }
23
+ }
24
+ sections: ExportedSection[]
25
+ textureAtlasDataUrl?: string
26
+ }
@@ -2,7 +2,6 @@
2
2
  import { Vec3 } from 'vec3'
3
3
  import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
4
4
  import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
5
- import legacyJson from '../lib/preflatMap.json'
6
5
  import { BlockType } from '../playground/shared'
7
6
  import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock, worldColumnKey } from './world'
8
7
  import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
@@ -10,6 +9,9 @@ import { getSideShading, vertexLightFromAo } from './vertexShading'
10
9
  import { INVISIBLE_BLOCKS } from './worldConstants'
11
10
  import { MesherGeometryOutput, HighestBlockInfo } from './shared'
12
11
  import { collectBlockEntityMetadata } from './blockEntityMetadata'
12
+ import { preflatBlockCalculation, resolveBlockPropertiesForMeshing } from './blockPropertiesForMeshing'
13
+
14
+ export { preflatBlockCalculation, resolveBlockPropertiesForMeshing } from './blockPropertiesForMeshing'
13
15
 
14
16
  // Log function disabled by default for zero overhead in production hot loops
15
17
  const ENABLE_TS_LOGS = false
@@ -51,84 +53,6 @@ function prepareTints(tints) {
51
53
  })
52
54
  }
53
55
 
54
- const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
55
-
56
- /**
57
- * Block name + properties for model lookup. Only runs neighbor/preflat work when
58
- * `world.preflat` (legacy); modern block-state worlds use `fromStateId` only.
59
- */
60
- export function resolveBlockPropertiesForMeshing(
61
- world: World | undefined,
62
- cursor: Vec3,
63
- blockProvider: WorldBlockProvider,
64
- blockStateId: number,
65
- PrismarineBlockCtor: { fromStateId: (id: number, biome: number) => Block }
66
- ): { name: string, properties: Record<string, unknown> } {
67
- if (world?.preflat) {
68
- const block = world.getBlock(cursor, blockProvider, {})
69
- if (block) {
70
- let properties: Record<string, unknown> = { ...block.getProperties() }
71
- const patch = preflatBlockCalculation(block, world, cursor)
72
- if (patch) properties = { ...properties, ...patch }
73
- return { name: block.name, properties }
74
- }
75
- }
76
- const fromState = PrismarineBlockCtor.fromStateId(blockStateId, 1)
77
- return { name: fromState.name, properties: fromState.getProperties() }
78
- }
79
-
80
- export function preflatBlockCalculation(block: Block, world: World, position: Vec3) {
81
- const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
82
- if (!type) return
83
- switch (type) {
84
- case 'directional': {
85
- const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire')
86
- const neighbors = [
87
- world.getBlock(position.offset(0, 0, 1)),
88
- world.getBlock(position.offset(0, 0, -1)),
89
- world.getBlock(position.offset(1, 0, 0)),
90
- world.getBlock(position.offset(-1, 0, 0))
91
- ]
92
- // set needed props to true: east:'false',north:'false',south:'false',west:'false'
93
- const props = {}
94
- let changed = false
95
- for (const [i, neighbor] of neighbors.entries()) {
96
- const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
97
- if (isConnectedToSolid || neighbor?.name === block.name) {
98
- props[['south', 'north', 'east', 'west'][i]] = 'true'
99
- changed = true
100
- }
101
- }
102
- return changed ? props : undefined
103
- }
104
- // case 'gate_in_wall': {}
105
- case 'block_snowy': {
106
- const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
107
- if (aboveIsSnow) {
108
- return {
109
- snowy: `${aboveIsSnow}`
110
- }
111
- } else {
112
- return
113
- }
114
- }
115
- case 'door': {
116
- // upper half matches lower in
117
- const { half } = block.getProperties()
118
- if (half === 'upper') {
119
- // copy other properties
120
- const lower = world.getBlock(position.offset(0, -1, 0))
121
- if (lower?.name === block.name) {
122
- return {
123
- ...lower.getProperties(),
124
- half: 'upper'
125
- }
126
- }
127
- }
128
- }
129
- }
130
- }
131
-
132
56
  function tintToGl(tint) {
133
57
  const r = (tint >> 16) & 0xff
134
58
  const g = (tint >> 8) & 0xff
@@ -0,0 +1,77 @@
1
+ //@ts-nocheck
2
+ import {
3
+ CONSTANT_LONG_RENDER_FRACTION,
4
+ CONSTANT_LONG_RENDER_MIN_SAMPLES,
5
+ FAST_SCENE_WITHOUT_ENTITIES_MS,
6
+ HIGH_TEXTURE_COUNT,
7
+ LONG_RENDER_TIME_MS,
8
+ LOW_FPS_THRESHOLD,
9
+ RENDER_TIME_HISTORY_SIZE,
10
+ SLOW_ENTITIES_RENDER_MS,
11
+ } from './constants'
12
+ import type { FramePerformanceSample, PerformanceInstabilityFactors } from './types'
13
+
14
+ /**
15
+ * Tracks render/FPS signals and writes instability factors into reactive state
16
+ * (alongside `mesherWork`).
17
+ */
18
+ export class PerformanceMonitor {
19
+ private readonly renderTimeHistory: number[] = []
20
+
21
+ constructor(private readonly factors: PerformanceInstabilityFactors) {}
22
+
23
+ onFrame(sample: FramePerformanceSample): void {
24
+ this.pushRenderTime(sample.totalMs)
25
+ this.recompute(sample)
26
+ }
27
+
28
+ private pushRenderTime(ms: number): void {
29
+ this.renderTimeHistory.push(ms)
30
+ if (this.renderTimeHistory.length > RENDER_TIME_HISTORY_SIZE) {
31
+ this.renderTimeHistory.shift()
32
+ }
33
+ }
34
+
35
+ private recompute(sample: FramePerformanceSample): void {
36
+ const lowFps = sample.fps > 0 && sample.fps <= LOW_FPS_THRESHOLD
37
+ const sceneWithoutEntitiesMs = Math.max(0, sample.totalMs - sample.entitiesMs)
38
+
39
+ const longRenderTime = sample.totalMs >= LONG_RENDER_TIME_MS
40
+
41
+ const historyLen = this.renderTimeHistory.length
42
+ const longFrames = this.renderTimeHistory.filter(t => t >= LONG_RENDER_TIME_MS).length
43
+ const constantLongRenderTime =
44
+ historyLen >= CONSTANT_LONG_RENDER_MIN_SAMPLES &&
45
+ longFrames / historyLen >= CONSTANT_LONG_RENDER_FRACTION
46
+
47
+ const tooManyTextures = sample.loadedTextureCount >= HIGH_TEXTURE_COUNT
48
+
49
+ const tooManyEntities =
50
+ lowFps &&
51
+ sample.entitiesMs >= SLOW_ENTITIES_RENDER_MS &&
52
+ sceneWithoutEntitiesMs <= FAST_SCENE_WITHOUT_ENTITIES_MS
53
+
54
+ const hasKnownCause =
55
+ longRenderTime ||
56
+ constantLongRenderTime ||
57
+ tooManyEntities ||
58
+ tooManyTextures
59
+
60
+ const unknownReason = lowFps && !hasKnownCause
61
+
62
+ this.factors.longRenderTime = longRenderTime
63
+ this.factors.constantLongRenderTime = constantLongRenderTime
64
+ this.factors.tooManyEntities = tooManyEntities
65
+ this.factors.tooManyTextures = tooManyTextures
66
+ this.factors.unknownReason = unknownReason
67
+ }
68
+
69
+ reset(): void {
70
+ this.renderTimeHistory.length = 0
71
+ this.factors.longRenderTime = false
72
+ this.factors.constantLongRenderTime = false
73
+ this.factors.tooManyEntities = false
74
+ this.factors.tooManyTextures = false
75
+ this.factors.unknownReason = false
76
+ }
77
+ }
@@ -0,0 +1,24 @@
1
+ //@ts-nocheck
2
+ /** Recent frame exceeded this → `longRenderTime`. */
3
+ export const LONG_RENDER_TIME_MS = 30
4
+
5
+ /** Scene pass without entities faster than this → candidate for entity bottleneck. */
6
+ export const FAST_SCENE_WITHOUT_ENTITIES_MS = 20
7
+
8
+ /** Entity pass slower than this (with low FPS) → `tooManyEntities`. */
9
+ export const SLOW_ENTITIES_RENDER_MS = 8
10
+
11
+ /** FPS at or below this is treated as low performance. */
12
+ export const LOW_FPS_THRESHOLD = 45
13
+
14
+ /** Loaded WebGL textures at or above this → `tooManyTextures` (labels, signs, iOS). */
15
+ export const HIGH_TEXTURE_COUNT = 100
16
+
17
+ /** Ring buffer length for sustained render-time analysis. */
18
+ export const RENDER_TIME_HISTORY_SIZE = 24
19
+
20
+ /** Fraction of recent frames over `LONG_RENDER_TIME_MS` → `constantLongRenderTime`. */
21
+ export const CONSTANT_LONG_RENDER_FRACTION = 0.65
22
+
23
+ /** Minimum frames in history before `constantLongRenderTime` can trigger. */
24
+ export const CONSTANT_LONG_RENDER_MIN_SAMPLES = 8
@@ -0,0 +1,16 @@
1
+ //@ts-nocheck
2
+ import type { PerformanceInstabilityFactors } from './types'
3
+
4
+ const FACTOR_CODES: Array<{ key: keyof PerformanceInstabilityFactors, code: string }> = [
5
+ { key: 'longRenderTime', code: 'LR' },
6
+ { key: 'constantLongRenderTime', code: 'CLR' },
7
+ { key: 'tooManyEntities', code: 'ENT' },
8
+ { key: 'tooManyTextures', code: 'TEX' },
9
+ { key: 'unknownReason', code: 'UNK' },
10
+ ]
11
+
12
+ /** Compact debug overlay fragment, e.g. `LR+ENT` or empty string. */
13
+ export function formatPerformanceFactorsDebug(factors: PerformanceInstabilityFactors): string {
14
+ const active = FACTOR_CODES.filter(({ key }) => factors[key]).map(({ code }) => code)
15
+ return active.length > 0 ? active.join('+') : ''
16
+ }