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:
Vitaly 2024-04-20 13:16:36 +03:00
commit 0acaa652a3
18 changed files with 201 additions and 90 deletions

View file

@ -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.

View file

@ -1,9 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"strictNullChecks": true
},
"files": [
"index.d.ts"
]
"compilerOptions": {
"module": "commonjs",
"strictNullChecks": true,
"experimentalDecorators": true
},
"files": [
"index.d.ts"
]
}

View file

@ -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)

View file

@ -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
}

View file

@ -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 {

View file

@ -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)

View file

@ -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?.()
})

View file

@ -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)

View file

@ -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

View file

@ -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') {

View file

@ -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

View file

@ -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: {},
}
],

View file

@ -57,6 +57,7 @@ const defaultOptions = {
unimplementedContainers: false,
dayCycleAndLighting: true,
loadPlayerSkins: true,
lowMemoryMode: false,
// antiAliasing: false,
showChunkBorders: false, // todo rename option

View file

@ -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 {

View file

@ -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) {

View file

@ -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')

View file

@ -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
})
}

View file

@ -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,