big worlds refactor with more scalable api, which allows hmr workers
fix: better free up ram after world unload & create meshers only when needed feat: add low ram setting, which makes chunks load performance ~4x worse
This commit is contained in:
parent
4fb2de503b
commit
0acaa652a3
18 changed files with 201 additions and 90 deletions
|
|
@ -19,6 +19,13 @@ You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.
|
|||
- Resource pack support
|
||||
- even even more!
|
||||
|
||||
### Recommended Settings
|
||||
|
||||
- Controls -> **Raw Input** -> **On** - This will make the controls more precise
|
||||
- Controls -> **Touch Controls Type** -> **Joystick**
|
||||
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
||||
- Interface -> **Chat Select** -> **On** - To select chat messages
|
||||
|
||||
### World Loading
|
||||
|
||||
Zip files and folders are supported. Just drag and drop them into the browser window. You can open folders in readonly and read-write mode. New chunks may be generated incorrectly for now.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { World } from './world'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { getSectionGeometry, setRendererData } from './models'
|
||||
import { getSectionGeometry, setBlockStatesData } from './models'
|
||||
|
||||
if (module.require) {
|
||||
// If we are in a node environement, we need to fake some env variables
|
||||
|
|
@ -39,7 +39,8 @@ function setSectionDirty (pos, value = true) {
|
|||
}
|
||||
|
||||
const softCleanup = () => {
|
||||
world.blockCache = {}
|
||||
// clean block cache and loaded chunks
|
||||
world = new World(world.config.version)
|
||||
}
|
||||
|
||||
self.onmessage = ({ data }) => {
|
||||
|
|
@ -47,16 +48,18 @@ self.onmessage = ({ data }) => {
|
|||
|
||||
if (data.type === 'mcData') {
|
||||
globalVar.mcData = data.mcData
|
||||
world = new World(data.version)
|
||||
} else if (data.type === 'rendererData') {
|
||||
setRendererData(data.json/* , data.textureSize */)
|
||||
world.outputFormat = data.outputFormat ?? world.outputFormat
|
||||
}
|
||||
|
||||
if (data.config) {
|
||||
world ??= new World(data.config.version)
|
||||
world.config = {...world.config, ...data.config}
|
||||
}
|
||||
|
||||
if (data.type === 'mesherData') {
|
||||
setBlockStatesData(data.json)
|
||||
blockStatesReady = true
|
||||
} else if (data.type === 'dirty') {
|
||||
const loc = new Vec3(data.x, data.y, data.z)
|
||||
world.skyLight = data.skyLight
|
||||
world.smoothLighting = data.smoothLighting
|
||||
world.enableLighting = data.enableLighting
|
||||
setSectionDirty(loc, data.value)
|
||||
} else if (data.type === 'chunk') {
|
||||
world.addColumn(data.x, data.z, data.chunk)
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ function renderElement (world: World, cursor: Vec3, element, doAO: boolean, attr
|
|||
const corner = world.getBlock(cursor.offset(...cornerDir))
|
||||
|
||||
let cornerLightResult = 15
|
||||
if (world.smoothLighting) {
|
||||
if (world.config.smoothLighting) {
|
||||
const side1Light = world.getLight(cursor.plus(new Vec3(...side1Dir)), true)
|
||||
const side2Light = world.getLight(cursor.plus(new Vec3(...side2Dir)), true)
|
||||
const cornerLight = world.getLight(cursor.plus(new Vec3(...cornerDir)), true)
|
||||
|
|
@ -605,7 +605,7 @@ function getModelVariants (block: import('prismarine-block').Block) {
|
|||
return []
|
||||
}
|
||||
|
||||
export const setRendererData = (_blockStates: BlockStatesOutput | null, _needTiles = false) => {
|
||||
export const setBlockStatesData = (_blockStates: BlockStatesOutput | null, _needTiles = false) => {
|
||||
blockStates = _blockStates!
|
||||
needTiles = _needTiles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { setRendererData, getSectionGeometry } from '../models'
|
||||
import { setBlockStatesData, getSectionGeometry } from '../models'
|
||||
import { World as MesherWorld } from '../world'
|
||||
import ChunkLoader from 'prismarine-chunk'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
|
@ -30,7 +30,7 @@ export const setup = (version, initialBlocks: [number[], string][]) => {
|
|||
}
|
||||
}
|
||||
|
||||
setRendererData(blockStates, true)
|
||||
setBlockStatesData(blockStates, true)
|
||||
mesherWorld.addColumn(0, 0, chunk1.toJson())
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import mcData from 'minecraft-data'
|
|||
import { Block } from "prismarine-block"
|
||||
import { Vec3 } from 'vec3'
|
||||
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
|
||||
import { defaultMesherConfig } from './shared'
|
||||
|
||||
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
||||
|
||||
|
|
@ -24,10 +25,7 @@ export type WorldBlock = Block & {
|
|||
|
||||
|
||||
export class World {
|
||||
enableLighting = true
|
||||
skyLight = 15
|
||||
smoothLighting = true
|
||||
outputFormat = 'threeJs' as 'threeJs' | 'webgl'
|
||||
config = defaultMesherConfig
|
||||
Chunk: typeof import('prismarine-chunk/types/index').PCChunk
|
||||
columns = {} as { [key: string]: import('prismarine-chunk/types/index').PCChunk }
|
||||
blockCache = {}
|
||||
|
|
@ -36,10 +34,12 @@ export class World {
|
|||
constructor(version) {
|
||||
this.Chunk = Chunks(version) as any
|
||||
this.biomeCache = mcData(version).biomes
|
||||
this.config.version = version
|
||||
}
|
||||
|
||||
getLight (pos: Vec3, isNeighbor = false) {
|
||||
if (!this.enableLighting) return 15
|
||||
const { enableLighting, skyLight } = this.config
|
||||
if (!enableLighting) return 15
|
||||
// const key = `${pos.x},${pos.y},${pos.z}`
|
||||
// if (lightsCache.has(key)) return lightsCache.get(key)
|
||||
const column = this.getColumnByPos(pos)
|
||||
|
|
@ -48,7 +48,7 @@ export class World {
|
|||
15,
|
||||
Math.max(
|
||||
column.getBlockLight(posInChunk(pos)),
|
||||
Math.min(this.skyLight, column.getSkyLight(posInChunk(pos)))
|
||||
Math.min(skyLight, column.getSkyLight(posInChunk(pos)))
|
||||
) + 2
|
||||
)
|
||||
// lightsCache.set(key, result)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { getVersion } from './version'
|
|||
import EventEmitter from 'events'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import { WorldRendererCommon } from './worldrendererCommon'
|
||||
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
|
|
@ -19,13 +19,13 @@ export class Viewer {
|
|||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
isSneaking = false
|
||||
version: string
|
||||
threeJsWorld: WorldRendererThree
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
|
||||
constructor(public renderer: THREE.WebGLRenderer, numWorkers?: number) {
|
||||
constructor(public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
|
@ -33,13 +33,18 @@ export class Viewer {
|
|||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.resetScene()
|
||||
this.world = new WorldRendererThree(this.scene, this.renderer, this.camera, numWorkers)
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, this.camera, worldConfig)
|
||||
this.setWorld()
|
||||
this.entities = new Entities(this.scene)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
}
|
||||
|
||||
setWorld () {
|
||||
this.world = this.threeJsWorld
|
||||
}
|
||||
|
||||
resetScene () {
|
||||
this.scene.background = new THREE.Color('lightblue')
|
||||
|
||||
|
|
@ -67,7 +72,6 @@ export class Viewer {
|
|||
setVersion (userVersion: string) {
|
||||
const texturesVersion = getVersion(userVersion)
|
||||
console.log('[viewer] Using version:', userVersion, 'textures:', texturesVersion)
|
||||
this.version = userVersion
|
||||
this.world.setVersion(userVersion, texturesVersion)
|
||||
this.entities.clear()
|
||||
// this.primitives.clear()
|
||||
|
|
@ -187,8 +191,8 @@ export class Viewer {
|
|||
skyLight = ((timeOfDay - 12000) / 6000) * 15
|
||||
}
|
||||
|
||||
if (this.world.skyLight === skyLight) return
|
||||
this.world.skyLight = skyLight
|
||||
if (this.world.mesherConfig.skyLight === skyLight) return
|
||||
this.world.mesherConfig.skyLight = skyLight
|
||||
; (this.world as WorldRendererThree).rerenderAllChunks?.()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -151,6 +151,13 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
unloadAllChunks () {
|
||||
for (const coords of Object.keys(this.loadedChunks)) {
|
||||
const [x, z] = coords.split(',').map(Number)
|
||||
this.unloadChunk({ x, z })
|
||||
}
|
||||
}
|
||||
|
||||
unloadChunk (pos: ChunkPos) {
|
||||
this.emitter.emit('unloadChunk', { x: pos.x, z: pos.z })
|
||||
delete this.loadedChunks[`${pos.x},${pos.z}`]
|
||||
|
|
@ -172,7 +179,6 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
chunksToUnload.push(p)
|
||||
}
|
||||
}
|
||||
// todo @sa2urami
|
||||
console.log('unloading', chunksToUnload.length, 'total now', Object.keys(this.loadedChunks).length)
|
||||
for (const p of chunksToUnload) {
|
||||
this.unloadChunk(p)
|
||||
|
|
|
|||
|
|
@ -7,21 +7,35 @@ import mcDataRaw from 'minecraft-data/data.js' // handled correctly in esbuild p
|
|||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajor } from './version.js'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { defaultMesherConfig } from './mesher/shared'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
export const worldCleanup = buildCleanupDecorator('resetWorld')
|
||||
|
||||
export const defaultWorldRendererConfig = {
|
||||
showChunkBorders: false,
|
||||
numWorkers: 4
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
||||
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
|
||||
worldConfig = { minY: 0, worldHeight: 256 }
|
||||
// todo @sa2urami set alphaTest back to 0.1 and instead properly sort transparent and solid objects (needs to be done in worker too)
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.5 })
|
||||
|
||||
showChunkBorders = false
|
||||
@worldCleanup()
|
||||
active = false
|
||||
version = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
loadedChunks = {} as Record<string, boolean>
|
||||
@worldCleanup()
|
||||
finishedChunks = {} as Record<string, boolean>
|
||||
@worldCleanup()
|
||||
sectionsOutstanding = new Map<string, number>()
|
||||
renderUpdateEmitter = new EventEmitter()
|
||||
customBlockStatesData = undefined as any
|
||||
|
|
@ -37,15 +51,21 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
texturesVersion?: string
|
||||
viewDistance = -1
|
||||
chunksLength = 0
|
||||
skyLight = 15
|
||||
smoothLighting = true
|
||||
enableLighting = true
|
||||
@worldCleanup()
|
||||
allChunksFinished = false
|
||||
handleResize = () => { }
|
||||
mesherConfig = defaultMesherConfig
|
||||
|
||||
abstract outputFormat: 'threeJs' | 'webgl'
|
||||
|
||||
constructor(numWorkers: number) {
|
||||
constructor (public config: WorldRendererConfig) {
|
||||
// this.initWorkers(1) // preload script on page load
|
||||
this.snapshotInitialValues()
|
||||
}
|
||||
|
||||
snapshotInitialValues() {}
|
||||
|
||||
initWorkers(numWorkers = this.config.numWorkers) {
|
||||
// init workers
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
// Node environment needs an absolute path, but browser needs the url of the file
|
||||
|
|
@ -127,32 +147,38 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
abstract updateShowChunksBorder (value: boolean): void
|
||||
|
||||
resetWorld () {
|
||||
this.active = false
|
||||
this.loadedChunks = {}
|
||||
this.sectionsOutstanding = new Map()
|
||||
this.finishedChunks = {}
|
||||
this.allChunksFinished = false
|
||||
// destroy workers
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'reset' })
|
||||
worker.terminate()
|
||||
}
|
||||
this.workers = []
|
||||
}
|
||||
|
||||
|
||||
// new game load happens here
|
||||
setVersion (version, texturesVersion = version) {
|
||||
this.version = version
|
||||
this.texturesVersion = texturesVersion
|
||||
this.resetWorld()
|
||||
this.initWorkers()
|
||||
this.active = true
|
||||
this.mesherConfig.outputFormat = this.outputFormat
|
||||
this.mesherConfig.version = this.version!
|
||||
|
||||
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)]
|
||||
for (const worker of this.workers) {
|
||||
const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key)))
|
||||
mcData.version = JSON.parse(JSON.stringify(mcData.version))
|
||||
worker.postMessage({ type: 'mcData', mcData, version: this.version })
|
||||
}
|
||||
|
||||
this.sendMesherMcData()
|
||||
this.updateTexturesData()
|
||||
}
|
||||
|
||||
sendMesherMcData () {
|
||||
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajor(this.version)]
|
||||
const mcData = Object.fromEntries(Object.entries(allMcData).filter(([key]) => dynamicMcDataFiles.includes(key)))
|
||||
mcData.version = JSON.parse(JSON.stringify(mcData.version))
|
||||
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'mcData', mcData, config: this.mesherConfig })
|
||||
}
|
||||
}
|
||||
|
||||
updateTexturesData () {
|
||||
loadTexture(this.customTexturesDataUrl || `textures/${this.texturesVersion}.png`, (texture: import('three').Texture) => {
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
|
|
@ -166,15 +192,20 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
if (this.customBlockStatesData) return resolve(this.customBlockStatesData)
|
||||
return loadJSON(`/blocksStates/${this.texturesVersion}.json`, (data) => {
|
||||
this.downloadedBlockStatesData = data
|
||||
// todo
|
||||
this.renderUpdateEmitter.emit('blockStatesDownloaded')
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
loadBlockStates().then((blockStates) => {
|
||||
this.mesherConfig.textureSize = this.material.map!.image.width
|
||||
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'rendererData', json: blockStates, textureSize: this.material.map!.image.width, outputFormat: this.outputFormat })
|
||||
worker.postMessage({
|
||||
type: 'mesherData',
|
||||
json: blockStates,
|
||||
config: this.mesherConfig,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -182,6 +213,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
|
||||
addColumn (x, z, chunk) {
|
||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
||||
this.initialChunksLoad = false
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
for (const worker of this.workers) {
|
||||
|
|
@ -224,7 +256,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||
this.allChunksFinished = false
|
||||
const distance = this.getDistance(pos)
|
||||
if (distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
|
||||
if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
|
||||
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
// if (this.sectionsOutstanding.has(key)) return
|
||||
this.renderUpdateEmitter.emit('dirty', pos, value)
|
||||
|
|
@ -233,7 +265,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
// is always dispatched to the same worker
|
||||
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length)
|
||||
this.sectionsOutstanding.set(key, (this.sectionsOutstanding.get(key) ?? 0) + 1)
|
||||
this.workers[hash].postMessage({ type: 'dirty', x: pos.x, y: pos.y, z: pos.z, value, skyLight: this.skyLight, smoothLighting: this.smoothLighting, enableLighting: this.enableLighting })
|
||||
this.workers[hash].postMessage({
|
||||
type: 'dirty',
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
z: pos.z,
|
||||
value,
|
||||
config: this.mesherConfig,
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
|
||||
|
|
|
|||
|
|
@ -5,19 +5,14 @@ import { dispose3 } from './dispose'
|
|||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { renderSign } from '../sign-renderer/'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
import { WorldRendererCommon } from './worldrendererCommon'
|
||||
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
outputFormat = 'threeJs' as const
|
||||
blockEntities = {}
|
||||
sectionObjects: Record<string, THREE.Object3D> = {}
|
||||
showChunkBorders = false
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
signsCache = new Map<string, any>()
|
||||
|
||||
|
|
@ -25,8 +20,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
||||
}
|
||||
|
||||
constructor(public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public camera: THREE.PerspectiveCamera, numWorkers = 4) {
|
||||
super(numWorkers)
|
||||
constructor(public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public camera: THREE.PerspectiveCamera, public config: WorldRendererConfig) {
|
||||
super(config)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,7 +89,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
object.name = 'chunk'
|
||||
//@ts-ignore
|
||||
object.tilesCount = data.geometry.positions.length / 3 / 4
|
||||
if (!this.showChunkBorders) {
|
||||
if (!this.config.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
}
|
||||
// should not compute it once
|
||||
|
|
@ -198,7 +193,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
this.showChunkBorders = value
|
||||
this.config.showChunkBorders = value
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ if (isFirefox) {
|
|||
}
|
||||
|
||||
// Create viewer
|
||||
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer, options.numWorkers)
|
||||
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer)
|
||||
window.viewer = viewer
|
||||
new THREE.TextureLoader().load(itemsPng, (texture) => {
|
||||
viewer.entities.itemsTexture = texture
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ export const guiOptionsScheme: {
|
|||
smoothLighting: {},
|
||||
newVersionsLighting: {
|
||||
text: 'Lighting in newer versions',
|
||||
}
|
||||
},
|
||||
lowMemoryMode: {
|
||||
text: 'Low Memory Mode',
|
||||
enableWarning: 'Enabling it will make chunks load ~4x slower'
|
||||
},
|
||||
},
|
||||
],
|
||||
main: [
|
||||
|
|
@ -175,6 +179,9 @@ export const guiOptionsScheme: {
|
|||
],
|
||||
controls: [
|
||||
{
|
||||
custom () {
|
||||
return <Category>Keyboard & Mouse</Category>
|
||||
},
|
||||
// keybindings
|
||||
mouseSensX: {},
|
||||
mouseSensY: {
|
||||
|
|
@ -188,9 +195,6 @@ export const guiOptionsScheme: {
|
|||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
disabledReason: Boolean(document.documentElement.requestPointerLock) ? undefined : 'Your browser does not support pointer lock.',
|
||||
},
|
||||
alwaysShowMobileControls: {
|
||||
text: 'Always Mobile Controls',
|
||||
},
|
||||
autoFullScreen: {
|
||||
tooltip: 'Auto Fullscreen allows you to use Ctrl+W and Escape having to wait/click on screen again.',
|
||||
disabledReason: navigator['keyboard'] ? undefined : 'Your browser doesn\'t support keyboard lock API'
|
||||
|
|
@ -198,6 +202,14 @@ export const guiOptionsScheme: {
|
|||
autoExitFullscreen: {
|
||||
tooltip: 'Exit fullscreen on escape (pause menu open). But note you can always do it with F11.',
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>Touch Controls</Category>
|
||||
},
|
||||
alwaysShowMobileControls: {
|
||||
text: 'Always Mobile Controls',
|
||||
},
|
||||
touchButtonsSize: {
|
||||
min: 40
|
||||
},
|
||||
|
|
@ -211,13 +223,6 @@ export const guiOptionsScheme: {
|
|||
touchControlsType: {
|
||||
values: [['classic', 'Classic'], ['joystick-buttons', 'New']],
|
||||
},
|
||||
autoJump: {
|
||||
values: [
|
||||
'always',
|
||||
'auto',
|
||||
'never'
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
|
|
@ -226,6 +231,16 @@ export const guiOptionsScheme: {
|
|||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>Auto Jump</Category>
|
||||
},
|
||||
autoJump: {
|
||||
values: [
|
||||
'always',
|
||||
'auto',
|
||||
'never'
|
||||
],
|
||||
},
|
||||
autoParkour: {},
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const defaultOptions = {
|
|||
unimplementedContainers: false,
|
||||
dayCycleAndLighting: true,
|
||||
loadPlayerSkins: true,
|
||||
lowMemoryMode: false,
|
||||
// antiAliasing: false,
|
||||
|
||||
showChunkBorders: false, // todo rename option
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ import { options, qsOptions } from '../optionsStorage'
|
|||
import Button from './Button'
|
||||
import Slider from './Slider'
|
||||
import Screen from './Screen'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
|
||||
type GeneralItem<T extends string | number | boolean> = {
|
||||
id?: string
|
||||
text?: string,
|
||||
disabledReason?: string,
|
||||
tooltip?: string
|
||||
// description?: string
|
||||
enableWarning?: string
|
||||
willHaveNoEffect?: boolean
|
||||
values?: Array<T | [T, string]>
|
||||
}
|
||||
|
|
@ -56,7 +59,11 @@ export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'togg
|
|||
|
||||
return <Button
|
||||
label={`${item.text}: ${valuesTitlesMap[optionValue]}`}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
if (item.enableWarning && !options[item.id!]) {
|
||||
const result = await showOptionsModal(item.enableWarning, ['Enable'])
|
||||
if (!result) return
|
||||
}
|
||||
const { values } = item
|
||||
if (values) {
|
||||
const getOptionValue = (arrItem) => {
|
||||
|
|
@ -108,9 +115,17 @@ const RenderOption = ({ item }: { item: OptionMeta }) => {
|
|||
item.text ??= titleCase(noCase(item.id))
|
||||
}
|
||||
|
||||
if (item.type === 'toggle') return <OptionButton item={item} />
|
||||
if (item.type === 'slider') return <OptionSlider item={item} />
|
||||
if (item.type === 'element') return <OptionElement item={item} />
|
||||
let baseElement = null as React.ReactNode | null
|
||||
if (item.type === 'toggle') baseElement = <OptionButton item={item} />
|
||||
if (item.type === 'slider') baseElement = <OptionSlider item={item} />
|
||||
if (item.type === 'element') baseElement = <OptionElement item={item} />
|
||||
return baseElement
|
||||
// if (!item.description && item.type === 'element') return baseElement
|
||||
|
||||
// return <div>
|
||||
// {baseElement}
|
||||
// {item.description && <div style={{ fontSize: 9, color: 'gray' }}>{item.description}</div>}
|
||||
// </div>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@ import { options } from './optionsStorage'
|
|||
import { loadOrPlaySound } from './basicSounds'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
|
||||
const globalObject = window as {
|
||||
allSoundsMap?: Record<string, Record<string, string>>,
|
||||
allSoundsVersionedMap?: Record<string, string[]>,
|
||||
}
|
||||
|
||||
subscribeKey(miscUiState, 'gameLoaded', async () => {
|
||||
if (!miscUiState.gameLoaded) return
|
||||
const soundsLegacyMap = window.allSoundsVersionedMap as Record<string, string[]>
|
||||
const allSoundsMap = window.allSoundsMap as Record<string, Record<string, string>>
|
||||
const { allSoundsMap } = globalObject
|
||||
const allSoundsMeta = window.allSoundsMeta as { format: string, baseUrl: string }
|
||||
if (!allSoundsMap) {
|
||||
return
|
||||
|
|
@ -115,7 +120,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
|
||||
const getStepSound = (blockUnder: Block) => {
|
||||
// const soundsMap = window.allSoundsMap?.[bot.version]
|
||||
// const soundsMap = globalObject.allSoundsMap?.[bot.version]
|
||||
// if (!soundsMap) return
|
||||
// let soundResult = 'block.stone.step'
|
||||
// for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) {
|
||||
|
|
@ -215,8 +220,21 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
// })
|
||||
})
|
||||
|
||||
// todo
|
||||
// const music = {
|
||||
// activated: false,
|
||||
// playing: '',
|
||||
// activate () {
|
||||
// const gameMusic = Object.entries(globalObject.allSoundsMap?.[bot.version] ?? {}).find(([id, sound]) => sound.includes('music.game'))
|
||||
// if (!gameMusic) return
|
||||
// const soundPath = gameMusic[0].split(';')[1]
|
||||
// const next = () => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
export const earlyCheck = () => {
|
||||
const allSoundsMap = window.allSoundsMap as Record<string, Record<string, string>>
|
||||
const { allSoundsMap } = globalObject
|
||||
if (!allSoundsMap) return
|
||||
|
||||
// todo also use major versioned hardcoded sounds
|
||||
const soundsMap = allSoundsMap[bot.version]
|
||||
|
|
@ -239,7 +257,7 @@ const getVersionedSound = (version: string, item: string, itemsMapSortedEntries:
|
|||
}
|
||||
|
||||
export const downloadSoundsIfNeeded = async () => {
|
||||
if (!window.allSoundsMap) {
|
||||
if (!globalObject.allSoundsMap) {
|
||||
try {
|
||||
await loadScript('./sounds.js')
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export const completeTexturePackInstall = async (name?: string) => {
|
|||
await fs.promises.writeFile(join(texturePackBasePath, 'name.txt'), name ?? '??', 'utf8')
|
||||
|
||||
if (viewer?.world.active) {
|
||||
await genTexturePackTextures(viewer.version)
|
||||
await genTexturePackTextures(viewer.world.version!)
|
||||
}
|
||||
setLoadingScreenStatus(undefined)
|
||||
showNotification('Texturepack installed')
|
||||
|
|
|
|||
|
|
@ -30,24 +30,30 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
|
||||
watchValue(options, o => {
|
||||
if (!viewer) return
|
||||
viewer.world.showChunkBorders = o.showChunkBorders
|
||||
viewer.world.config.showChunkBorders = o.showChunkBorders
|
||||
viewer.entities.setDebugMode(o.showChunkBorders ? 'basic' : 'none')
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
if (!viewer) return
|
||||
// todo ideally there shouldnt be this setting and we don't need to send all same chunks to all workers
|
||||
viewer.world.config.numWorkers = o.lowMemoryMode ? 1 : o.numWorkers
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.entities.setVisible(o.renderEntities)
|
||||
})
|
||||
|
||||
viewer.world.smoothLighting = options.smoothLighting
|
||||
viewer.world.mesherConfig.smoothLighting = options.smoothLighting
|
||||
subscribeKey(options, 'smoothLighting', () => {
|
||||
viewer.world.smoothLighting = options.smoothLighting;
|
||||
viewer.world.mesherConfig.smoothLighting = options.smoothLighting;
|
||||
(viewer.world as WorldRendererThree).rerenderAllChunks()
|
||||
})
|
||||
subscribeKey(options, 'newVersionsLighting', () => {
|
||||
viewer.world.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting;
|
||||
viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting;
|
||||
(viewer.world as WorldRendererThree).rerenderAllChunks()
|
||||
})
|
||||
customEvents.on('gameLoaded', () => {
|
||||
viewer.world.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
// this the only options that allows smooth transition from js to ts (by not dropping types from js files)
|
||||
// however might need to consider includeing *only needed libraries* instead of using this
|
||||
"maxNodeModuleJsDepth": 1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue