minecraft-renderer 0.1.106 → 0.1.108
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/dist/lightOwnerWorker.js +9 -0
- package/dist/lightOwnerWorker.js.map +7 -0
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +222 -222
- package/dist/minecraft-renderer.js +105 -76
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +537 -508
- package/package.json +1 -1
- package/src/bundler/bundlePrepare.test.ts +25 -0
- package/src/bundler/bundlePrepare.ts +1 -1
- package/src/graphicsBackend/appViewer.ts +15 -0
- package/src/graphicsBackend/config.ts +8 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/skyLight.ts +17 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +340 -67
- package/src/lighting/cache/rendererLightCache.test.ts +158 -0
- package/src/lighting/cache/rendererLightCache.ts +248 -0
- package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
- package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
- package/src/lighting/debug/clientLightProbe.test.ts +24 -0
- package/src/lighting/debug/clientLightProbe.ts +20 -0
- package/src/lighting/debug/clientLightTrace.test.ts +271 -0
- package/src/lighting/debug/clientLightTrace.ts +335 -0
- package/src/lighting/entity/entityLightController.test.ts +216 -0
- package/src/lighting/entity/entityLightController.ts +322 -0
- package/src/lighting/owner/clientLightOwner.test.ts +467 -0
- package/src/lighting/owner/clientLightOwner.ts +523 -0
- package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
- package/src/lighting/owner/lightOwnerHost.ts +202 -0
- package/src/lighting/owner/lightOwnerWorker.ts +154 -0
- package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
- package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
- package/src/lighting/tables/lightTables1171.generated.json +10 -0
- package/src/lighting/tables/lightTables1171.test.ts +168 -0
- package/src/lighting/tables/lightTables1171.ts +59 -0
- package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
- package/src/lighting/versioning/clientLightVersions.ts +203 -0
- package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
- package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
- package/src/mesher-shared/lightNibblePack.ts +26 -0
- package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
- package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
- package/src/mesher-shared/shared.ts +48 -1
- package/src/three/chunkMeshManager.ts +26 -0
- package/src/three/entities.ts +97 -8
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +154 -0
- package/src/three/entity/legWalkAnimation.ts +193 -0
- package/src/three/globalBlockBuffer.ts +94 -2
- package/src/three/globalLegacyBuffer.ts +94 -2
- package/src/three/graphicsBackendBase.ts +2 -9
- package/src/three/modules/clouds.ts +191 -0
- package/src/three/modules/index.ts +4 -0
- package/src/three/modules/sunMoon.ts +151 -0
- package/src/three/skyboxRenderer.ts +15 -8
- package/src/three/tests/clouds.test.ts +59 -0
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/tests/waterFog.test.ts +25 -0
- package/src/three/waterFog.ts +28 -0
- package/src/three/worldRendererThree.ts +84 -39
- package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
- package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
- package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
- package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
- package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
- package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
- package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
- package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
- package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
- package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
- package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
- package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
- package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
- package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
- package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
- package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
- package/src/worldView/types.ts +3 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +49 -1
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { bundlePrepareMesherWorkers, MESHER_DIST_FILES } from './bundlePrepare'
|
|
7
|
+
|
|
8
|
+
describe('MESHER_DIST_FILES', () => {
|
|
9
|
+
it('is the source of truth for lightOwnerWorker.js', () => {
|
|
10
|
+
expect(MESHER_DIST_FILES).toContain('lightOwnerWorker.js')
|
|
11
|
+
expect(MESHER_DIST_FILES).toContain('lightOwnerWorker.js.map')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('copies lightOwnerWorker.js from renderer dist', async () => {
|
|
15
|
+
const mesherDistDir = await mkdtemp(join(tmpdir(), 'renderer-dist-'))
|
|
16
|
+
const outDir = await mkdtemp(join(tmpdir(), 'app-dist-'))
|
|
17
|
+
for (const name of MESHER_DIST_FILES) {
|
|
18
|
+
await writeFile(join(mesherDistDir, name), `asset:${name}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const copied = await bundlePrepareMesherWorkers({ cwd: outDir, mesherDistDir, outDir })
|
|
22
|
+
expect(copied.some(path => path.endsWith('lightOwnerWorker.js'))).toBe(true)
|
|
23
|
+
expect(await readFile(join(outDir, 'lightOwnerWorker.js'), 'utf8')).toBe('asset:lightOwnerWorker.js')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -4,7 +4,7 @@ import { createRequire } from 'node:module'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
6
|
/** Worker-related basenames inside `minecraft-renderer`/`dist`; skipped if missing. */
|
|
7
|
-
export const MESHER_DIST_FILES = ['mesher.js', 'mesher.js.map', 'mesherWasm.js', 'mesherWasm.js.map'] as const
|
|
7
|
+
export const MESHER_DIST_FILES = ['mesher.js', 'mesher.js.map', 'mesherWasm.js', 'mesherWasm.js.map', 'lightOwnerWorker.js', 'lightOwnerWorker.js.map'] as const
|
|
8
8
|
|
|
9
9
|
export type BundlePrepareMesherOptions = {
|
|
10
10
|
cwd?: string
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
import { WorldView, WorldProvider, WorldViewWorker } from '../worldView'
|
|
26
26
|
import { getInitialPlayerState } from './playerState'
|
|
27
27
|
import { defaultWorldRendererConfig, defaultGraphicsBackendConfig, getDefaultRendererState, WorldRendererConfig } from './config'
|
|
28
|
+
import { maybeEnableClientLightTraceFromLocation } from '../lighting/debug/clientLightTrace'
|
|
28
29
|
import { PlayerStateReactive } from '../playerState/playerState'
|
|
29
30
|
import { ResourcesManager, ResourcesManagerTransferred } from '../resourcesManager'
|
|
30
31
|
import { preloadMesherWorkerScript } from './preloadWorkers'
|
|
@@ -102,6 +103,20 @@ export class AppViewer {
|
|
|
102
103
|
...defaultWorldRendererConfig,
|
|
103
104
|
...options.rendererConfig
|
|
104
105
|
})
|
|
106
|
+
// Manual live-test hatch: `?clientLight=1` sets `enableClientLightOwner`. Spawn still
|
|
107
|
+
// requires session version 1.17.1 (`shouldSpawnClientLightOwner`); other versions keep
|
|
108
|
+
// server light. An explicit code override always wins; without a browser location this
|
|
109
|
+
// is a no-op (unit tests keep testing the default).
|
|
110
|
+
if (options.rendererConfig?.enableClientLightOwner === undefined && typeof location !== 'undefined') {
|
|
111
|
+
try {
|
|
112
|
+
if (new URLSearchParams(location.search).get('clientLight') === '1') {
|
|
113
|
+
this.inWorldRenderingConfig.enableClientLightOwner = true
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// ignore malformed URL in non-browser runtimes
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
maybeEnableClientLightTraceFromLocation()
|
|
105
120
|
|
|
106
121
|
const defaultState = getDefaultRendererState()
|
|
107
122
|
this.rendererState = defaultState.reactive
|
|
@@ -43,6 +43,11 @@ export const defaultWorldRendererConfig = {
|
|
|
43
43
|
* remeshing (change worker). When true, initial chunk meshing is
|
|
44
44
|
* distributed only across workers[0 .. n-2]. */
|
|
45
45
|
dedicatedChangeWorker: false,
|
|
46
|
+
/**
|
|
47
|
+
* Dedicated WASM light-owner worker. Default OFF: wiring exists so a
|
|
48
|
+
* human can flip this for live 1.17.1 runs. Do not enable for players.
|
|
49
|
+
*/
|
|
50
|
+
enableClientLightOwner: false,
|
|
46
51
|
|
|
47
52
|
// Rendering engine settings
|
|
48
53
|
/** Face shading: vanilla Minecraft vs higher-contrast client look */
|
|
@@ -54,6 +59,9 @@ export const defaultWorldRendererConfig = {
|
|
|
54
59
|
enableLighting: true,
|
|
55
60
|
starfield: true,
|
|
56
61
|
defaultSkybox: true,
|
|
62
|
+
clouds: true,
|
|
63
|
+
/** World Y of the cloud layer (vanilla overworld: 192 since 1.18, 128 before). */
|
|
64
|
+
cloudHeight: 192,
|
|
57
65
|
renderEntities: true,
|
|
58
66
|
extraBlockRenderers: true,
|
|
59
67
|
foreground: true,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { migrateRendererOptions, RENDERER_DEFAULT_OPTIONS, resolveEnableLighting } from './rendererDefaultOptions'
|
|
4
|
+
|
|
5
|
+
describe('migrateRendererOptions', () => {
|
|
6
|
+
it('drops the leaked migration flag so it is not a user option', () => {
|
|
7
|
+
const saved: Record<string, unknown> = {
|
|
8
|
+
newVersionsLighting: false,
|
|
9
|
+
migratedNewVersionsLightingDefault: true
|
|
10
|
+
}
|
|
11
|
+
migrateRendererOptions(saved)
|
|
12
|
+
expect(saved.newVersionsLighting).toBe(false)
|
|
13
|
+
expect(saved).not.toHaveProperty('migratedNewVersionsLightingDefault')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('newer-version lighting defaults', () => {
|
|
18
|
+
it('keeps lighting opt-in on 1.13+', () => {
|
|
19
|
+
expect(RENDERER_DEFAULT_OPTIONS.newVersionsLighting).toBe(false)
|
|
20
|
+
expect(resolveEnableLighting(RENDERER_DEFAULT_OPTIONS.newVersionsLighting, true)).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it.each([false, true])('preserves a saved lighting preference (%s)', value => {
|
|
24
|
+
const saved: Record<string, unknown> = { newVersionsLighting: value }
|
|
25
|
+
migrateRendererOptions(saved)
|
|
26
|
+
expect(saved.newVersionsLighting).toBe(value)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not invent a stored value when the key was never saved', () => {
|
|
30
|
+
const saved: Record<string, unknown> = {}
|
|
31
|
+
migrateRendererOptions(saved)
|
|
32
|
+
expect(saved).not.toHaveProperty('newVersionsLighting')
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('resolveEnableLighting', () => {
|
|
37
|
+
it('follows the option on 1.13+ (blockStateId)', () => {
|
|
38
|
+
expect(resolveEnableLighting(true, true)).toBe(true)
|
|
39
|
+
expect(resolveEnableLighting(false, true)).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('always enables lighting on pre-1.13', () => {
|
|
43
|
+
expect(resolveEnableLighting(false, false)).toBe(true)
|
|
44
|
+
expect(resolveEnableLighting(true, false)).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('treats a missing protocol probe as pre-1.13 so a menu toggle cannot throw', () => {
|
|
48
|
+
expect(resolveEnableLighting(false, undefined)).toBe(true)
|
|
49
|
+
expect(resolveEnableLighting(true, undefined)).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -46,6 +46,7 @@ export const RENDERER_DEFAULT_OPTIONS = {
|
|
|
46
46
|
rendererWorldPerformance: 'normal' as 'low-energy' | 'normal' | 'maximum',
|
|
47
47
|
rendererMeshersCountOverride: null as number | null,
|
|
48
48
|
starfieldRendering: true as boolean,
|
|
49
|
+
cloudsRendering: true as boolean,
|
|
49
50
|
defaultSkybox: true as boolean,
|
|
50
51
|
menuBackgroundMode: MB.mode,
|
|
51
52
|
menuBackgroundMinecraftTextures: MB.minecraftTextures as boolean,
|
|
@@ -122,6 +123,17 @@ export function migrateRendererOptions(saved: Record<string, unknown>): void {
|
|
|
122
123
|
}
|
|
123
124
|
delete saved[oldKey]
|
|
124
125
|
}
|
|
126
|
+
|
|
127
|
+
// Internal one-shot flag from an earlier lighting-default migration. Never a user option.
|
|
128
|
+
delete saved.migratedNewVersionsLightingDefault
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Runtime `enableLighting` from the stored option and protocol.
|
|
133
|
+
* Pre-1.13 (no blockStateId) always lights. Missing protocol (menu, no bot) must not throw.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveEnableLighting(newVersionsLighting: boolean, blockStateIdSupported: boolean | undefined): boolean {
|
|
136
|
+
return blockStateIdSupported !== true || newVersionsLighting
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
/** Settings UI metadata for {@link RENDERER_DEFAULT_OPTIONS} keys. */
|
|
@@ -175,6 +187,9 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
|
|
|
175
187
|
starfieldRendering: {
|
|
176
188
|
text: 'Starfield'
|
|
177
189
|
},
|
|
190
|
+
cloudsRendering: {
|
|
191
|
+
text: 'Clouds'
|
|
192
|
+
},
|
|
178
193
|
defaultSkybox: {
|
|
179
194
|
text: 'Default skybox'
|
|
180
195
|
},
|
|
@@ -236,7 +251,8 @@ export const RENDERER_OPTIONS_META: Partial<Record<RendererDefaultOptionKey, Ren
|
|
|
236
251
|
},
|
|
237
252
|
smoothLighting: {},
|
|
238
253
|
newVersionsLighting: {
|
|
239
|
-
text: 'Lighting in newer versions'
|
|
254
|
+
text: 'Lighting in newer versions',
|
|
255
|
+
tooltip: 'Block, sky, and entity lighting for 1.13+. Off renders the world fullbright.'
|
|
240
256
|
},
|
|
241
257
|
vrSupport: {
|
|
242
258
|
text: 'VR support',
|
|
@@ -298,6 +314,7 @@ export const RENDERER_RENDER_GUI_SECTIONS: ReadonlyArray<{
|
|
|
298
314
|
keys: [
|
|
299
315
|
'rendererWorldPerformance',
|
|
300
316
|
'starfieldRendering',
|
|
317
|
+
'cloudsRendering',
|
|
301
318
|
'defaultSkybox',
|
|
302
319
|
'disableBlockEntityTextures',
|
|
303
320
|
'showChunkBorders',
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { subscribe } from 'valtio/vanilla'
|
|
8
8
|
import type { AppViewer } from './appViewer'
|
|
9
9
|
import type { RendererStorageOptions } from './rendererDefaultOptions'
|
|
10
|
-
import { rendererShaderCubeDebugModeToValue } from './rendererDefaultOptions'
|
|
10
|
+
import { rendererShaderCubeDebugModeToValue, resolveEnableLighting } from './rendererDefaultOptions'
|
|
11
11
|
import type { MenuBackgroundOptions } from '../three/menuBackground/types'
|
|
12
12
|
import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
|
|
13
13
|
import { menuBackgroundSpeedToMultiplier } from '../three/menuBackground/config'
|
|
@@ -153,6 +153,7 @@ export function applyRendererOptions(appViewer: AppViewer, o: RendererStorageOpt
|
|
|
153
153
|
cfg.smoothLighting = o.smoothLighting
|
|
154
154
|
cfg.shadingTheme = o.vanillaLook ? 'vanilla' : 'high-contrast'
|
|
155
155
|
cfg.starfield = o.starfieldRendering
|
|
156
|
+
cfg.clouds = o.cloudsRendering
|
|
156
157
|
cfg.defaultSkybox = o.defaultSkybox
|
|
157
158
|
cfg.fov = o.fov
|
|
158
159
|
cfg.shaderCubeDebugMode = rendererShaderCubeDebugModeToValue(o.rendererShaderCubeDebugMode)
|
|
@@ -216,6 +217,6 @@ export function subscribeRendererOptions<T extends RendererStorageOptions>(
|
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
/** Call when mineflayer bot is created (lighting depends on protocol features). */
|
|
219
|
-
export function applyRendererEnableLighting(appViewer: AppViewer, newVersionsLighting: boolean, blockStateIdSupported: boolean): void {
|
|
220
|
-
appViewer.inWorldRenderingConfig.enableLighting =
|
|
220
|
+
export function applyRendererEnableLighting(appViewer: AppViewer, newVersionsLighting: boolean, blockStateIdSupported: boolean | undefined): void {
|
|
221
|
+
appViewer.inWorldRenderingConfig.enableLighting = resolveEnableLighting(newVersionsLighting, blockStateIdSupported)
|
|
221
222
|
}
|
|
@@ -41,6 +41,10 @@ export class BlockEntityLightRegistry {
|
|
|
41
41
|
return this.skyLevel
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
getLightmapParams(): BlockLightmapParams {
|
|
45
|
+
return { ...this.lightmapParams }
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
private refreshAll(): void {
|
|
45
49
|
for (const entry of this.entries) {
|
|
46
50
|
this.applyBrightness(entry)
|
package/src/lib/skyLight.ts
CHANGED
|
@@ -29,6 +29,23 @@ export const getCelestialAngle = (timeOfDay: number): number => {
|
|
|
29
29
|
return smoothedAngle
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Vanilla `DimensionType.timeOfDay`: 0 = noon (sun overhead), 0.25 = sunset, 0.5 = midnight,
|
|
34
|
+
* ~0.78 at tick 0 (sunrise). Drives sun/moon placement and sky/cloud brightness.
|
|
35
|
+
*/
|
|
36
|
+
export const vanillaCelestialAngle = (timeOfDay: number): number => {
|
|
37
|
+
const t = timeOfDay / 24_000 - 0.25
|
|
38
|
+
const d = t - Math.floor(t)
|
|
39
|
+
const e = 0.5 - Math.cos(d * Math.PI) / 2
|
|
40
|
+
return (d * 2 + e) / 3
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Vanilla daylight factor (0 at night, 1 at day) used to tint clouds and celestial bodies. */
|
|
44
|
+
export const vanillaDayBrightness = (celestialAngle: number): number => {
|
|
45
|
+
const b = Math.cos(celestialAngle * Math.PI * 2) * 2 + 0.5
|
|
46
|
+
return Math.max(0, Math.min(1, b))
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* Calculate sky light level (0-15) based on time of day in ticks.
|
|
34
51
|
* Matches Minecraft vanilla behavior.
|