pages235/src/appViewer.ts

298 lines
9.6 KiB
TypeScript

import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
import { BasePlayerState, IPlayerState } from 'renderer/viewer/lib/basePlayerState'
import { subscribeKey } from 'valtio/utils'
import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon'
import { Vec3 } from 'vec3'
import { SoundSystem } from 'renderer/viewer/three/threeJsSound'
import { proxy } from 'valtio'
import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend'
import { getSyncWorld } from 'renderer/playground/shared'
import { playerState } from './mineflayer/playerState'
import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter'
import { setLoadingScreenStatus } from './appStatus'
import { activeModalStack, miscUiState } from './globalState'
import { options } from './optionsStorage'
import { ResourcesManager } from './resourcesManager'
import { watchOptionsAfterWorldViewInit } from './watchOptions'
export interface RendererReactiveState {
world: {
chunksLoaded: string[]
chunksTotalNumber: number
allChunksLoaded: boolean
mesherWork: boolean
intersectMedia: { id: string, x: number, y: number } | null
}
renderer: string
preventEscapeMenu: boolean
}
export interface NonReactiveState {
world: {
chunksLoaded: string[]
chunksTotalNumber: number
allChunksLoaded: boolean
mesherWork: boolean
intersectMedia: { id: string, x: number, y: number } | null
}
}
export interface GraphicsBackendConfig {
fpsLimit?: number
powerPreference?: 'high-performance' | 'low-power'
statsVisible?: number
sceneBackground: string
}
const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
fpsLimit: undefined,
powerPreference: undefined,
sceneBackground: 'lightblue'
}
export interface GraphicsInitOptions<S = any> {
resourcesManager: ResourcesManager
config: GraphicsBackendConfig
rendererSpecificSettings: S
displayCriticalError: (error: Error) => void
setRendererSpecificSettings: (key: string, value: any) => void
}
export interface DisplayWorldOptions {
version: string
worldView: WorldDataEmitter
inWorldRenderingConfig: WorldRendererConfig
playerState: IPlayerState
rendererState: RendererReactiveState
nonReactiveState: NonReactiveState
}
export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => GraphicsBackend) & {
id: string
}
// no sync methods
export interface GraphicsBackend {
id: string
displayName?: string
startPanorama: () => void
// prepareResources: (version: string, progressReporter: ProgressReporter) => Promise<void>
startWorld: (options: DisplayWorldOptions) => Promise<void> | void
disconnect: () => void
setRendering: (rendering: boolean) => void
getDebugOverlay?: () => Record<string, any>
updateCamera: (pos: Vec3 | null, yaw: number, pitch: number) => void
setRoll?: (roll: number) => void
soundSystem: SoundSystem | undefined
backendMethods: Record<string, unknown> | undefined
}
export class AppViewer {
waitBackendLoadPromises = [] as Array<Promise<void>>
resourcesManager = new ResourcesManager()
worldView: WorldDataEmitter | undefined
readonly config: GraphicsBackendConfig = {
...defaultGraphicsBackendConfig,
powerPreference: options.gpuPreference === 'default' ? undefined : options.gpuPreference
}
backend?: GraphicsBackend
backendLoader?: GraphicsBackendLoader
private currentState?: {
method: string
args: any[]
}
currentDisplay = null as 'menu' | 'world' | null
inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig)
lastCamUpdate = 0
playerState = playerState
rendererState = proxy(getDefaultRendererState())
nonReactiveState: NonReactiveState = getDefaultRendererState()
worldReady: Promise<void>
private resolveWorldReady: () => void
constructor () {
this.disconnectBackend()
}
async loadBackend (loader: GraphicsBackendLoader) {
if (this.backend) {
this.disconnectBackend()
}
await Promise.all(this.waitBackendLoadPromises)
this.waitBackendLoadPromises = []
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,
displayCriticalError (error) {
console.error(error)
setLoadingScreenStatus(error.message, true)
},
rendererSpecificSettings,
setRendererSpecificSettings (key: string, value: any) {
options[`${rendererSettingsKey}.${key}`] = value
}
}
this.backend = loader(loaderOptions)
// if (this.resourcesManager.currentResources) {
// void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter())
// }
// Execute queued action if exists
if (this.currentState) {
const { method, args } = this.currentState
this.backend[method](...args)
if (method === 'startWorld') {
// void this.worldView!.init(args[0].playerState.getPosition())
}
}
}
async startWithBot () {
const renderDistance = miscUiState.singleplayer ? options.renderDistance : options.multiplayerRenderDistance
await this.startWorld(bot.world, renderDistance)
this.worldView!.listenToBot(bot)
}
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()
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
window.worldView = this.worldView
watchOptionsAfterWorldViewInit(this.worldView)
const displayWorldOptions: DisplayWorldOptions = {
version: this.resourcesManager.currentConfig!.version,
worldView: this.worldView,
inWorldRenderingConfig: this.inWorldRenderingConfig,
playerState: playerStateSend,
rendererState: this.rendererState,
nonReactiveState: this.nonReactiveState
}
let promise: undefined | Promise<void>
if (this.backend) {
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) {
this.disconnectBackend(cleanState)
if (this.backendLoader) {
this.loadBackend(this.backendLoader)
}
}
startPanorama () {
if (this.currentDisplay === 'menu') return
this.currentDisplay = 'menu'
if (options.disableAssets) return
if (this.backend) {
this.backend.startPanorama()
}
this.currentState = { method: 'startPanorama', args: [] }
}
// async prepareResources (version: string, progressReporter: ProgressReporter) {
// if (this.backend) {
// await this.backend.prepareResources(version, progressReporter)
// }
// }
destroyAll () {
this.disconnectBackend()
this.resourcesManager.destroy()
}
disconnectBackend (cleanState = false) {
if (cleanState) {
this.currentState = undefined
this.currentDisplay = null
this.worldView = undefined
}
if (this.backend) {
this.backend.disconnect()
this.backend = undefined
}
this.currentDisplay = null
const { promise, resolve } = Promise.withResolvers<void>()
this.worldReady = promise
this.resolveWorldReady = resolve
this.rendererState = proxy(getDefaultRendererState())
// this.queuedDisplay = undefined
}
get utils () {
return {
async waitingForChunks () {
if (this.backend?.worldState.allChunksLoaded) return
return new Promise((resolve) => {
const interval = setInterval(() => {
if (this.backend?.worldState.allChunksLoaded) {
clearInterval(interval)
resolve(true)
}
}, 100)
})
}
}
}
}
export const appViewer = new AppViewer()
window.appViewer = appViewer
const initialMenuStart = async () => {
if (appViewer.currentDisplay === 'world') {
appViewer.resetBackend(true)
}
appViewer.startPanorama()
// 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
// await appViewer.startWorld(world, 3)
// appViewer.backend?.updateCamera(new Vec3(0, 64, 2), 0, 0)
// void appViewer.worldView!.init(new Vec3(0, 64, 0))
}
window.initialMenuStart = initialMenuStart
const modalStackUpdateChecks = () => {
// maybe start panorama
if (activeModalStack.length === 0 && !miscUiState.gameLoaded) {
void initialMenuStart()
}
if (appViewer.backend) {
const hasAppStatus = activeModalStack.some(m => m.reactType === 'app-status')
appViewer.backend.setRendering(!hasAppStatus)
}
appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0
}
subscribeKey(activeModalStack, 'length', modalStackUpdateChecks)
modalStackUpdateChecks()