pick most of changes from webgpu for better stability (#322)

This commit is contained in:
Vitaly 2025-04-06 00:22:27 +03:00 committed by GitHub
commit 908fa64f2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 304 additions and 164 deletions

View file

@ -150,7 +150,7 @@
"http-browserify": "^1.7.0",
"http-server": "^14.1.1",
"https-browserify": "^1.0.0",
"mc-assets": "^0.2.50",
"mc-assets": "^0.2.52",
"mineflayer-mouse": "^0.1.7",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer",

22
pnpm-lock.yaml generated
View file

@ -353,8 +353,8 @@ importers:
specifier: ^1.0.0
version: 1.0.0
mc-assets:
specifier: ^0.2.50
version: 0.2.50
specifier: ^0.2.52
version: 0.2.52
minecraft-inventory-gui:
specifier: github:zardoy/minecraft-inventory-gui#next
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/f57dd78ca8e3b7cdd724d4272d8cbf6743b0cf00(@types/react@18.2.20)(react@18.2.0)
@ -3546,6 +3546,9 @@ packages:
engines: {node: '>=8'}
hasBin: true
'@zardoy/maxrects-packer@2.7.4':
resolution: {integrity: sha512-ZIDcSdtSg6EhKFxGYWCcTnA/0YVbpixBL+psUS6ncw4IvdDF5hWauMU3XeCfYwrT/88QFgAq/Pafxt+P9OJyoQ==}
'@zardoy/react-util@0.2.4':
resolution: {integrity: sha512-YRBbXi54QOgWGDSn3NLEMMGrWbfL/gn2khxO31HT0WPFB6IW2rSnB4hcy+S/nc+2D6PRNq4kQxGs4vTAe4a7Xg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -6691,11 +6694,8 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
maxrects-packer@2.7.3:
resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==}
mc-assets@0.2.50:
resolution: {integrity: sha512-X27KLLTyeEAVTlBVKqGmoG/YvZq3tDG29kyRgy3Hj9s03m6+/TI8HvCyxumRjEQE8IYPcjPiX+7iuEZtNQ9N+w==}
mc-assets@0.2.52:
resolution: {integrity: sha512-6mUI63fcUIjB0Ghjls7bLMnse2XUgvhPajsFkRQf10PcXYbfS/OAnX51X8sNx2pzfoHSlA81U7v+v906YwoAUw==}
engines: {node: '>=18.0.0'}
mcraft-fun-mineflayer@0.1.14:
@ -13688,6 +13688,8 @@ snapshots:
- encoding
- supports-color
'@zardoy/maxrects-packer@2.7.4': {}
'@zardoy/react-util@0.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
classnames: 2.5.1
@ -17603,11 +17605,9 @@ snapshots:
math-intrinsics@1.1.0: {}
maxrects-packer@2.7.3: {}
mc-assets@0.2.50:
mc-assets@0.2.52:
dependencies:
maxrects-packer: 2.7.3
maxrects-packer: '@zardoy/maxrects-packer@2.7.4'
zod: 3.24.1
mcraft-fun-mineflayer@0.1.14(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d(encoding@0.1.13)):

View file

@ -2,7 +2,7 @@ import { EventEmitter } from 'events'
import { Vec3 } from 'vec3'
import TypedEmitter from 'typed-emitter'
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
import { proxy } from 'valtio'
import { proxy, ref } from 'valtio'
import { GameMode } from 'mineflayer'
import { HandItemBlock } from '../three/holdingBlock'
@ -34,6 +34,7 @@ export interface IPlayerState {
reactive: {
playerSkin: string | undefined
inWater: boolean
waterBreathing: boolean
backgroundColor: [number, number, number]
ambientLight: number
directionalLight: number
@ -45,7 +46,8 @@ export class BasePlayerState implements IPlayerState {
reactive = proxy({
playerSkin: undefined as string | undefined,
inWater: false,
backgroundColor: [0, 0, 0] as [number, number, number],
waterBreathing: false,
backgroundColor: ref([0, 0, 0]) as [number, number, number],
ambientLight: 0,
directionalLight: 0,
})

View file

@ -41,9 +41,10 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
private readonly lastPos: Vec3
private eventListeners: Record<string, any> = {}
private readonly emitter: WorldDataEmitter
keepChunksDistance = 0
addWaitTime = 1
isPlayground = false
/* config */ keepChunksDistance = 0
/* config */ isPlayground = false
/* config */ allowPositionUpdate = true
public reactive = proxy({
cursorBlock: null as Vec3 | null,
@ -165,6 +166,8 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
console.error('error processing entity', err)
}
}
void this.init(bot.entity.position)
}
removeListenersFromBot (bot: import('mineflayer').Bot) {
@ -253,6 +256,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
}
async updatePosition (pos: Vec3, force = false) {
if (!this.allowPositionUpdate) return
const [lastX, lastZ] = chunkPos(this.lastPos)
const [botX, botZ] = chunkPos(pos)
if (lastX !== botX || lastZ !== botZ || force) {

View file

@ -7,6 +7,7 @@ import TypedEmitter from 'typed-emitter'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { subscribeKey } from 'valtio/utils'
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajorVersion } from '../../../src/utils'
import { ResourcesManager } from '../../../src/resourcesManager'
@ -15,7 +16,7 @@ import { SoundSystem } from '../three/threeJsSound'
import { buildCleanupDecorator } from './cleanupDecorator'
import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { removeStat, updateStatText } from './ui/newStats'
import { removeAllStats, removeStat, updateStatText } from './ui/newStats'
import { WorldDataEmitter } from './worldDataEmitter'
import { IPlayerState } from './basePlayerState'
@ -105,7 +106,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
workersProcessAverageTime = 0
workersProcessAverageTimeCount = 0
maxWorkersProcessTime = 0
geometryReceiveCount = {}
geometryReceiveCount = {} as Record<number, number>
allLoadedIn: undefined | number
onWorldSwitched = [] as Array<() => void>
@ -137,6 +138,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
abortController = new AbortController()
lastRendered = 0
renderingActive = true
geometryReceiveCountPerSec = 0
workerLogger = {
contents: [] as string[],
active: new URL(location.href).searchParams.get('mesherlog') === 'true'
@ -155,6 +157,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
})
this.connect(this.displayOptions.worldView)
setInterval(() => {
this.geometryReceiveCountPerSec = Object.values(this.geometryReceiveCount).reduce((acc, curr) => acc + curr, 0)
this.geometryReceiveCount = {}
this.updateChunksStats()
}, 1000)
}
logWorkerWork (message: string | (() => string)) {
@ -164,6 +172,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
init () {
if (this.active) throw new Error('WorldRendererCommon is already initialized')
this.watchReactivePlayerState()
void this.setVersion(this.version).then(() => {
this.resourcesManager.on('assetsTexturesUpdated', () => {
if (!this.active) return
@ -238,6 +247,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
}
onReactiveValueUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) {
callback(this.displayOptions.playerState.reactive[key])
subscribeKey(this.displayOptions.playerState.reactive, key, callback)
}
watchReactivePlayerState () {
this.onReactiveValueUpdated('backgroundColor', (value) => {
this.changeBackgroundColor(value)
})
}
async processMessageQueue (source: string) {
if (this.isProcessingQueue || this.messageQueue.length === 0) return
this.logWorkerWork(`# ${source} processing queue`)
@ -503,7 +523,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.displayOptions.nonReactiveState.world.chunksTotalNumber = this.chunksLength
this.reactiveState.world.allChunksLoaded = this.allChunksFinished
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`)
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.geometryReceiveCountPerSec}ss/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`)
}
addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {
@ -884,7 +904,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.renderUpdateEmitter.removeAllListeners()
this.displayOptions.worldView.removeAllListeners() // todo
this.abortController.abort()
removeStat('chunks-loaded')
removeStat('chunks-read')
removeAllStats()
}
}

View file

@ -117,4 +117,5 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO
return backend
}
createGraphicsBackend.id = 'threejs'
export default createGraphicsBackend

View file

@ -911,6 +911,6 @@ export const getBlockMeshFromModel = (material: THREE.Material, model: BlockMode
const worldRenderModel = blockProvider.transformModel(model, {
name,
properties: {}
})
}) as any
return getThreeBlockModelGroup(material, [[worldRenderModel]], undefined, 'plains', loadedData)
}

View file

@ -70,7 +70,6 @@ export class WorldRendererThree extends WorldRendererCommon {
this.addDebugOverlay()
this.resetScene()
this.watchReactivePlayerState()
this.init()
void initVR(this)
@ -120,22 +119,15 @@ export class WorldRendererThree extends WorldRendererCommon {
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
}
watchReactivePlayerState () {
const updateValue = <T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) => {
callback(this.displayOptions.playerState.reactive[key])
subscribeKey(this.displayOptions.playerState.reactive, key, callback)
}
updateValue('backgroundColor', (value) => {
this.changeBackgroundColor(value)
override watchReactivePlayerState () {
this.onReactiveValueUpdated('inWater', (value) => {
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null
})
updateValue('inWater', (value) => {
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, 100) : null
})
updateValue('ambientLight', (value) => {
this.onReactiveValueUpdated('ambientLight', (value) => {
if (!value) return
this.ambientLight.intensity = value
})
updateValue('directionalLight', (value) => {
this.onReactiveValueUpdated('directionalLight', (value) => {
if (!value) return
this.directionalLight.intensity = value
})
@ -613,8 +605,6 @@ export class WorldRendererThree extends WorldRendererCommon {
}
destroy (): void {
removeAllStats()
this.media.onWorldGone()
super.destroy()
}
}

View file

@ -49,11 +49,13 @@ const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
sceneBackground: 'lightblue'
}
export interface GraphicsInitOptions {
export interface GraphicsInitOptions<S = any> {
resourcesManager: ResourcesManager
config: GraphicsBackendConfig
rendererSpecificSettings: S
displayCriticalError: (error: Error) => void
setRendererSpecificSettings: (key: string, value: any) => void
}
export interface DisplayWorldOptions {
@ -65,7 +67,9 @@ export interface DisplayWorldOptions {
nonReactiveState: NonReactiveState
}
export type GraphicsBackendLoader = (options: GraphicsInitOptions) => GraphicsBackend
export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => GraphicsBackend) & {
id: string
}
// no sync methods
export interface GraphicsBackend {
@ -73,7 +77,7 @@ export interface GraphicsBackend {
displayName?: string
startPanorama: () => void
// prepareResources: (version: string, progressReporter: ProgressReporter) => Promise<void>
startWorld: (options: DisplayWorldOptions) => void
startWorld: (options: DisplayWorldOptions) => Promise<void> | void
disconnect: () => void
setRendering: (rendering: boolean) => void
getDebugOverlay?: () => Record<string, any>
@ -116,6 +120,13 @@ export class AppViewer {
}
this.backendLoader = loader
const rendererSpecificSettings = {} as Record<string, any>
const rendererSettingsKey = `renderer.${this.backendLoader?.id}`
for (const key in options) {
if (key.startsWith(rendererSettingsKey)) {
rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key]
}
}
const loaderOptions: GraphicsInitOptions = {
resourcesManager: this.resourcesManager,
config: this.config,
@ -123,6 +134,10 @@ export class AppViewer {
console.error(error)
setLoadingScreenStatus(error.message, true)
},
rendererSpecificSettings,
setRendererSpecificSettings (key: string, value: any) {
options[`${rendererSettingsKey}.${key}`] = value
}
}
this.backend = loader(loaderOptions)
@ -135,12 +150,12 @@ export class AppViewer {
const { method, args } = this.currentState
this.backend[method](...args)
if (method === 'startWorld') {
void this.worldView!.init(args[0].playerState.getPosition())
// void this.worldView!.init(args[0].playerState.getPosition())
}
}
}
startWorld (world, renderDistance: number, playerStateSend: IPlayerState = this.playerState) {
async startWorld (world, renderDistance: number, playerStateSend: IPlayerState = this.playerState) {
if (this.currentDisplay === 'world') throw new Error('World already started')
this.currentDisplay = 'world'
const startPosition = playerStateSend.getPosition()
@ -156,14 +171,17 @@ export class AppViewer {
rendererState: this.rendererState,
nonReactiveState: this.nonReactiveState
}
let promise: undefined | Promise<void>
if (this.backend) {
this.backend.startWorld(displayWorldOptions)
void this.worldView.init(startPosition)
promise = this.backend.startWorld(displayWorldOptions) ?? undefined
// void this.worldView.init(startPosition)
}
this.currentState = { method: 'startWorld', args: [displayWorldOptions] }
await promise
// Resolve the promise after world is started
this.resolveWorldReady()
return !!promise
}
resetBackend (cleanState = false) {
@ -237,13 +255,15 @@ const initialMenuStart = async () => {
}
appViewer.startPanorama()
// await appViewer.resourcesManager.loadMcData('1.21.4')
// const world = getSyncWorld('1.21.4')
// world.setBlockStateId(new Vec3(0, 64, 0), 1)
// appViewer.resourcesManager.currentConfig = { version: '1.21.4' }
// const version = '1.18.2'
// const version = '1.21.4'
// await appViewer.resourcesManager.loadMcData(version)
// const world = getSyncWorld(version)
// world.setBlockStateId(new Vec3(0, 64, 0), loadedData.blocksByName.water.defaultState)
// appViewer.resourcesManager.currentConfig = { version }
// await appViewer.resourcesManager.updateAssetsData({})
// appViewer.playerState = new BasePlayerState() as any
// appViewer.startWorld(world, 3)
// await appViewer.startWorld(world, 3)
// appViewer.backend?.updateCamera(new Vec3(0, 64, 2), 0, 0)
// void appViewer.worldView!.init(new Vec3(0, 64, 0))
}

34
src/appViewerLoad.ts Normal file
View file

@ -0,0 +1,34 @@
import { subscribeKey } from 'valtio/utils'
import createGraphicsBackend from 'renderer/viewer/three/graphicsBackend'
import { options } from './optionsStorage'
import { appViewer } from './appViewer'
import { miscUiState } from './globalState'
import { watchOptionsAfterViewerInit } from './watchOptions'
const loadBackend = () => {
if (options.activeRenderer === 'webgpu') {
// appViewer.loadBackend(createWebgpuBackend)
} else {
appViewer.loadBackend(createGraphicsBackend)
}
}
window.loadBackend = loadBackend
if (process.env.SINGLE_FILE_BUILD_MODE) {
const unsub = subscribeKey(miscUiState, 'fsReady', () => {
if (miscUiState.fsReady) {
// don't do it earlier to load fs and display menu faster
loadBackend()
unsub()
}
})
} else {
loadBackend()
}
const animLoop = () => {
for (const fn of beforeRenderFrame) fn()
requestAnimationFrame(animLoop)
}
requestAnimationFrame(animLoop)
watchOptionsAfterViewerInit()

View file

@ -2,12 +2,13 @@ import fs from 'fs'
import { join } from 'path'
import JSZip from 'jszip'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
import { readLevelDat } from './loadSave'
import { fsState, readLevelDat } from './loadSave'
import { closeWan, openToWanAndCopyJoinLink } from './localServerMultiplayer'
import { copyFilesAsync, uniqueFileNameFromWorldName } from './browserfs'
import { saveServer } from './flyingSquidUtils'
import { setLoadingScreenStatus } from './appStatus'
import { displayClientChat } from './botUtils'
import { miscUiState } from './globalState'
const notImplemented = () => {
return 'Not implemented yet'
@ -69,7 +70,7 @@ export const exportWorld = async (path: string, type: 'zip' | 'folder', zipName
// todo include in help
const exportLoadedWorld = async () => {
await saveServer()
let { worldFolder } = localServer!.options
let worldFolder = fsState.inMemorySavePath
if (!worldFolder.startsWith('/')) worldFolder = `/${worldFolder}`
await exportWorld(worldFolder, 'zip')
}
@ -141,12 +142,12 @@ export const commands: Array<{
]
//@ts-format-ignore-endregion
export const getBuiltinCommandsList = () => commands.filter(command => command.alwaysAvailable || localServer).flatMap(command => command.command)
export const getBuiltinCommandsList = () => commands.filter(command => command.alwaysAvailable || miscUiState.singleplayer).flatMap(command => command.command)
export const tryHandleBuiltinCommand = (message: string) => {
const [userCommand, ...args] = message.split(' ')
for (const command of commands.filter(command => command.alwaysAvailable || localServer)) {
for (const command of commands.filter(command => command.alwaysAvailable || miscUiState.singleplayer)) {
if (command.command.includes(userCommand)) {
void command.invoke(args) // ignoring for now
return true

View file

@ -417,8 +417,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
export function lockUrl () {
let newQs = ''
if (fsState.saveLoaded) {
const save = localServer!.options.worldFolder.split('/').at(-1)
if (fsState.saveLoaded && fsState.inMemorySave) {
const worldFolder = fsState.inMemorySavePath
const save = worldFolder.split('/').at(-1)
newQs = `loadSave=${save}`
} else if (process.env.NODE_ENV === 'development') {
newQs = `reconnect=1`
@ -579,7 +580,7 @@ contro.on('release', ({ command }) => {
export const f3Keybinds: Array<{
key?: string,
action: () => void,
action: () => void | Promise<void>,
mobileTitle: string
enabled?: () => boolean
}> = [
@ -694,7 +695,7 @@ document.addEventListener('keydown', (e) => {
if (hardcodedPressedKeys.has('F3')) {
const keybind = f3Keybinds.find((v) => v.key === e.code)
if (keybind && (keybind.enabled?.() ?? true)) {
keybind.action()
void keybind.action()
e.stopPropagation()
}
return
@ -933,13 +934,17 @@ window.addEventListener('keydown', (e) => {
if (e.code === 'KeyL' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
console.clear()
}
if (e.code === 'KeyK' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
if (e.code === 'KeyK' && e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey) {
if (sessionStorage.delayLoadUntilFocus) {
sessionStorage.removeItem('delayLoadUntilFocus')
} else {
sessionStorage.setItem('delayLoadUntilFocus', 'true')
}
}
if (e.code === 'KeyK' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
// eslint-disable-next-line no-debugger
debugger
}
})
// #endregion

View file

@ -124,12 +124,12 @@ export const preventThrottlingWithSound = () => {
// Start playing
oscillator.start()
return () => {
return async () => {
try {
oscillator.stop()
audioContext.close()
await audioContext.close()
} catch (err) {
console.error('Error stopping silent audio:', err)
console.warn('Error stopping silent audio:', err)
}
}
} catch (err) {

View file

@ -21,7 +21,8 @@ window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/
Object.defineProperty(window, 'debugSceneChunks', {
get () {
return (window.world as WorldRendererThree)?.getLoadedChunksRelative?.(bot.entity.position, true)
if (!(window.world instanceof WorldRendererThree)) return undefined
return (window.world)?.getLoadedChunksRelative?.(bot.entity.position, true)
},
})
@ -149,7 +150,7 @@ Object.defineProperty(window, 'debugToggle', {
})
customEvents.on('gameLoaded', () => {
window.holdingBlock = (window.world as WorldRendererThree).holdingBlock
window.holdingBlock = (window.world as WorldRendererThree | undefined)?.holdingBlock
})
window.clearStorage = (...keysToKeep: string[]) => {

13
src/globals.d.ts vendored
View file

@ -34,4 +34,17 @@ declare interface Document {
exitPointerLock?(): void
}
declare module '*.frag' {
const png: string
export default png
}
declare module '*.vert' {
const png: string
export default png
}
declare module '*.wgsl' {
const png: string
export default png
}
declare interface Window extends Record<string, any> { }

View file

@ -8,3 +8,4 @@ window.worldView = undefined
window.viewer = undefined
window.loadedData = undefined
window.customEvents = new EventEmitter()
window.customEvents.setMaxListeners(10_000)

View file

@ -4,7 +4,6 @@ import React from 'react'
import { loadScript } from 'renderer/viewer/lib/utils'
import { loadGoogleDriveApi, loadInMemorySave } from './react/SingleplayerProvider'
import { setLoadingScreenStatus } from './appStatus'
import { mountGoogleDriveFolder } from './browserfs'
import { showOptionsModal } from './react/SelectOption'
import { appQueryParams } from './appParams'
@ -67,7 +66,7 @@ export const possiblyHandleStateVariable = async () => {
}
setLoadingScreenStatus('Opening world in read only mode...')
googleProviderState.accessToken = response.access_token
await mountGoogleDriveFolder(true, parsed.ids[0])
// await mountGoogleDriveFolder(true, parsed.ids[0])
await loadInMemorySave('/google')
}
})

View file

@ -95,8 +95,7 @@ import { startLocalReplayServer } from './packetsReplay/replayPackets'
import { localRelayServerPlugin } from './mineflayer/plugins/packetsRecording'
import { createConsoleLogProgressReporter, createFullScreenProgressReporter, ProgressReporter } from './core/progressReporter'
import { appViewer } from './appViewer'
import createGraphicsBackend from 'renderer/viewer/three/graphicsBackend'
import { subscribeKey } from 'valtio/utils'
import './appViewerLoad'
window.debug = debug
window.beforeRenderFrame = []
@ -115,30 +114,6 @@ customChannels()
if (appQueryParams.testCrashApp === '2') throw new Error('test')
const loadBackend = () => {
appViewer.loadBackend(createGraphicsBackend)
}
window.loadBackend = loadBackend
if (process.env.SINGLE_FILE_BUILD_MODE) {
const unsub = subscribeKey(miscUiState, 'fsReady', () => {
if (miscUiState.fsReady) {
// don't do it earlier to load fs and display menu faster
loadBackend()
unsub()
}
})
} else {
loadBackend()
}
const animLoop = () => {
for (const fn of beforeRenderFrame) fn()
requestAnimationFrame(animLoop)
}
requestAnimationFrame(animLoop)
watchOptionsAfterViewerInit()
function hideCurrentScreens () {
activeModalStacks['main-menu'] = [...activeModalStack]
insertActiveModalStack('', [])
@ -223,6 +198,7 @@ export async function connect (connectOptions: ConnectOptions) {
let bot!: typeof __type_bot
const destroyAll = () => {
if (ended) return
errorAbortController.abort()
ended = true
progress.end()
// dont reset viewer so we can still do debugging
@ -240,7 +216,6 @@ export async function connect (connectOptions: ConnectOptions) {
//@ts-expect-error
window.bot = bot = undefined
}
resetStateAfterDisconnect()
cleanFs()
}
const cleanFs = () => {
@ -261,7 +236,6 @@ export async function connect (connectOptions: ConnectOptions) {
if (err === 'ResizeObserver loop completed with undelivered notifications.') {
return
}
errorAbortController.abort()
if (isCypress()) throw err
miscUiState.hasErrors = true
if (miscUiState.gameLoaded) return
@ -272,6 +246,7 @@ export async function connect (connectOptions: ConnectOptions) {
destroyAll()
}
// todo(hard): remove it!
const errorAbortController = new AbortController()
window.addEventListener('unhandledrejection', (e) => {
if (e.reason.name === 'ServerPluginLoadFailure') {
@ -732,7 +707,7 @@ export async function connect (connectOptions: ConnectOptions) {
console.log('bot spawned - starting viewer')
appViewer.startWorld(bot.world, renderDistance)
await appViewer.startWorld(bot.world, renderDistance)
appViewer.worldView!.listenToBot(bot)
initMotionTracking()

View file

@ -22,7 +22,8 @@ export const fsState = proxy({
saveLoaded: false,
openReadOperations: 0,
openWriteOperations: 0,
remoteBackend: false
remoteBackend: false,
inMemorySavePath: ''
})
const PROPOSE_BACKUP = true
@ -181,6 +182,7 @@ export const loadSave = async (root = '/world') => {
// todo should not be set here
fsState.saveLoaded = true
fsState.inMemorySavePath = root
window.dispatchEvent(new CustomEvent('singleplayer', {
// todo check gamemode level.dat data etc
detail: {

View file

@ -61,8 +61,6 @@ export const guiOptionsScheme: {
custom () {
return <Button label='Guide: Disable VSync' onClick={() => openURL('https://gist.github.com/zardoy/6e5ce377d2b4c1e322e660973da069cd')} inScreen />
},
},
{
backgroundRendering: {
text: 'Background FPS limit',
values: [
@ -71,6 +69,12 @@ export const guiOptionsScheme: {
['20fps', '20 FPS'],
],
},
activeRenderer: {
text: 'Renderer',
values: [
['threejs', 'Three.js (stable)'],
],
},
},
{
custom () {
@ -108,12 +112,12 @@ export const guiOptionsScheme: {
},
{
custom () {
const { _renderByChunks } = useSnapshot(options).rendererOptions.three
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions
return <Button
inScreen
label={`Batch Chunks Display ${_renderByChunks ? 'ON' : 'OFF'}`}
onClick={() => {
options.rendererOptions.three._renderByChunks = !_renderByChunks
options.rendererSharedOptions._renderByChunks = !_renderByChunks
}}
/>
}

View file

@ -34,6 +34,7 @@ const defaultOptions = {
touchButtonsOpacity: 80,
touchButtonsPosition: 12,
touchControlsPositions: getDefaultTouchControlsPositions(),
touchControlsSize: getTouchControlsSize(),
touchMovementType: 'modern' as 'modern' | 'classic',
touchInteractionType: 'classic' as 'classic' | 'buttons',
gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power',
@ -109,11 +110,10 @@ const defaultOptions = {
disabledUiParts: [] as string[],
neighborChunkUpdates: true,
highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic',
rendererOptions: {
three: {
_experimentalSmoothChunkLoading: true,
_renderByChunks: false
}
activeRenderer: 'threejs',
rendererSharedOptions: {
_experimentalSmoothChunkLoading: true,
_renderByChunks: false
}
}
@ -138,6 +138,16 @@ function getDefaultTouchControlsPositions () {
} as Record<string, [number, number]>
}
function getTouchControlsSize () {
return {
joystick: 60,
action: 36,
break: 36,
jump: 36,
sneak: 36,
}
}
// const qsOptionsRaw = new URLSearchParams(location.search).getAll('setting')
const qsOptionsRaw = appQueryParamsArray.setting ?? []
export const qsOptions = Object.fromEntries(qsOptionsRaw.map(o => {

View file

@ -4,11 +4,11 @@ import { ParsedReplayPacket, parseReplayContents } from 'mcraft-fun-mineflayer/b
import { PACKETS_REPLAY_FILE_EXTENSION, WORLD_STATE_FILE_EXTENSION } from 'mcraft-fun-mineflayer/build/worldState'
import MinecraftData from 'minecraft-data'
import { GameMode } from 'mineflayer'
import { LocalServer } from '../customServer'
import { UserError } from '../mineflayer/userError'
import { packetsReplayState } from '../react/state/packetsReplayState'
import { getFixedFilesize } from '../react/simpleUtils'
import { appQueryParams } from '../appParams'
import { LocalServer } from '../customServer'
const SUPPORTED_FORMAT_VERSION = 1

View file

@ -71,12 +71,36 @@ export default () => {
useDidUpdateEffect(() => {
// todo play effect only when world successfully loaded
if (!isOpen) {
const divingElem: HTMLElement = document.querySelector('#viewer-canvas')!
divingElem.style.animationName = 'dive-animation'
divingElem.parentElement!.style.perspective = '1200px'
divingElem.onanimationend = () => {
divingElem.parentElement!.style.perspective = ''
divingElem.onanimationend = null
const startDiveAnimation = (divingElem: HTMLElement) => {
divingElem.style.animationName = 'dive-animation'
divingElem.parentElement!.style.perspective = '1200px'
divingElem.onanimationend = () => {
divingElem.parentElement!.style.perspective = ''
divingElem.onanimationend = null
}
}
const divingElem = document.querySelector('#viewer-canvas')
let observer: MutationObserver | null = null
if (divingElem) {
startDiveAnimation(divingElem as HTMLElement)
} else {
observer = new MutationObserver((mutations) => {
const divingElem = document.querySelector('#viewer-canvas')
if (divingElem) {
startDiveAnimation(divingElem as HTMLElement)
observer!.disconnect()
}
})
observer.observe(document.body, {
childList: true,
subtree: true
})
}
return () => {
if (observer) {
observer.disconnect()
}
}
}
}, [isOpen])

View file

@ -9,14 +9,15 @@ interface Props extends React.ComponentProps<typeof Button> {
localStorageKey?: string | null
offset?: number
}
alwaysTooltip?: string
}
const ARROW_HEIGHT = 7
const GAP = 0
export default ({ initialTooltip, ...args }: Props) => {
export default ({ initialTooltip, alwaysTooltip, ...args }: Props) => {
const { localStorageKey = 'firstTimeTooltip', offset = 0 } = initialTooltip
const [showTooltips, setShowTooltips] = useState(localStorageKey ? localStorage[localStorageKey] !== 'false' : true)
const [showTooltips, setShowTooltips] = useState(alwaysTooltip || (localStorageKey ? localStorage[localStorageKey] !== 'false' : true))
useEffect(() => {
let timeout
@ -67,7 +68,7 @@ export default ({ initialTooltip, ...args }: Props) => {
zIndex: 11
}}
>
{initialTooltip.content}
{alwaysTooltip || initialTooltip.content}
<FloatingArrow ref={arrowRef} context={context} style={{ opacity: 0.7 }} />
</div>
</>

View file

@ -11,12 +11,12 @@
}
.debug-left-side {
top: 1px;
top: 25px;
left: 1px;
}
.debug-right-side {
top: 5px;
top: 25px;
right: 1px;
/* limit renderer long text width */
width: 50%;

View file

@ -112,8 +112,10 @@ export default () => {
}
}, [])
let mapsProviderUrl = appConfig?.mapsProvider
if (mapsProviderUrl && location.origin !== 'https://mcraft.fun') mapsProviderUrl = mapsProviderUrl + '?to=' + encodeURIComponent(location.href)
const mapsProviderUrl = appConfig?.mapsProvider && new URL(appConfig?.mapsProvider)
if (mapsProviderUrl && location.origin !== 'https://mcraft.fun') {
mapsProviderUrl.searchParams.set('to', location.href)
}
// todo clean, use custom csstransition
return <Transition in={!noDisplay} timeout={disableAnimation ? 0 : 100} mountOnEnter unmountOnExit>
@ -134,7 +136,7 @@ export default () => {
openFilePicker()
}
}}
mapsProvider={mapsProviderUrl}
mapsProvider={mapsProviderUrl?.toString()}
versionStatus={versionStatus}
versionTitle={versionTitle}
onVersionStatusClick={async () => {

View file

@ -53,9 +53,8 @@ export const saveToBrowserMemory = async () => {
}
})
})
//@ts-expect-error
const { worldFolder } = localServer.options
const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`)
const worldFolder = fsState.inMemorySavePath
const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop()!, `/data/worlds`)
await mkdirRecursive(saveRootPath)
console.log('made world folder', saveRootPath)
const allRootPaths = [...usedServerPathsV1]
@ -290,7 +289,7 @@ export default () => {
) : null}
{!lockConnect && <>
<Button className="button" style={{ width: '204px' }} onClick={disconnect}>
{localServer && !fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect & Reset'}
{fsState.inMemorySave && !fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect & Reset'}
</Button>
</>}
{noConnection && (

View file

@ -50,7 +50,7 @@ export class LoadedResources {
export interface ResourcesCurrentConfig {
version: string
texturesVersion?: string
noBlockstatesModels?: boolean
// noBlockstatesModels?: boolean
noInventoryGui?: boolean
includeOnlyBlocks?: string[]
}
@ -115,34 +115,12 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
...resources.customModels
}
const blocksAssetsParser = new AtlasParser(this.sourceBlocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
const blockTexturesChanges = {} as Record<string, string>
const date = new Date()
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) {
Object.assign(blockTexturesChanges, christmasPack)
}
const customBlockTextures = Object.keys(resources.customTextures.blocks?.textures ?? {})
const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {})
console.time('createBlocksAtlas')
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(
resources.texturesVersion,
(textureName) => {
if (this.currentConfig!.includeOnlyBlocks && !this.currentConfig!.includeOnlyBlocks.includes(textureName)) return false
const texture = resources.customTextures.blocks?.textures[textureName]
return blockTexturesChanges[textureName] ?? texture
},
undefined,
undefined,
customBlockTextures
)
console.timeEnd('createBlocksAtlas')
await this.recreateBlockAtlas(resources)
if (abortController.signal.aborted) return
const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {})
console.time('createItemsAtlas')
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(
resources.texturesVersion,
@ -157,9 +135,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
)
console.timeEnd('createItemsAtlas')
resources.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
resources.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
resources.blocksAtlasImage = await getLoadedImage(blocksCanvas.toDataURL())
resources.itemsAtlasImage = await getLoadedImage(itemsCanvas.toDataURL())
if (resources.version && resources.blockstatesModels && resources.itemsAtlasParser && resources.blocksAtlasParser) {
@ -169,11 +145,6 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
resources.itemsAtlasParser,
resources.blocksAtlasParser
)
resources.worldBlockProvider = worldBlockProvider(
resources.blockstatesModels,
resources.blocksAtlasParser.atlas,
STABLE_MODELS_VERSION
)
}
if (abortController.signal.aborted) return
@ -196,6 +167,43 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
}
}
async recreateBlockAtlas (resources: LoadedResources = this.currentResources!) {
const blockTexturesChanges = {} as Record<string, string>
const date = new Date()
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) {
Object.assign(blockTexturesChanges, christmasPack)
}
const blocksAssetsParser = new AtlasParser(this.sourceBlocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
const customBlockTextures = Object.keys(resources.customTextures.blocks?.textures ?? {})
console.time('createBlocksAtlas')
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(
resources.texturesVersion,
(textureName) => {
if (this.currentConfig!.includeOnlyBlocks && !this.currentConfig!.includeOnlyBlocks.includes(textureName)) return false
const texture = resources.customTextures.blocks?.textures[textureName]
return blockTexturesChanges[textureName] ?? texture
},
undefined,
undefined,
customBlockTextures,
{
needHorizontalIndexes: !!this.currentConfig!.includeOnlyBlocks,
}
)
console.timeEnd('createBlocksAtlas')
resources.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
resources.blocksAtlasImage = await getLoadedImage(blocksCanvas.toDataURL())
resources.worldBlockProvider = worldBlockProvider(
resources.blockstatesModels,
resources.blocksAtlasParser.atlas,
STABLE_MODELS_VERSION
)
}
async generateGuiTextures () {
await generateGuiAtlas()
}

View file

@ -1 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-useless-empty-export
export { }
export default {}

View file

@ -1,3 +1,12 @@
const BrowserFS = require('browserfs')
module.exports = BrowserFS.BFSRequire('fs')
globalThis.fs ??= BrowserFS.BFSRequire('fs')
globalThis.fs.promises = new Proxy({}, {
get(target, p) {
return (...args) => {
return globalThis.promises[p](...args)
}
}
})
module.exports = globalThis.fs

View file

@ -1 +1 @@
module.exports.performance = window.performance
module.exports.performance = globalThis.performance

View file

@ -65,6 +65,16 @@ export const pointerLock = {
}
}
export const logAction = (category: string, action: string, value?: string, label?: string) => {
if (!options.externalLoggingService) return
window.loggingServiceChannel?.({
category,
action,
value,
label
})
}
export const isInRealGameSession = () => {
return isGameActive(true) && (!packetsReplayState.isOpen || packetsReplayState.isMinimized) && !gameAdditionalState.viewerConnection
}
@ -148,11 +158,11 @@ export const setRenderDistance = () => {
localServer!.players[0].view = 0
renderDistance = 0
}
worldView.updateViewDistance(renderDistance)
worldView?.updateViewDistance(renderDistance)
prevRenderDistance = renderDistance
}
export const reloadChunks = async () => {
if (!worldView) return
if (!bot || !worldView) return
setRenderDistance()
await worldView.updatePosition(bot.entity.position, true)
}

View file

@ -8,6 +8,8 @@ import { reloadChunks } from './utils'
import { miscUiState } from './globalState'
import { isCypress } from './standaloneUtils'
globalThis.viewer ??= { world: {} }
subscribeKey(options, 'renderDistance', reloadChunks)
subscribeKey(options, 'multiplayerRenderDistance', reloadChunks)
@ -85,8 +87,8 @@ export const watchOptionsAfterViewerInit = () => {
appViewer.inWorldRenderingConfig.extraBlockRenderers = !o.disableSignsMapsSupport
appViewer.inWorldRenderingConfig.fetchPlayerSkins = o.loadPlayerSkins
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererOptions.three._experimentalSmoothChunkLoading
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererOptions.three._renderByChunks
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
})
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting

View file

@ -1,3 +1,4 @@
import { ref } from 'valtio'
import { watchUnloadForCleanup } from './gameUnload'
let inWater = false
@ -9,9 +10,10 @@ customEvents.on('gameLoaded', () => {
watchUnloadForCleanup(cleanup)
const updateInWater = () => {
const waterBr = Object.keys(bot.entity.effects).find((effect: any) => loadedData.effects[effect.id].name === 'water_breathing')
const waterBr = Object.keys(bot.entity.effects).find((effect: any) => loadedData.effects[effect.id]?.name === 'water_breathing')
if (inWater) {
appViewer.playerState.reactive.inWater = true
appViewer.playerState.reactive.waterBreathing = waterBr !== undefined
} else {
cleanup()
}
@ -31,5 +33,5 @@ let sceneBg = { r: 0, g: 0, b: 0 }
export const updateBackground = (newSceneBg = sceneBg) => {
sceneBg = newSceneBg
const color: [number, number, number] = inWater ? [0, 0, 1] : [sceneBg.r, sceneBg.g, sceneBg.b]
appViewer.playerState.reactive.backgroundColor = color
appViewer.playerState.reactive.backgroundColor = ref(color)
}