diff --git a/.cursor/rules/vars-usage.mdc b/.cursor/rules/vars-usage.mdc new file mode 100644 index 00000000..7120e7ae --- /dev/null +++ b/.cursor/rules/vars-usage.mdc @@ -0,0 +1,16 @@ +--- +description: Restricts usage of the global Mineflayer `bot` variable to only src/ files; prohibits usage in renderer/. Specifies correct usage of player state and appViewer globals. +globs: src/**/*.ts,renderer/**/*.ts +alwaysApply: false +--- +Ask AI + +- The global variable `bot` refers to the Mineflayer bot instance. +- You may use `bot` directly in any file under the `src/` directory (e.g., `src/mineflayer/playerState.ts`). +- Do **not** use `bot` directly in any file under the `renderer/` directory or its subfolders (e.g., `renderer/viewer/three/worldrendererThree.ts`). +- In renderer code, all bot/player state and events must be accessed via explicit interfaces, state managers, or passed-in objects, never by referencing `bot` directly. +- In renderer code (such as in `WorldRendererThree`), use the `playerState` property (e.g., `worldRenderer.playerState.gameMode`) to access player state. The implementation for `playerState` lives in `src/mineflayer/playerState.ts`. +- In `src/` code, you may use the global variable `appViewer` from `src/appViewer.ts` directly. Do **not** import `appViewer` or use `window.appViewer`; use the global `appViewer` variable as-is. +- Some other global variables that can be used without window prefixes are listed in src/globals.d.ts + +Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state. diff --git a/.gitignore b/.gitignore index bd774315..33734572 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,6 @@ generated storybook-static server-jar config.local.json +logs/ src/react/npmReactComponents.ts diff --git a/renderer/buildMesherWorker.mjs b/renderer/buildMesherWorker.mjs index 47ea5771..d88297a5 100644 --- a/renderer/buildMesherWorker.mjs +++ b/renderer/buildMesherWorker.mjs @@ -35,6 +35,10 @@ const buildOptions = { define: { 'process.env.BROWSER': '"true"', }, + loader: { + '.png': 'dataurl', + '.obj': 'text' + }, plugins: [ ...mesherSharedPlugins, { diff --git a/renderer/viewer/baseGraphicsBackend.ts b/renderer/viewer/baseGraphicsBackend.ts index 4724076a..3cd227de 100644 --- a/renderer/viewer/baseGraphicsBackend.ts +++ b/renderer/viewer/baseGraphicsBackend.ts @@ -1,16 +1,26 @@ -import { RendererReactiveState } from '../../src/appViewer' +import { NonReactiveState, RendererReactiveState } from '../../src/appViewer' -export const getDefaultRendererState = (): RendererReactiveState => { +export const getDefaultRendererState = (): { + reactive: RendererReactiveState + nonReactive: NonReactiveState +} => { return { - world: { - chunksLoaded: new Set(), - heightmaps: new Map(), - chunksTotalNumber: 0, - allChunksLoaded: true, - mesherWork: false, - intersectMedia: null + reactive: { + world: { + chunksLoaded: new Set(), + heightmaps: new Map(), + allChunksLoaded: true, + mesherWork: false, + intersectMedia: null + }, + renderer: '', + preventEscapeMenu: false }, - renderer: '', - preventEscapeMenu: false + nonReactive: { + world: { + chunksLoaded: new Set(), + chunksTotalNumber: 0, + } + } } } diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index 856772b1..fe6e39c4 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -1,125 +1,69 @@ -import { EventEmitter } from 'events' -import { Vec3 } from 'vec3' -import TypedEmitter from 'typed-emitter' import { ItemSelector } from 'mc-assets/dist/itemDefinitions' -import { proxy, ref } from 'valtio' import { GameMode } from 'mineflayer' -import { HandItemBlock } from '../three/holdingBlock' +import { proxy } from 'valtio' +import type { HandItemBlock } from '../three/holdingBlock' export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING' export type ItemSpecificContextProperties = Partial> -export type PlayerStateEvents = { - heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void -} - export type BlockShape = { position: any; width: any; height: any; depth: any; } export type BlocksShapes = BlockShape[] -export interface IPlayerState { - getEyeHeight(): number - getMovementState(): MovementState - getVelocity(): Vec3 - isOnGround(): boolean - isSneaking(): boolean - isFlying(): boolean - isSprinting (): boolean - getItemUsageTicks?(): number - getPosition(): Vec3 - // isUsingItem?(): boolean - getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined - username?: string - onlineMode?: boolean - lightingDisabled?: boolean - shouldHideHand?: boolean +// edit src/mineflayer/playerState.ts for implementation of player state from mineflayer +export const getInitialPlayerState = () => proxy({ + playerSkin: undefined as string | undefined, + inWater: false, + waterBreathing: false, + backgroundColor: [0, 0, 0] as [number, number, number], + ambientLight: 0, + directionalLight: 0, + eyeHeight: 0, + gameMode: undefined as GameMode | undefined, + lookingAtBlock: undefined as { + x: number + y: number + z: number + face?: number + shapes: BlocksShapes + } | undefined, + diggingBlock: undefined as { + x: number + y: number + z: number + stage: number + face?: number + mergedShape: BlockShape | undefined + } | undefined, + movementState: 'NOT_MOVING' as MovementState, + onGround: true, + sneaking: false, + flying: false, + sprinting: false, + itemUsageTicks: 0, + username: '', + onlineMode: false, + lightingDisabled: false, + shouldHideHand: false, + heldItemMain: undefined as HandItemBlock | undefined, + heldItemOff: undefined as HandItemBlock | undefined, +}) - events: TypedEmitter +export const getInitialPlayerStateRenderer = () => ({ + reactive: getInitialPlayerState() +}) - reactive: { - playerSkin: string | undefined - inWater: boolean - waterBreathing: boolean - backgroundColor: [number, number, number] - ambientLight: number - directionalLight: number - gameMode?: GameMode - lookingAtBlock?: { - x: number - y: number - z: number - face?: number - shapes: BlocksShapes - } - diggingBlock?: { - x: number - y: number - z: number - stage: number - face?: number - mergedShape?: BlockShape - } - } +export type PlayerStateReactive = ReturnType + +export interface PlayerStateRenderer { + reactive: PlayerStateReactive } -export class BasePlayerState implements IPlayerState { - reactive = proxy({ - playerSkin: undefined as string | undefined, - inWater: false, - waterBreathing: false, - backgroundColor: ref([0, 0, 0]) as [number, number, number], - ambientLight: 0, - directionalLight: 0, - }) - protected movementState: MovementState = 'NOT_MOVING' - protected velocity = new Vec3(0, 0, 0) - protected onGround = true - protected sneaking = false - protected flying = false - protected sprinting = false - readonly events = new EventEmitter() as TypedEmitter - - getEyeHeight (): number { - return 1.62 - } - - getMovementState (): MovementState { - return this.movementState - } - - getVelocity (): Vec3 { - return this.velocity - } - - isOnGround (): boolean { - return this.onGround - } - - isSneaking (): boolean { - return this.sneaking - } - - isFlying (): boolean { - return this.flying - } - - isSprinting (): boolean { - return this.sprinting - } - - getPosition (): Vec3 { - return new Vec3(0, 0, 0) - } - - // For testing purposes - setState (state: Partial<{ - movementState: MovementState - velocity: Vec3 - onGround: boolean - sneaking: boolean - flying: boolean - sprinting: boolean - }>) { - Object.assign(this, state) +export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => { + return { + ...specificProperties, + 'minecraft:date': new Date(), + // "minecraft:context_dimension": bot.entityp, + // 'minecraft:time': bot.time.timeOfDay / 24_000, } } diff --git a/renderer/viewer/lib/guiRenderer.ts b/renderer/viewer/lib/guiRenderer.ts index 2689126e..709941dc 100644 --- a/renderer/viewer/lib/guiRenderer.ts +++ b/renderer/viewer/lib/guiRenderer.ts @@ -9,11 +9,6 @@ import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator' import { proxy, ref } from 'valtio' import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' -export const activeGuiAtlas = proxy({ - atlas: null as null | { json, image }, - version: 0 -}) - export const getNonFullBlocksModels = () => { let version = appViewer.resourcesManager.currentResources!.version ?? 'latest' if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13' @@ -122,18 +117,18 @@ const RENDER_SIZE = 64 const generateItemsGui = async (models: Record, isItems = false) => { const { currentResources } = appViewer.resourcesManager - const img = await getLoadedImage(isItems ? currentResources!.itemsAtlasParser.latestImage : currentResources!.blocksAtlasParser.latestImage) + const imgBitmap = isItems ? currentResources!.itemsAtlasImage : currentResources!.blocksAtlasImage const canvasTemp = document.createElement('canvas') - canvasTemp.width = img.width - canvasTemp.height = img.height + canvasTemp.width = imgBitmap.width + canvasTemp.height = imgBitmap.height canvasTemp.style.imageRendering = 'pixelated' const ctx = canvasTemp.getContext('2d')! ctx.imageSmoothingEnabled = false - ctx.drawImage(img, 0, 0) + ctx.drawImage(imgBitmap, 0, 0) - const atlasParser = isItems ? currentResources!.itemsAtlasParser : currentResources!.blocksAtlasParser + const atlasParser = isItems ? appViewer.resourcesManager.itemsAtlasParser : appViewer.resourcesManager.blocksAtlasParser const textureAtlas = new TextureAtlas( - ctx.getImageData(0, 0, img.width, img.height), + ctx.getImageData(0, 0, imgBitmap.width, imgBitmap.height), Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => { return [key, [ value.u, @@ -243,6 +238,9 @@ const generateItemsGui = async (models: Record, isIt return images } +/** + * @mainThread + */ const generateAtlas = async (images: Record) => { const atlas = makeTextureAtlas({ input: Object.keys(images), @@ -260,9 +258,9 @@ const generateAtlas = async (images: Record) => { // a.download = 'blocks_atlas.png' // a.click() - activeGuiAtlas.atlas = { + appViewer.resourcesManager.currentResources!.guiAtlas = { json: atlas.json, - image: ref(await getLoadedImage(atlas.canvas.toDataURL())), + image: await createImageBitmap(atlas.canvas), } return atlas @@ -279,6 +277,6 @@ export const generateGuiAtlas = async () => { const itemImages = await generateItemsGui(itemsModelsResolved, true) console.timeEnd('generate items gui atlas') await generateAtlas({ ...blockImages, ...itemImages }) - activeGuiAtlas.version++ + appViewer.resourcesManager.currentResources!.guiAtlasVersion++ // await generateAtlas(blockImages) } diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index f26d8022..a063d77f 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -77,6 +77,7 @@ const handleMessage = data => { if (data.type === 'mcData') { globalVar.mcData = data.mcData + globalVar.loadedData = data.mcData } if (data.config) { @@ -138,6 +139,7 @@ const handleMessage = data => { dirtySections = new Map() // todo also remove cached globalVar.mcData = null + globalVar.loadedData = null allDataReady = false break diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 82416fab..53e0c534 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -9,7 +9,7 @@ export const defaultMesherConfig = { skyLight: 15, smoothLighting: true, outputFormat: 'threeJs' as 'threeJs' | 'webgpu', - textureSize: 1024, // for testing + // textureSize: 1024, // for testing debugModelVariant: undefined as undefined | number[], clipWorldBelowY: undefined as undefined | number, disableSignsMapsSupport: false diff --git a/renderer/viewer/lib/utils.ts b/renderer/viewer/lib/utils.ts index a1574b5c..5c9c00f1 100644 --- a/renderer/viewer/lib/utils.ts +++ b/renderer/viewer/lib/utils.ts @@ -1,4 +1,5 @@ import * as THREE from 'three' +import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './utils/skins' let textureCache: Record = {} let imagesPromises: Record> = {} @@ -7,7 +8,9 @@ export async function loadTexture (texture: string, cb: (texture: THREE.Texture) const cached = textureCache[texture] if (!cached) { const { promise, resolve } = Promise.withResolvers() - textureCache[texture] = new THREE.TextureLoader().load(texture, resolve) + const t = loadThreeJsTextureFromUrlSync(texture) + textureCache[texture] = t.texture + void t.promise.then(resolve) imagesPromises[texture] = promise } diff --git a/renderer/viewer/lib/utils/skins.ts b/renderer/viewer/lib/utils/skins.ts index 98792820..e0c8c32e 100644 --- a/renderer/viewer/lib/utils/skins.ts +++ b/renderer/viewer/lib/utils/skins.ts @@ -1,18 +1,43 @@ import { loadSkinToCanvas } from 'skinview-utils' import * as THREE from 'three' import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png' +import { getLoadedImage } from 'mc-assets/dist/utils' -// eslint-disable-next-line unicorn/prefer-export-from -export const stevePngUrl = stevePng -export const steveTexture = new THREE.TextureLoader().loadAsync(stevePng) - -export async function loadImageFromUrl (imageUrl: string): Promise { - const img = new Image() - img.src = imageUrl - await new Promise(resolve => { - img.onload = () => resolve() +export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => { + const texture = new THREE.Texture() + const promise = getLoadedImage(imageUrl).then(image => { + texture.image = image + texture.needsUpdate = true + return texture }) - return img + return { + texture, + promise + } +} + +export const loadThreeJsTextureFromUrl = async (imageUrl: string) => { + const loaded = new THREE.TextureLoader().loadAsync(imageUrl) + return loaded +} +export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => { + const canvas = new OffscreenCanvas(image.width, image.height) + const ctx = canvas.getContext('2d')! + ctx.drawImage(image, 0, 0) + const texture = new THREE.Texture(canvas) + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + return texture +} + +export const stevePngUrl = stevePng +export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl) + + +export async function loadImageFromUrl (imageUrl: string): Promise { + const response = await fetch(imageUrl) + const blob = await response.blob() + return createImageBitmap(blob) } const config = { @@ -52,13 +77,13 @@ export const parseSkinTexturesValue = (value: string) => { return decodedData.textures?.SKIN?.url } -export async function loadSkinImage (skinUrl: string): Promise<{ canvas: HTMLCanvasElement, image: HTMLImageElement }> { +export async function loadSkinImage (skinUrl: string): Promise<{ canvas: OffscreenCanvas, image: ImageBitmap }> { if (!skinUrl.startsWith('data:')) { skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://')) } const image = await loadImageFromUrl(skinUrl) - const skinCanvas = document.createElement('canvas') + const skinCanvas = new OffscreenCanvas(64, 64) loadSkinToCanvas(skinCanvas, image) return { canvas: skinCanvas, image } } diff --git a/renderer/viewer/lib/workerProxy.ts b/renderer/viewer/lib/workerProxy.ts index 9d8e7fcc..2b38dca9 100644 --- a/renderer/viewer/lib/workerProxy.ts +++ b/renderer/viewer/lib/workerProxy.ts @@ -1,9 +1,20 @@ -export function createWorkerProxy void>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { +import { proxy, getVersion, subscribe } from 'valtio' + +export function createWorkerProxy void | Promise>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { const target = channel ?? globalThis target.addEventListener('message', (event: any) => { - const { type, args } = event.data + const { type, args, msgId } = event.data if (handlers[type]) { - handlers[type](...args) + const result = handlers[type](...args) + if (result instanceof Promise) { + void result.then((result) => { + target.postMessage({ + type: 'result', + msgId, + args: [result] + }) + }) + } } }) return null as any @@ -23,6 +34,7 @@ export function createWorkerProxy v export const useWorkerProxy = void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & { transfer: (...args: Transferable[]) => T['__workerProxy'] } => { + let messageId = 0 // in main thread return new Proxy({} as any, { get (target, prop) { @@ -41,11 +53,30 @@ export const useWorkerProxy = { - const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : [] + const msgId = messageId++ + const transfer = autoTransfer ? args.filter(arg => { + return arg instanceof ArrayBuffer || arg instanceof MessagePort + || (typeof ImageBitmap !== 'undefined' && arg instanceof ImageBitmap) + || (typeof OffscreenCanvas !== 'undefined' && arg instanceof OffscreenCanvas) + || (typeof ImageData !== 'undefined' && arg instanceof ImageData) + }) : [] worker.postMessage({ type: prop, + msgId, args, - }, transfer as any[]) + }, transfer) + return { + // eslint-disable-next-line unicorn/no-thenable + then (onfulfilled: (value: any) => void) { + const handler = ({ data }: MessageEvent): void => { + if (data.type === 'result' && data.msgId === msgId) { + onfulfilled(data.args[0]) + worker.removeEventListener('message', handler as EventListener) + } + } + worker.addEventListener('message', handler as EventListener) + } + } } } }) diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index 9e483ee2..86f372d1 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -7,9 +7,7 @@ import { Vec3 } from 'vec3' import { BotEvents } from 'mineflayer' import { proxy } from 'valtio' import TypedEmitter from 'typed-emitter' -import { getItemFromBlock } from '../../../src/chatUtils' import { delayedIterator } from '../../playground/shared' -import { playerState } from '../../../src/mineflayer/playerState' import { chunkPos } from './simpleUtils' export type ChunkPosKey = string // like '16,16' @@ -23,7 +21,6 @@ export type WorldDataEmitterEvents = { time: (data: number) => void renderDistance: (viewDistance: number) => void blockEntities: (data: Record | { blockEntities: Record }) => void - listening: () => void markAsLoaded: (data: { x: number, z: number }) => void unloadChunk: (data: { x: number, z: number }) => void loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void @@ -32,10 +29,10 @@ export type WorldDataEmitterEvents = { end: () => void } -/** - * Usually connects to mineflayer bot and emits world data (chunks, entities) - * It's up to the consumer to serialize the data if needed - */ +export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter) { + static readonly restorerName = 'WorldDataEmitterWorker' +} + export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter) { loadedChunks: Record readonly lastPos: Vec3 @@ -57,11 +54,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter TypedEmitter { - this.emitter.emit('blockEntities', new Proxy({}, { - get (_target, posKey, receiver) { - if (typeof posKey !== 'string') return - const [x, y, z] = posKey.split(',').map(Number) - return bot.world.getBlock(new Vec3(x, y, z))?.entity - }, - })) - this.emitter.emit('renderDistance', this.viewDistance) - this.emitter.emit('time', bot.time.timeOfDay) - }) - // node.js stream data event pattern - if (this.emitter.listenerCount('blockEntities')) { - this.emitter.emit('listening') - } - for (const [evt, listener] of Object.entries(this.eventListeners)) { bot.on(evt as any, listener) } @@ -200,8 +176,16 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter TypedEmitter new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 2b37742c..1f0df0bd 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -1,25 +1,22 @@ /* eslint-disable guard-for-in */ import { EventEmitter } from 'events' import { Vec3 } from 'vec3' -import * as THREE from 'three' import mcDataRaw from 'minecraft-data/data.js' // note: using alias 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 { proxy } from 'valtio' import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' -import { toMajorVersion } from '../../../src/utils' -import { ResourcesManager } from '../../../src/resourcesManager' +import type { ResourcesManagerTransferred } from '../../../src/resourcesManager' import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer' import { SoundSystem } from '../three/threeJsSound' import { buildCleanupDecorator } from './cleanupDecorator' -import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' +import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared' import { chunkPos } from './simpleUtils' -import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats' -import { WorldDataEmitter } from './worldDataEmitter' -import { IPlayerState } from './basePlayerState' +import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats' +import { WorldDataEmitterWorker } from './worldDataEmitter' +import { PlayerStateRenderer } from './basePlayerState' import { MesherLogReader } from './mesherlogReader' import { setSkinsConfig } from './utils/skins' @@ -27,6 +24,11 @@ function mod (x, n) { return ((x % n) + n) % n } +const toMajorVersion = version => { + const [a, b] = (String(version)).split('.') + return `${a}.${b}` +} + export const worldCleanup = buildCleanupDecorator('resetWorld') export const defaultWorldRendererConfig = { @@ -52,7 +54,8 @@ export const defaultWorldRendererConfig = { foreground: true, enableDebugOverlay: false, _experimentalSmoothChunkLoading: true, - _renderByChunks: false + _renderByChunks: false, + volume: 1 } export type WorldRendererConfig = typeof defaultWorldRendererConfig @@ -153,7 +156,7 @@ export abstract class WorldRendererCommon abstract changeBackgroundColor (color: [number, number, number]): void worldRendererConfig: WorldRendererConfig - playerState: IPlayerState + playerState: PlayerStateRenderer reactiveState: RendererReactiveState mesherLogReader: MesherLogReader | undefined forceCallFromMesherReplayer = false @@ -169,6 +172,7 @@ export abstract class WorldRendererCommon } currentRenderedFrames = 0 fpsAverage = 0 + lastFps = 0 fpsWorst = undefined as number | undefined fpsSamples = 0 mainThreadRendering = true @@ -184,7 +188,7 @@ export abstract class WorldRendererCommon return (this.initOptions.config.statsVisible ?? 0) > 1 } - constructor (public readonly resourcesManager: ResourcesManager, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) { + constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) { this.snapshotInitialValues() this.worldRendererConfig = displayOptions.inWorldRenderingConfig this.playerState = displayOptions.playerState @@ -221,6 +225,7 @@ export abstract class WorldRendererCommon } else { this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames) } + this.lastFps = this.currentRenderedFrames this.currentRenderedFrames = 0 } @@ -231,15 +236,11 @@ export abstract class WorldRendererCommon async init () { if (this.active) throw new Error('WorldRendererCommon is already initialized') - await this.resourcesManager.loadMcData(this.version) - if (!this.resourcesManager.currentResources) { - await this.resourcesManager.updateAssetsData({ }) - } await Promise.all([ this.resetWorkers(), (async () => { - if (this.resourcesManager.currentResources) { + if (this.resourcesManager.currentResources?.allReady) { await this.updateAssetsData() } })() @@ -291,35 +292,22 @@ export abstract class WorldRendererCommon initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) { // init workers for (let i = 0; i < numWorkers + 1; i++) { - // Node environment needs an absolute path, but browser needs the url of the file - const workerName = 'mesher.js' - // eslint-disable-next-line node/no-path-concat - const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName - - let worker: any - if (process.env.SINGLE_FILE_BUILD) { - const workerCode = document.getElementById('mesher-worker-code')!.textContent! - const blob = new Blob([workerCode], { type: 'text/javascript' }) - worker = new Worker(window.URL.createObjectURL(blob)) - } else { - worker = new Worker(src) - } - - worker.onmessage = ({ data }) => { + const worker = initMesherWorker((data) => { if (Array.isArray(data)) { this.messageQueue.push(...data) } else { this.messageQueue.push(data) } void this.processMessageQueue('worker') - } - if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) + }) this.workers.push(worker) } } - onReactiveValueUpdated(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) { - callback(this.displayOptions.playerState.reactive[key]) + onReactivePlayerStateUpdated(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void, initial = true) { + if (initial) { + callback(this.displayOptions.playerState.reactive[key]) + } subscribeKey(this.displayOptions.playerState.reactive, key, callback) } @@ -334,7 +322,7 @@ export abstract class WorldRendererCommon } watchReactivePlayerState () { - this.onReactiveValueUpdated('backgroundColor', (value) => { + this.onReactivePlayerStateUpdated('backgroundColor', (value) => { this.changeBackgroundColor(value) }) } @@ -466,7 +454,7 @@ export abstract class WorldRendererCommon } if (data.type === 'heightmap') { - appViewer.rendererState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap)) + this.reactiveState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap)) } } @@ -543,7 +531,7 @@ export abstract class WorldRendererCommon this.resetWorld() // for workers in single file build - if (document?.readyState === 'loading') { + if (typeof document !== 'undefined' && document?.readyState === 'loading') { await new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve) }) @@ -575,7 +563,7 @@ export abstract class WorldRendererCommon skyLight, smoothLighting: this.worldRendererConfig.smoothLighting, outputFormat: this.outputFormat, - textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, + // textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, debugModelVariant: undefined, clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, @@ -600,7 +588,7 @@ export abstract class WorldRendererCommon } async updateAssetsData () { - const resources = this.resourcesManager.currentResources! + const resources = this.resourcesManager.currentResources if (this.workers.length === 0) throw new Error('workers not initialized yet') for (const [i, worker] of this.workers.entries()) { @@ -610,7 +598,7 @@ export abstract class WorldRendererCommon type: 'mesherData', workerIndex: i, blocksAtlas: { - latest: resources.blocksAtlasParser.atlas.latest + latest: resources.blocksAtlasJson }, blockstatesModels, config: this.getMesherConfig(), @@ -733,7 +721,7 @@ export abstract class WorldRendererCommon lightUpdate (chunkX: number, chunkZ: number) { } - connect (worldView: WorldDataEmitter) { + connect (worldView: WorldDataEmitterWorker) { const worldEmitter = worldView worldEmitter.on('entity', (e) => { @@ -812,7 +800,16 @@ export abstract class WorldRendererCommon }) worldEmitter.on('onWorldSwitch', () => { - for (const fn of this.onWorldSwitched) fn() + for (const fn of this.onWorldSwitched) { + try { + fn() + } catch (e) { + setTimeout(() => { + console.log('[Renderer Backend] Error in onWorldSwitched:') + throw e + }, 0) + } + } }) worldEmitter.on('time', (timeOfDay) => { @@ -830,8 +827,6 @@ export abstract class WorldRendererCommon // (this).rerenderAllChunks?.() // } }) - - worldEmitter.emit('listening') } setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) { @@ -1029,3 +1024,37 @@ export abstract class WorldRendererCommon removeAllStats() } } + +export const initMesherWorker = (onGotMessage: (data: any) => void) => { + // Node environment needs an absolute path, but browser needs the url of the file + const workerName = 'mesher.js' + + let worker: any + if (process.env.SINGLE_FILE_BUILD) { + const workerCode = document.getElementById('mesher-worker-code')!.textContent! + const blob = new Blob([workerCode], { type: 'text/javascript' }) + worker = new Worker(window.URL.createObjectURL(blob)) + } else { + worker = new Worker(workerName) + } + + worker.onmessage = ({ data }) => { + onGotMessage(data) + } + if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) }) + return worker +} + +export const meshersSendMcData = (workers: Worker[], version: string, addData = {} as Record) => { + const allMcData = mcDataRaw.pc[version] ?? mcDataRaw.pc[toMajorVersion(version)] + const mcData = { + version: JSON.parse(JSON.stringify(allMcData.version)) + } + for (const key of dynamicMcDataFiles) { + mcData[key] = allMcData[key] + } + + for (const worker of workers) { + worker.postMessage({ type: 'mcData', mcData, ...addData }) + } +} diff --git a/renderer/viewer/three/appShared.ts b/renderer/viewer/three/appShared.ts index 1dfb343c..5be9e10b 100644 --- a/renderer/viewer/three/appShared.ts +++ b/renderer/viewer/three/appShared.ts @@ -1,16 +1,16 @@ import { BlockModel } from 'mc-assets/dist/types' -import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' -import { renderSlot } from '../../../src/inventoryWindows' +import { ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items' -import { ResourcesManager } from '../../../src/resourcesManager' +import { ResourcesManager, ResourcesManagerTransferred } from '../../../src/resourcesManager' +import { renderSlot } from './renderSlot' -export const getItemUv = (item: Record, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager): { +export const getItemUv = (item: Record, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): { u: number v: number su: number sv: number renderInfo?: ReturnType - texture: HTMLImageElement + // texture: ImageBitmap modelName: string } | { resolvedModel: BlockModel @@ -30,11 +30,11 @@ export const getItemUv = (item: Record, specificProps: ItemSpecific const model = getItemModelName({ ...item, name, - } as GeneralInputItem, specificProps, resourcesManager) + } as GeneralInputItem, specificProps, resourcesManager, playerState) const renderInfo = renderSlot({ modelName: model, - }, false, true) + }, resourcesManager, false, true) if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`) @@ -53,7 +53,7 @@ export const getItemUv = (item: Record, specificProps: ItemSpecific return { u, v, su, sv, renderInfo, - texture: img, + // texture: img, modelName: renderInfo.modelName! } } @@ -67,7 +67,7 @@ export const getItemUv = (item: Record, specificProps: ItemSpecific v: 0, su: 16 / resources.blocksAtlasImage.width, sv: 16 / resources.blocksAtlasImage.width, - texture: resources.blocksAtlasImage, + // texture: resources.blocksAtlasImage, modelName: 'missing' } } diff --git a/renderer/viewer/three/documentRenderer.ts b/renderer/viewer/three/documentRenderer.ts index 1d556c2a..8ff31e69 100644 --- a/renderer/viewer/three/documentRenderer.ts +++ b/renderer/viewer/three/documentRenderer.ts @@ -6,15 +6,20 @@ import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appView import { WorldRendererConfig } from '../lib/worldrendererCommon' export class DocumentRenderer { - readonly canvas = document.createElement('canvas') + canvas: HTMLCanvasElement | OffscreenCanvas readonly renderer: THREE.WebGLRenderer private animationFrameId?: number + private timeoutId?: number private lastRenderTime = 0 - private previousWindowWidth = window.innerWidth - private previousWindowHeight = window.innerHeight + + private previousCanvasWidth = 0 + private previousCanvasHeight = 0 + private currentWidth = 0 + private currentHeight = 0 + private renderedFps = 0 private fpsInterval: any - private readonly stats: TopRightStats + private readonly stats: TopRightStats | undefined private paused = false disconnected = false preRender = () => { } @@ -26,9 +31,16 @@ export class DocumentRenderer { onRender = [] as Array<(sizeChanged: boolean) => void> inWorldRenderingConfig: WorldRendererConfig | undefined - constructor (initOptions: GraphicsInitOptions) { + constructor (initOptions: GraphicsInitOptions, public externalCanvas?: OffscreenCanvas) { this.config = initOptions.config + // Handle canvas creation/transfer based on context + if (externalCanvas) { + this.canvas = externalCanvas + } else { + this.addToPage() + } + try { this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, @@ -37,17 +49,24 @@ export class DocumentRenderer { powerPreference: this.config.powerPreference }) } catch (err) { - initOptions.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)) + initOptions.callbacks.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)) throw err } this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace - this.updatePixelRatio() - this.updateSize() - this.addToPage() + if (!externalCanvas) { + this.updatePixelRatio() + } + this.sizeUpdated() + // Initialize previous dimensions + this.previousCanvasWidth = this.canvas.width + this.previousCanvasHeight = this.canvas.height - this.stats = new TopRightStats(this.canvas, this.config.statsVisible) + // Only initialize stats and DOM-related features in main thread + if (!externalCanvas) { + this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible) + this.setupFpsTracking() + } - this.setupFpsTracking() this.startRenderLoop() } @@ -59,15 +78,33 @@ export class DocumentRenderer { this.renderer.setPixelRatio(pixelRatio) } - updateSize () { - this.renderer.setSize(window.innerWidth, window.innerHeight) + sizeUpdated () { + this.renderer.setSize(this.currentWidth, this.currentHeight, false) } private addToPage () { - this.canvas.id = 'viewer-canvas' - this.canvas.style.width = '100%' - this.canvas.style.height = '100%' - document.body.appendChild(this.canvas) + this.canvas = addCanvasToPage() + this.updateCanvasSize() + } + + updateSizeExternal (newWidth: number, newHeight: number, pixelRatio: number) { + this.currentWidth = newWidth + this.currentHeight = newHeight + this.renderer.setPixelRatio(pixelRatio) + this.sizeUpdated() + } + + private updateCanvasSize () { + if (!this.externalCanvas) { + const innnerWidth = window.innerWidth + const innnerHeight = window.innerHeight + if (this.currentWidth !== innnerWidth) { + this.currentWidth = innnerWidth + } + if (this.currentHeight !== innnerHeight) { + this.currentHeight = innnerHeight + } + } } private setupFpsTracking () { @@ -81,20 +118,15 @@ export class DocumentRenderer { }, 1000) } - // private handleResize () { - // const width = window.innerWidth - // const height = window.innerHeight - - // viewer.camera.aspect = width / height - // viewer.camera.updateProjectionMatrix() - // this.renderer.setSize(width, height) - // viewer.world.handleResize() - // } - private startRenderLoop () { const animate = () => { if (this.disconnected) return - this.animationFrameId = requestAnimationFrame(animate) + + if (this.config.timeoutRendering) { + this.timeoutId = setTimeout(animate, this.config.fpsLimit ? 1000 / this.config.fpsLimit : 0) as unknown as number + } else { + this.animationFrameId = requestAnimationFrame(animate) + } if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return @@ -112,18 +144,19 @@ export class DocumentRenderer { } let sizeChanged = false - if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) { - this.previousWindowWidth = window.innerWidth - this.previousWindowHeight = window.innerHeight - this.updateSize() + this.updateCanvasSize() + if (this.previousCanvasWidth !== this.currentWidth || this.previousCanvasHeight !== this.currentHeight) { + this.previousCanvasWidth = this.currentWidth + this.previousCanvasHeight = this.currentHeight + this.sizeUpdated() sizeChanged = true } this.frameRender(sizeChanged) - // Update stats visibility each frame + // Update stats visibility each frame (main thread only) if (this.config.statsVisible !== undefined) { - this.stats.setVisibility(this.config.statsVisible) + this.stats?.setVisibility(this.config.statsVisible) } } @@ -132,16 +165,16 @@ export class DocumentRenderer { frameRender (sizeChanged: boolean) { this.preRender() - this.stats.markStart() + this.stats?.markStart() tween.update() - if (!window.freezeRender) { + if (!globalThis.freezeRender) { this.render(sizeChanged) } for (const fn of this.onRender) { fn(sizeChanged) } this.renderedFps++ - this.stats.markEnd() + this.stats?.markEnd() this.postRender() } @@ -154,10 +187,15 @@ export class DocumentRenderer { if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId) } - this.canvas.remove() - this.renderer.dispose() + if (this.timeoutId) { + clearTimeout(this.timeoutId) + } + if (this.canvas instanceof HTMLCanvasElement) { + this.canvas.remove() + } clearInterval(this.fpsInterval) - this.stats.dispose() + this.stats?.dispose() + this.renderer.dispose() } } @@ -250,3 +288,40 @@ class TopRightStats { this.statsGl.container.remove() } } + +const addCanvasToPage = () => { + const canvas = document.createElement('canvas') + canvas.id = 'viewer-canvas' + document.body.appendChild(canvas) + return canvas +} + +export const addCanvasForWorker = () => { + const canvas = addCanvasToPage() + const transferred = canvas.transferControlToOffscreen() + let removed = false + let onSizeChanged = (w, h) => { } + let oldSize = { width: 0, height: 0 } + const checkSize = () => { + if (removed) return + if (oldSize.width !== window.innerWidth || oldSize.height !== window.innerHeight) { + onSizeChanged(window.innerWidth, window.innerHeight) + oldSize = { width: window.innerWidth, height: window.innerHeight } + } + requestAnimationFrame(checkSize) + } + requestAnimationFrame(checkSize) + return { + canvas: transferred, + destroy () { + removed = true + canvas.remove() + }, + onSizeChanged (cb: (width: number, height: number) => void) { + onSizeChanged = cb + }, + get size () { + return { width: window.innerWidth, height: window.innerHeight } + } + } +} diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 8a270332..0e8e6384 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -96,7 +96,7 @@ function getUsernameTexture ({ nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255 }: any, { fontFamily = 'sans-serif' }: any) { - const canvas = document.createElement('canvas') + const canvas = new OffscreenCanvas(64, 64) const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Could not get 2d context') @@ -173,7 +173,7 @@ const nametags = {} const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase() -function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos: any; name: any }, world: WorldRendererThree | undefined, options: { fontFamily: string }, overrides) { +function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree | undefined, options: { fontFamily: string }, overrides) { if (entity.name) { try { // https://github.com/PrismarineJS/prismarine-viewer/pull/410 @@ -209,6 +209,7 @@ export type SceneEntity = THREE.Object3D & { username?: string uuid?: string additionalCleanup?: () => void + originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name } } export class Entities { @@ -250,6 +251,7 @@ export class Entities { constructor (public worldRenderer: WorldRendererThree) { this.debugMode = 'none' this.onSkinUpdate = () => { } + this.watchResourcesUpdates() } clear () { @@ -260,6 +262,20 @@ export class Entities { this.entities = {} } + reloadEntities () { + for (const entity of Object.values(this.entities)) { + // update all entities textures like held items, armour, etc + // todo update entity textures itself + this.update({ ...entity.originalEntity, delete: true, } as SceneEntity['originalEntity'], {}) + this.update(entity.originalEntity, {}) + } + } + + watchResourcesUpdates () { + this.worldRenderer.resourcesManager.on('assetsTexturesUpdated', () => this.reloadEntities()) + this.worldRenderer.resourcesManager.on('assetsInventoryReady', () => this.reloadEntities()) + } + setDebugMode (mode: string, entity: THREE.Object3D | null = null) { this.debugMode = mode for (const mesh of entity ? [entity] : Object.values(this.entities)) { @@ -291,7 +307,7 @@ export class Entities { const dt = this.clock.getDelta() const botPos = this.worldRenderer.viewerPosition - const VISIBLE_DISTANCE = 8 * 8 + const VISIBLE_DISTANCE = 10 * 10 for (const entityId of Object.keys(this.entities)) { const entity = this.entities[entityId] @@ -312,13 +328,8 @@ export class Entities { const dz = entity.position.z - botPos.z const distanceSquared = dx * dx + dy * dy + dz * dz - // Get chunk coordinates - const chunkX = Math.floor(entity.position.x / 16) * 16 - const chunkZ = Math.floor(entity.position.z / 16) * 16 - const chunkKey = `${chunkX},${chunkZ}` - - // Entity is visible if within 16 blocks OR in a finished chunk - entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.finishedChunks[chunkKey]) + // Entity is visible if within 20 blocks OR in a finished chunk + entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity)) this.maybeRenderPlayerSkin(entityId) } @@ -467,16 +478,16 @@ export class Entities { if (!playerObject) return try { - let playerCustomSkinImage: HTMLImageElement | undefined + let playerCustomSkinImage: ImageBitmap | undefined playerObject = this.getPlayerObject(entityId) if (!playerObject) return let skinTexture: THREE.Texture - let skinCanvas: HTMLCanvasElement + let skinCanvas: OffscreenCanvas if (skinUrl === stevePngUrl) { skinTexture = await steveTexture - const canvas = document.createElement('canvas') + const canvas = new OffscreenCanvas(64, 64) const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Failed to get context') ctx.drawImage(skinTexture.image, 0, 0) @@ -550,6 +561,12 @@ export class Entities { } } + debugSwingArm () { + const playerObject = Object.values(this.entities).find(entity => entity.playerObject?.animation instanceof WalkingGeneralSwing) + if (!playerObject) return + (playerObject.playerObject!.animation as WalkingGeneralSwing).swingArm() + } + playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') { const playerObject = this.getPlayerObject(entityPlayerId) if (!playerObject) return @@ -594,7 +611,7 @@ export class Entities { if (previousModel && previousModel === textureUv?.modelName) return undefined if (textureUv && 'resolvedModel' in textureUv) { - const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources!.worldBlockProvider) + const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources.worldBlockProvider!) let SCALE = 1 if (specificProps['minecraft:display_context'] === 'ground') { SCALE = 0.5 @@ -675,7 +692,7 @@ export class Entities { } } - update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) { + update (entity: SceneEntity['originalEntity'], overrides) { const justAdded = !this.entities[entity.id] const isPlayerModel = entity.name === 'player' @@ -703,9 +720,10 @@ export class Entities { return } - let mesh + let mesh: THREE.Object3D | undefined if (e === undefined) { - const group = new THREE.Group() + const group = new THREE.Group() as unknown as SceneEntity + group.originalEntity = entity if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block') { const item = entity.name === 'tnt' ? { name: 'tnt' } @@ -732,7 +750,7 @@ export class Entities { if (entity.name === 'item') { mesh.onBeforeRender = () => { const delta = clock.getDelta() - mesh.rotation.y += delta + mesh!.rotation.y += delta } } @@ -756,7 +774,6 @@ export class Entities { // } // } - //@ts-expect-error group.additionalCleanup = () => { // important: avoid texture memory leak and gpu slowdown object.itemsTexture?.dispose() @@ -795,7 +812,6 @@ export class Entities { wrapper.add(nameTag) } - //@ts-expect-error group.playerObject = playerObject wrapper.rotation.set(0, Math.PI, 0) mesh = wrapper @@ -808,7 +824,8 @@ export class Entities { if (!mesh) return mesh.name = 'mesh' // set initial position so there are no weird jumps update after - group.position.set(entity.pos.x, entity.pos.y, entity.pos.z) + const pos = entity.pos ?? entity.position + group.position.set(pos.x, pos.y, pos.z) // todo use width and height instead const boxHelper = new THREE.BoxHelper( @@ -856,7 +873,7 @@ export class Entities { //@ts-expect-error // set visibility const isInvisible = entity.metadata?.[0] & 0x20 - for (const child of mesh.children ?? []) { + for (const child of mesh!.children ?? []) { if (child.name !== 'nametag') { child.visible = !isInvisible } @@ -895,8 +912,8 @@ export class Entities { const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0 const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0 const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0 - mesh.castShadow = !isMarker - mesh.receiveShadow = !isMarker + mesh!.castShadow = !isMarker + mesh!.receiveShadow = !isMarker if (isSmall) { e.scale.set(0.5, 0.5, 0.5) } else { @@ -965,7 +982,9 @@ export class Entities { // TODO: fix type // todo! fix errors in mc-data (no entities data prior 1.18.2) const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } } - mesh.scale.set(1, 1, 1) + mesh!.scale.set(1, 1, 1) + mesh!.position.set(0, 0, -0.5) + e.rotation.x = -entity.pitch e.children.find(c => { if (c.name.startsWith('map_')) { @@ -982,25 +1001,33 @@ export class Entities { } return false })?.removeFromParent() + if (item && (item.itemId ?? item.blockId ?? 0) !== 0) { + // Get rotation from metadata, default to 0 if not present + // Rotation is stored in 45° increments (0-7) for items, 90° increments (0-3) for maps const rotation = (itemFrameMeta.rotation as any as number) ?? 0 const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data if (mapNumber) { // TODO: Use proper larger item frame model when a map exists - mesh.scale.set(16 / 12, 16 / 12, 1) + mesh!.scale.set(16 / 12, 16 / 12, 1) + // Handle map rotation (4 possibilities, 90° increments) this.addMapModel(e, mapNumber, rotation) } else { + // Handle regular item rotation (8 possibilities, 45° increments) const itemMesh = this.getItemMesh(item, { 'minecraft:display_context': 'fixed', }) if (itemMesh) { - itemMesh.mesh.position.set(0, 0, 0.43) + itemMesh.mesh.position.set(0, 0, -0.05) + // itemMesh.mesh.position.set(0, 0, 0.43) if (itemMesh.isBlock) { itemMesh.mesh.scale.set(0.25, 0.25, 0.25) } else { itemMesh.mesh.scale.set(0.5, 0.5, 0.5) } + // Rotate 180° around Y axis first itemMesh.mesh.rotateY(Math.PI) + // Then apply the 45° increment rotation itemMesh.mesh.rotateZ(-rotation * Math.PI / 4) itemMesh.mesh.name = 'item' e.add(itemMesh.mesh) @@ -1115,6 +1142,7 @@ export class Entities { } else { mapMesh.position.set(0, 0, 0.437) } + // Apply 90° increment rotation for maps (0-3) mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2) mapMesh.name = `map_${mapNumber}` @@ -1267,7 +1295,7 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj if (!texturePath) { // TODO: Support mirroring on certain parts of the model const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}` - texturePath = worldRenderer.resourcesManager.currentResources!.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName] + texturePath = worldRenderer.resourcesManager.currentResources.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName] } if (!texturePath || !armorModel[slotType]) { removeArmorModel(entityMesh, slotType) diff --git a/renderer/viewer/three/entity/EntityMesh.ts b/renderer/viewer/three/entity/EntityMesh.ts index 2db09dd5..454bf35c 100644 --- a/renderer/viewer/three/entity/EntityMesh.ts +++ b/renderer/viewer/three/entity/EntityMesh.ts @@ -238,10 +238,11 @@ export function getMesh ( if (useBlockTexture) { if (!worldRenderer) throw new Error('worldRenderer is required for block textures') const blockName = texture.slice(6) - const textureInfo = worldRenderer.resourcesManager.currentResources!.blocksAtlasParser.getTextureInfo(blockName) + const textureInfo = worldRenderer.resourcesManager.currentResources.blocksAtlasJson.textures[blockName] if (textureInfo) { textureWidth = blocksTexture?.image.width ?? textureWidth textureHeight = blocksTexture?.image.height ?? textureHeight + // todo support su/sv textureOffset = [textureInfo.u, textureInfo.v] } else { console.error(`Unknown block ${blockName}`) @@ -546,4 +547,4 @@ export class EntityMesh { } } } -window.EntityMesh = EntityMesh +globalThis.EntityMesh = EntityMesh diff --git a/renderer/viewer/three/graphicsBackend.ts b/renderer/viewer/three/graphicsBackend.ts index 01bb7c56..5ea89b34 100644 --- a/renderer/viewer/three/graphicsBackend.ts +++ b/renderer/viewer/three/graphicsBackend.ts @@ -5,6 +5,7 @@ import { ProgressReporter } from '../../../src/core/progressReporter' import { showNotification } from '../../../src/react/NotificationProvider' import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug' import supportedVersions from '../../../src/supportedVersions.mjs' +import { ResourcesManager } from '../../../src/resourcesManager' import { WorldRendererThree } from './worldrendererThree' import { DocumentRenderer } from './documentRenderer' import { PanoramaRenderer } from './panorama' @@ -12,7 +13,7 @@ import { initVR } from './world/vr' // https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791 THREE.ColorManagement.enabled = false -window.THREE = THREE +globalThis.THREE = THREE const getBackendMethods = (worldRenderer: WorldRendererThree) => { return { @@ -24,7 +25,7 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => { updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities), changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer), getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer), - rerenderAllChunks: worldRenderer.rerenderAllChunks.bind(worldRenderer), + reloadWorld: worldRenderer.reloadWorld.bind(worldRenderer), addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media), destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media), @@ -57,31 +58,27 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO let worldRenderer: WorldRendererThree | null = null const startPanorama = async () => { + if (!documentRenderer) throw new Error('Document renderer not initialized') if (worldRenderer) return - const qs = new URLSearchParams(window.location.search) + const qs = new URLSearchParams(location.search) if (qs.get('debugEntities')) { - initOptions.resourcesManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true } - await initOptions.resourcesManager.updateAssetsData({ }) + const fullResourceManager = initOptions.resourcesManager as ResourcesManager + fullResourceManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true } + await fullResourceManager.updateAssetsData({ }) - displayEntitiesDebugList(initOptions.resourcesManager.currentConfig.version) + displayEntitiesDebugList(fullResourceManager.currentConfig.version) return } if (!panoramaRenderer) { panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE) - window.panoramaRenderer = panoramaRenderer + globalThis.panoramaRenderer = panoramaRenderer callModsMethod('panoramaCreated', panoramaRenderer) await panoramaRenderer.start() callModsMethod('panoramaReady', panoramaRenderer) } } - let version = '' - const prepareResources = async (ver: string, progressReporter: ProgressReporter): Promise => { - version = ver - await initOptions.resourcesManager.updateAssetsData({ }) - } - const startWorld = async (displayOptions: DisplayWorldOptions) => { if (panoramaRenderer) { panoramaRenderer.dispose() diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts index 6836d4f0..422076f0 100644 --- a/renderer/viewer/three/holdingBlock.ts +++ b/renderer/viewer/three/holdingBlock.ts @@ -5,7 +5,7 @@ import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBloc import { BlockModel } from 'mc-assets' import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer' import { getMyHand } from '../lib/hand' -import { IPlayerState, MovementState } from '../lib/basePlayerState' +import { MovementState, PlayerStateRenderer } from '../lib/basePlayerState' import { DebugGui } from '../lib/DebugGui' import { SmoothSwitcher } from '../lib/smoothSwitcher' import { watchProperty } from '../lib/utils/proxy' @@ -116,16 +116,22 @@ export default class HoldingBlock { offHandModeLegacy = false swingAnimator: HandSwingAnimator | undefined - playerState: IPlayerState + playerState: PlayerStateRenderer config: WorldRendererConfig constructor (public worldRenderer: WorldRendererThree, public offHand = false) { this.initCameraGroup() this.playerState = worldRenderer.displayOptions.playerState - this.playerState.events.on('heldItemChanged', (_, isOffHand) => { - if (this.offHand !== isOffHand) return - this.updateItem() - }) + this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => { + if (!this.offHand) { + this.updateItem() + } + }, false) + this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => { + if (this.offHand) { + this.updateItem() + } + }, false) this.config = worldRenderer.displayOptions.inWorldRenderingConfig this.offHandDisplay = this.offHand @@ -134,17 +140,21 @@ export default class HoldingBlock { // load default hand void getMyHand().then((hand) => { this.playerHand = hand + // trigger update + this.updateItem() }).then(() => { // now watch over the player skin watchProperty( async () => { - return getMyHand(this.playerState.reactive.playerSkin, this.playerState.onlineMode ? this.playerState.username : undefined) + return getMyHand(this.playerState.reactive.playerSkin, this.playerState.reactive.onlineMode ? this.playerState.reactive.username : undefined) }, this.playerState.reactive, 'playerSkin', (newHand) => { if (newHand) { this.playerHand = newHand + // trigger update + this.updateItem() } }, (oldHand) => { @@ -156,8 +166,8 @@ export default class HoldingBlock { } updateItem () { - if (!this.ready || !this.playerState.getHeldItem) return - const item = this.playerState.getHeldItem(this.offHand) + if (!this.ready) return + const item = this.offHand ? this.playerState.reactive.heldItemOff : this.playerState.reactive.heldItemMain if (item) { void this.setNewItem(item) } else if (this.offHand) { @@ -347,8 +357,8 @@ export default class HoldingBlock { itemId: handItem.id, }, { 'minecraft:display_context': 'firstperson', - 'minecraft:use_duration': this.playerState.getItemUsageTicks?.(), - 'minecraft:using_item': !!this.playerState.getItemUsageTicks?.(), + 'minecraft:use_duration': this.playerState.reactive.itemUsageTicks, + 'minecraft:using_item': !!this.playerState.reactive.itemUsageTicks, }, this.lastItemModelName) if (result) { const { mesh: itemMesh, isBlock, modelName } = result @@ -546,7 +556,7 @@ class HandIdleAnimator { private readonly debugGui: DebugGui - constructor (public handMesh: THREE.Object3D, public playerState: IPlayerState) { + constructor (public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) { this.handMesh = handMesh this.globalTime = 0 this.currentState = 'NOT_MOVING' @@ -700,7 +710,7 @@ class HandIdleAnimator { // Check for state changes from player state if (this.playerState) { - const newState = this.playerState.getMovementState() + const newState = this.playerState.reactive.movementState if (newState !== this.targetState) { this.setState(newState) } diff --git a/renderer/viewer/three/panorama.ts b/renderer/viewer/three/panorama.ts index ba1c4ac2..58a0fffa 100644 --- a/renderer/viewer/three/panorama.ts +++ b/renderer/viewer/three/panorama.ts @@ -6,8 +6,10 @@ import * as tweenJs from '@tweenjs/tween.js' import type { GraphicsInitOptions } from '../../../src/appViewer' import { WorldDataEmitter } from '../lib/worldDataEmitter' import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon' -import { BasePlayerState } from '../lib/basePlayerState' import { getDefaultRendererState } from '../baseGraphicsBackend' +import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from '../lib/utils/skins' +import { ResourcesManager } from '../../../src/resourcesManager' +import { getInitialPlayerStateRenderer } from '../lib/basePlayerState' import { WorldRendererThree } from './worldrendererThree' import { EntityMesh } from './entity/EntityMesh' import { DocumentRenderer } from './documentRenderer' @@ -48,7 +50,7 @@ export class PanoramaRenderer { this.directionalLight.castShadow = true this.scene.add(this.directionalLight) - this.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000) + this.camera = new THREE.PerspectiveCamera(85, this.documentRenderer.canvas.width / this.documentRenderer.canvas.height, 0.05, 1000) this.camera.position.set(0, 0, 0) this.camera.rotation.set(0, 0, 0) } @@ -63,47 +65,57 @@ export class PanoramaRenderer { this.documentRenderer.render = (sizeChanged = false) => { if (sizeChanged) { - this.camera.aspect = window.innerWidth / window.innerHeight + this.camera.aspect = this.documentRenderer.canvas.width / this.documentRenderer.canvas.height this.camera.updateProjectionMatrix() } this.documentRenderer.renderer.render(this.scene, this.camera) } } + async debugImageInFrontOfCamera () { + const image = await loadThreeJsTextureFromUrl(join('background', 'panorama_0.png')) + const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), new THREE.MeshBasicMaterial({ map: image })) + mesh.position.set(0, 0, -500) + mesh.rotation.set(0, 0, 0) + this.scene.add(mesh) + } + addClassicPanorama () { const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000) - const loader = new THREE.TextureLoader() const panorMaterials = [] as THREE.MeshBasicMaterial[] const fadeInDuration = 200 - for (const file of panoramaFiles) { - // eslint-disable-next-line prefer-const - let material: THREE.MeshBasicMaterial + // void this.debugImageInFrontOfCamera() + + for (const file of panoramaFiles) { + const load = async () => { + const { texture } = loadThreeJsTextureFromUrlSync(join('background', file)) + + // Instead of using repeat/offset to flip, we'll use the texture matrix + texture.matrixAutoUpdate = false + texture.matrix.set( + -1, 0, 1, 0, 1, 0, 0, 0, 1 + ) + + texture.wrapS = THREE.ClampToEdgeWrapping + texture.wrapT = THREE.ClampToEdgeWrapping + texture.minFilter = THREE.LinearFilter + texture.magFilter = THREE.LinearFilter + + const material = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + opacity: 0 // Start with 0 opacity + }) - const texture = loader.load(join('background', file), () => { // Start fade-in when texture is loaded this.startTimes.set(material, Date.now()) - }) + panorMaterials.push(material) + } - // Instead of using repeat/offset to flip, we'll use the texture matrix - texture.matrixAutoUpdate = false - texture.matrix.set( - -1, 0, 1, 0, 1, 0, 0, 0, 1 - ) - - texture.wrapS = THREE.ClampToEdgeWrapping - texture.wrapT = THREE.ClampToEdgeWrapping - texture.minFilter = THREE.LinearFilter - texture.magFilter = THREE.LinearFilter - - material = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - side: THREE.DoubleSide, - depthWrite: false, - opacity: 0 // Start with 0 opacity - }) - panorMaterials.push(material) + void load() } const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials) @@ -145,8 +157,9 @@ export class PanoramaRenderer { async worldBlocksPanorama () { const version = '1.21.4' - this.options.resourcesManager.currentConfig = { version, noInventoryGui: true, } - await this.options.resourcesManager.updateAssetsData({ }) + const fullResourceManager = this.options.resourcesManager as ResourcesManager + fullResourceManager.currentConfig = { version, noInventoryGui: true, } + await fullResourceManager.updateAssetsData({ }) if (this.abortController.signal.aborted) return console.time('load panorama scene') const world = getSyncWorld(version) @@ -184,9 +197,9 @@ export class PanoramaRenderer { version, worldView, inWorldRenderingConfig: defaultWorldRendererConfig, - playerState: new BasePlayerState(), - rendererState: getDefaultRendererState(), - nonReactiveState: getDefaultRendererState() + playerState: getInitialPlayerStateRenderer(), + rendererState: getDefaultRendererState().reactive, + nonReactiveState: getDefaultRendererState().nonReactive } ) if (this.worldRenderer instanceof WorldRendererThree) { diff --git a/renderer/viewer/three/renderSlot.ts b/renderer/viewer/three/renderSlot.ts new file mode 100644 index 00000000..fd1eae91 --- /dev/null +++ b/renderer/viewer/three/renderSlot.ts @@ -0,0 +1,78 @@ +import { getRenamedData } from 'flying-squid/dist/blockRenames' +import { BlockModel } from 'mc-assets' +import { versionToNumber } from 'mc-assets/dist/utils' +import type { ResourcesManagerCommon } from '../../../src/resourcesManager' + +export type ResolvedItemModelRender = { + modelName: string, + originalItemName?: string +} + +export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: ResourcesManagerCommon, debugIsQuickbar = false, fullBlockModelSupport = false): { + texture: string, + blockData?: Record & { resolvedModel: BlockModel }, + scale?: number, + slice?: number[], + modelName?: string, + image?: ImageBitmap +} | undefined => { + let itemModelName = model.modelName + const isItem = loadedData.itemsByName[itemModelName] + + // #region normalize item name + if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string + // #endregion + + + let itemTexture + + if (!fullBlockModelSupport) { + const atlas = resourcesManager.currentResources?.guiAtlas?.json + // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) + const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')] + const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName) + if (item) { + const x = item.u * atlas.width + const y = item.v * atlas.height + return { + texture: 'gui', + image: resourcesManager.currentResources!.guiAtlas!.image, + slice: [x, y, atlas.tileSize, atlas.tileSize], + scale: 0.25, + } + } + } + + const blockToTopTexture = (r) => r.top ?? r + + try { + if (!appViewer.resourcesManager.currentResources?.itemsRenderer) throw new Error('Items renderer is not available') + itemTexture = + appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) + ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined) + ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')! + } catch (err) { + // get resourcepack from resource manager + reportError?.(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: TODO!): ${err.stack}`) + itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!) + } + + itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!) + + + if ('type' in itemTexture) { + // is item + return { + texture: itemTexture.type, + slice: itemTexture.slice, + modelName: itemModelName + } + } else { + // is block + return { + texture: 'blocks', + blockData: itemTexture, + modelName: itemModelName + } + } +} diff --git a/renderer/viewer/three/world/cursorBlock.ts b/renderer/viewer/three/world/cursorBlock.ts index fe95c2c9..495a2d55 100644 --- a/renderer/viewer/three/world/cursorBlock.ts +++ b/renderer/viewer/three/world/cursorBlock.ts @@ -15,6 +15,7 @@ import destroyStage6 from '../../../../assets/destroy_stage_6.png' import destroyStage7 from '../../../../assets/destroy_stage_7.png' import destroyStage8 from '../../../../assets/destroy_stage_8.png' import destroyStage9 from '../../../../assets/destroy_stage_9.png' +import { loadThreeJsTextureFromUrl } from '../../lib/utils/skins' export class CursorBlock { _cursorLinesHidden = false @@ -36,17 +37,17 @@ export class CursorBlock { constructor (public readonly worldRenderer: WorldRendererThree) { // Initialize break mesh and textures - const loader = new THREE.TextureLoader() const destroyStagesImages = [ destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4, destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9 ] for (let i = 0; i < 10; i++) { - const texture = loader.load(destroyStagesImages[i]) - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter - this.breakTextures.push(texture) + void loadThreeJsTextureFromUrl(destroyStagesImages[i]).then((texture) => { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + this.breakTextures.push(texture) + }) } const breakMaterial = new THREE.MeshBasicMaterial({ diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index a8e08068..78381a29 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -3,17 +3,16 @@ import { Vec3 } from 'vec3' import nbt from 'prismarine-nbt' import PrismarineChatLoader from 'prismarine-chat' import * as tweenJs from '@tweenjs/tween.js' -import { subscribeKey } from 'valtio/utils' import { renderSign } from '../sign-renderer' -import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer' +import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer' import { chunkPos, sectionPos } from '../lib/simpleUtils' import { WorldRendererCommon } from '../lib/worldrendererCommon' -import { addNewStat, removeAllStats } from '../lib/ui/newStats' +import { addNewStat } from '../lib/ui/newStats' import { MesherGeometryOutput } from '../lib/mesher/shared' import { ItemSpecificContextProperties } from '../lib/basePlayerState' import { getMyHand } from '../lib/hand' import { setBlockPosition } from '../lib/mesher/standaloneRenderer' -import { sendVideoPlay, sendVideoStop } from '../../../src/customChannels' +import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins' import HoldingBlock from './holdingBlock' import { getMesh } from './entity/EntityMesh' import { armorModel } from './entity/armorModels' @@ -44,7 +43,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraGroupVr?: THREE.Object3D material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) itemsTexture: THREE.Texture - cursorBlock = new CursorBlock(this) + cursorBlock: CursorBlock onRender: Array<() => void> = [] cameraShake: CameraShake media: ThreeJsMedia @@ -82,8 +81,10 @@ export class WorldRendererThree extends WorldRendererCommon { if (!initOptions.resourcesManager) throw new Error('resourcesManager is required') super(initOptions.resourcesManager, displayOptions, initOptions) + this.renderer = renderer displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...' this.starField = new StarField(this.scene) + this.cursorBlock = new CursorBlock(this) this.holdingBlock = new HoldingBlock(this) this.holdingBlockLeft = new HoldingBlock(this, true) @@ -148,21 +149,21 @@ export class WorldRendererThree extends WorldRendererCommon { override watchReactivePlayerState () { super.watchReactivePlayerState() - this.onReactiveValueUpdated('inWater', (value) => { + this.onReactivePlayerStateUpdated('inWater', (value) => { this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null }) - this.onReactiveValueUpdated('ambientLight', (value) => { + this.onReactivePlayerStateUpdated('ambientLight', (value) => { if (!value) return this.ambientLight.intensity = value }) - this.onReactiveValueUpdated('directionalLight', (value) => { + this.onReactivePlayerStateUpdated('directionalLight', (value) => { if (!value) return this.directionalLight.intensity = value }) - this.onReactiveValueUpdated('lookingAtBlock', (value) => { + this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => { this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes) }) - this.onReactiveValueUpdated('diggingBlock', (value) => { + this.onReactivePlayerStateUpdated('diggingBlock', (value) => { this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape) }) } @@ -184,20 +185,18 @@ export class WorldRendererThree extends WorldRendererCommon { } async updateAssetsData (): Promise { - const resources = this.resourcesManager.currentResources! + const resources = this.resourcesManager.currentResources const oldTexture = this.material.map const oldItemsTexture = this.itemsTexture - const texture = await new THREE.TextureLoader().loadAsync(resources.blocksAtlasParser.latestImage) - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter + const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage) + texture.needsUpdate = true texture.flipY = false this.material.map = texture - const itemsTexture = await new THREE.TextureLoader().loadAsync(resources.itemsAtlasParser.latestImage) - itemsTexture.magFilter = THREE.NearestFilter - itemsTexture.minFilter = THREE.NearestFilter + const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage) + itemsTexture.needsUpdate = true itemsTexture.flipY = false this.itemsTexture = itemsTexture @@ -239,7 +238,7 @@ export class WorldRendererThree extends WorldRendererCommon { } getItemRenderData (item: Record, specificProps: ItemSpecificContextProperties) { - return getItemUv(item, specificProps, this.resourcesManager) + return getItemUv(item, specificProps, this.resourcesManager, this.playerState) } async demoModel () { @@ -431,7 +430,7 @@ export class WorldRendererThree extends WorldRendererCommon { } setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { - const yOffset = this.displayOptions.playerState.getEyeHeight() + const yOffset = this.displayOptions.playerState.reactive.eyeHeight this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch) this.media.tryIntersectMedia() @@ -495,7 +494,8 @@ export class WorldRendererThree extends WorldRendererCommon { const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov if (sizeOrFovChanged) { - this.camera.aspect = window.innerWidth / window.innerHeight + const size = this.renderer.getSize(new THREE.Vector2()) + this.camera.aspect = size.width / size.height this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov this.camera.updateProjectionMatrix() } @@ -508,7 +508,7 @@ export class WorldRendererThree extends WorldRendererCommon { const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera this.renderer.render(this.scene, cam) - if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { + if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerState.reactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) { this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight) } @@ -710,6 +710,18 @@ export class WorldRendererThree extends WorldRendererCommon { super.destroy() } + shouldObjectVisible (object: THREE.Object3D) { + // Get chunk coordinates + const chunkX = Math.floor(object.position.x / 16) * 16 + const chunkZ = Math.floor(object.position.z / 16) * 16 + const sectionY = Math.floor(object.position.y / 16) * 16 + + const chunkKey = `${chunkX},${chunkZ}` + const sectionKey = `${chunkX},${sectionY},${chunkZ}` + + return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey] + } + updateSectionOffsets () { const currentTime = performance.now() for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) { @@ -756,6 +768,10 @@ export class WorldRendererThree extends WorldRendererCommon { } } } + + reloadWorld () { + this.entities.reloadEntities() + } } class StarField { diff --git a/src/appViewer.ts b/src/appViewer.ts index f4a21481..5ac0beaf 100644 --- a/src/appViewer.ts +++ b/src/appViewer.ts @@ -1,25 +1,26 @@ -import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter' -import { BasePlayerState, IPlayerState } from 'renderer/viewer/lib/basePlayerState' +import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter' +import { PlayerStateRenderer } 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 { proxy, subscribe } from 'valtio' import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend' import { getSyncWorld } from 'renderer/playground/shared' +import { MaybePromise } from 'contro-max/build/types/store' 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 { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager' import { watchOptionsAfterWorldViewInit } from './watchOptions' export interface RendererReactiveState { world: { chunksLoaded: Set + // chunksTotalNumber: number heightmaps: Map - chunksTotalNumber: number allChunksLoaded: boolean mesherWork: boolean intersectMedia: { id: string, x: number, y: number } | null @@ -31,9 +32,6 @@ export interface NonReactiveState { world: { chunksLoaded: Set chunksTotalNumber: number - allChunksLoaded: boolean - mesherWork: boolean - intersectMedia: { id: string, x: number, y: number } | null } } @@ -42,33 +40,39 @@ export interface GraphicsBackendConfig { powerPreference?: 'high-performance' | 'low-power' statsVisible?: number sceneBackground: string + timeoutRendering?: boolean } const defaultGraphicsBackendConfig: GraphicsBackendConfig = { fpsLimit: undefined, powerPreference: undefined, - sceneBackground: 'lightblue' + sceneBackground: 'lightblue', + timeoutRendering: false } export interface GraphicsInitOptions { - resourcesManager: ResourcesManager + resourcesManager: ResourcesManagerTransferred config: GraphicsBackendConfig rendererSpecificSettings: S - displayCriticalError: (error: Error) => void - setRendererSpecificSettings: (key: string, value: any) => void + callbacks: { + displayCriticalError: (error: Error) => void + setRendererSpecificSettings: (key: string, value: any) => void + + fireCustomEvent: (eventName: string, ...args: any[]) => void + } } export interface DisplayWorldOptions { version: string - worldView: WorldDataEmitter + worldView: WorldDataEmitterWorker inWorldRenderingConfig: WorldRendererConfig - playerState: IPlayerState + playerState: PlayerStateRenderer rendererState: RendererReactiveState nonReactiveState: NonReactiveState } -export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => GraphicsBackend) & { +export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => MaybePromise) & { id: string } @@ -108,8 +112,8 @@ export class AppViewer { inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig) lastCamUpdate = 0 playerState = playerState - rendererState = proxy(getDefaultRendererState()) - nonReactiveState: NonReactiveState = getDefaultRendererState() + rendererState = proxy(getDefaultRendererState().reactive) + nonReactiveState: NonReactiveState = getDefaultRendererState().nonReactive worldReady: Promise private resolveWorldReady: () => void @@ -133,19 +137,24 @@ export class AppViewer { rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key] } } - const loaderOptions: GraphicsInitOptions = { - resourcesManager: this.resourcesManager, + const loaderOptions: GraphicsInitOptions = { // todo! + resourcesManager: this.resourcesManager as ResourcesManagerTransferred, config: this.config, - displayCriticalError (error) { - console.error(error) - setLoadingScreenStatus(error.message, true) + callbacks: { + displayCriticalError (error) { + console.error(error) + setLoadingScreenStatus(error.message, true) + }, + setRendererSpecificSettings (key: string, value: any) { + options[`${rendererSettingsKey}.${key}`] = value + }, + fireCustomEvent (eventName, ...args) { + // this.callbacks.fireCustomEvent(eventName, ...args) + } }, rendererSpecificSettings, - setRendererSpecificSettings (key: string, value: any) { - options[`${rendererSettingsKey}.${key}`] = value - } } - this.backend = loader(loaderOptions) + this.backend = await loader(loaderOptions) // if (this.resourcesManager.currentResources) { // void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter()) @@ -156,9 +165,13 @@ export class AppViewer { const { method, args } = this.currentState this.backend[method](...args) if (method === 'startWorld') { + void this.worldView!.init(bot.entity.position) // void this.worldView!.init(args[0].playerState.getPosition()) } } + + // todo + modalStackUpdateChecks() } async startWithBot () { @@ -167,10 +180,10 @@ export class AppViewer { this.worldView!.listenToBot(bot) } - async startWorld (world, renderDistance: number, playerStateSend: IPlayerState = this.playerState) { + async startWorld (world, renderDistance: number, playerStateSend: PlayerStateRenderer = this.playerState) { if (this.currentDisplay === 'world') throw new Error('World already started') this.currentDisplay = 'world' - const startPosition = playerStateSend.getPosition() + const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0) this.worldView = new WorldDataEmitter(world, renderDistance, startPosition) window.worldView = this.worldView watchOptionsAfterWorldViewInit(this.worldView) @@ -238,7 +251,8 @@ export class AppViewer { const { promise, resolve } = Promise.withResolvers() this.worldReady = promise this.resolveWorldReady = resolve - this.rendererState = proxy(getDefaultRendererState()) + this.rendererState = proxy(getDefaultRendererState().reactive) + this.nonReactiveState = getDefaultRendererState().nonReactive // this.queuedDisplay = undefined } @@ -259,6 +273,7 @@ export class AppViewer { } } +// do not import this. Use global appViewer instead (without window prefix). export const appViewer = new AppViewer() window.appViewer = appViewer @@ -284,7 +299,7 @@ window.initialMenuStart = initialMenuStart const modalStackUpdateChecks = () => { // maybe start panorama - if (activeModalStack.length === 0 && !miscUiState.gameLoaded) { + if (!miscUiState.gameLoaded) { void initialMenuStart() } @@ -295,5 +310,4 @@ const modalStackUpdateChecks = () => { appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0 } -subscribeKey(activeModalStack, 'length', modalStackUpdateChecks) -modalStackUpdateChecks() +subscribe(activeModalStack, modalStackUpdateChecks) diff --git a/src/browserfs.ts b/src/browserfs.ts index a4ae96cc..006b6db8 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -263,7 +263,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string) return true } -export async function removeFileRecursiveAsync (path) { +export async function removeFileRecursiveAsync (path, removeDirectoryItself = true) { const errors = [] as Array<[string, Error]> try { const files = await fs.promises.readdir(path) @@ -282,7 +282,9 @@ export async function removeFileRecursiveAsync (path) { })) // After removing all files/directories, remove the current directory - await fs.promises.rmdir(path) + if (removeDirectoryItself) { + await fs.promises.rmdir(path) + } } catch (error) { errors.push([path, error]) } diff --git a/src/controls.ts b/src/controls.ts index f32cbda6..94364f2d 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -661,6 +661,9 @@ export const f3Keybinds: Array<{ localServer.players[0].world.columns = {} } void reloadChunks() + if (appViewer.backend?.backendMethods && typeof appViewer.backend.backendMethods.reloadWorld === 'function') { + appViewer.backend.backendMethods.reloadWorld() + } }, mobileTitle: 'Reload chunks', }, diff --git a/src/index.ts b/src/index.ts index bea10726..483abd74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,7 @@ import { showNotification } from './react/NotificationProvider' import { saveToBrowserMemory } from './react/PauseScreen' import './devReload' import './water' -import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect' +import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData, loadMinecraftData } from './connect' import { ref, subscribe } from 'valtio' import { signInMessageState } from './react/SignInMessageProvider' import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' @@ -331,6 +331,7 @@ export async function connect (connectOptions: ConnectOptions) { await progress.executeWithMessage( 'Processing downloaded Minecraft data', async () => { + await loadMinecraftData(version) await appViewer.resourcesManager.loadSourceData(version) } ) @@ -448,17 +449,20 @@ export async function connect (connectOptions: ConnectOptions) { let newTokensCacheResult = null as any const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {} - const authData = connectOptions.authenticatedAccount ? await microsoftAuthflow({ - tokenCaches: cachedTokens, - proxyBaseUrl: connectOptions.proxy, - setProgressText (text) { - progress.setMessage(text) - }, - setCacheResult (result) { - newTokensCacheResult = result - }, - connectingServer: server.host - }) : undefined + let authData: Awaited> | undefined + if (connectOptions.authenticatedAccount) { + authData = await microsoftAuthflow({ + tokenCaches: cachedTokens, + proxyBaseUrl: connectOptions.proxy, + setProgressText (text) { + progress.setMessage(text) + }, + setCacheResult (result) { + newTokensCacheResult = result + }, + connectingServer: server.host + }) + } if (p2pMultiplayer) { clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions) @@ -569,6 +573,7 @@ export async function connect (connectOptions: ConnectOptions) { // "mapDownloader-saveInternal": false, // do not save into memory, todo must be implemeneted as we do really care of ram }) as unknown as typeof __type_bot window.bot = bot + if (connectOptions.viewerWsConnect) { void onBotCreatedViewerHandler() } @@ -691,6 +696,7 @@ export async function connect (connectOptions: ConnectOptions) { onBotCreate() bot.once('login', () => { + errorAbortController.abort() loadingTimerState.networkOnlyStart = 0 progress.setMessage('Loading world') }) @@ -708,7 +714,7 @@ export async function connect (connectOptions: ConnectOptions) { resolve() unsub() } else { - const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.rendererState.world.chunksTotalNumber * 100) + const perc = Math.round(appViewer.rendererState.world.chunksLoaded.size / appViewer.nonReactiveState.world.chunksTotalNumber * 100) progress?.reportProgress('chunks', perc / 100) } }) @@ -727,9 +733,12 @@ export async function connect (connectOptions: ConnectOptions) { }) await appViewer.resourcesManager.promiseAssetsReady } - errorAbortController.abort() if (appStatusState.isError) return + if (!appViewer.resourcesManager.currentResources?.itemsRenderer) { + await appViewer.resourcesManager.updateAssetsData({}) + } + const loadWorldStart = Date.now() console.log('try to focus window') window.focus?.() @@ -741,7 +750,7 @@ export async function connect (connectOptions: ConnectOptions) { try { if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) - playerState.onlineMode = !!connectOptions.authenticatedAccount + playerState.reactive.onlineMode = !!connectOptions.authenticatedAccount progress.setMessage('Placing blocks (starting viewer)') if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) { @@ -765,6 +774,9 @@ export async function connect (connectOptions: ConnectOptions) { console.log('bot spawned - starting viewer') await appViewer.startWorld(bot.world, renderDistance) appViewer.worldView!.listenToBot(bot) + if (appViewer.backend) { + void appViewer.worldView!.init(bot.entity.position) + } initMotionTracking() dayCycle() @@ -975,7 +987,7 @@ if (!reconnectOptions) { } }) - if (appQueryParams.serversList) { + if (appQueryParams.serversList && !appQueryParams.ip) { showModal({ reactType: 'serversList' }) } diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 187bb997..d03b9fa4 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -10,7 +10,7 @@ import { versionToNumber } from 'renderer/viewer/common/utils' import { getRenamedData } from 'flying-squid/dist/blockRenames' import PrismarineChatLoader from 'prismarine-chat' import { BlockModel } from 'mc-assets' -import { activeGuiAtlas } from 'renderer/viewer/lib/guiRenderer' +import { renderSlot } from 'renderer/viewer/three/renderSlot' import Generic95 from '../assets/generic_95.png' import { appReplacableResources } from './generated/resources' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' @@ -21,6 +21,7 @@ import { currentScaling } from './scaleInterface' import { getItemDescription } from './itemsDescriptions' import { MessageFormatPart } from './chatUtils' import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' +import { playerState } from './mineflayer/playerState' const loadedImagesCache = new Map() const cleanLoadedImagesCache = () => { @@ -134,8 +135,8 @@ export const onGameLoad = () => { const getImageSrc = (path): string | HTMLImageElement => { switch (path) { case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content - case 'blocks': return appViewer.resourcesManager.currentResources!.blocksAtlasParser.latestImage - case 'items': return appViewer.resourcesManager.currentResources!.itemsAtlasParser.latestImage + case 'blocks': return appViewer.resourcesManager.blocksAtlasParser.latestImage + case 'items': return appViewer.resourcesManager.itemsAtlasParser.latestImage case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content case 'gui/container/furnace': return appReplacableResources.latest_gui_container_furnace.content case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.content @@ -177,79 +178,6 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined return loadedImagesCache.get(loadPath) } -export type ResolvedItemModelRender = { - modelName: string, - originalItemName?: string -} - -export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = false, fullBlockModelSupport = false): { - texture: string, - blockData?: Record & { resolvedModel: BlockModel }, - scale?: number, - slice?: number[], - modelName?: string, - image?: HTMLImageElement -} | undefined => { - let itemModelName = model.modelName - const isItem = loadedData.itemsByName[itemModelName] - - // #region normalize item name - if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string - // #endregion - - - let itemTexture - - if (!fullBlockModelSupport) { - const atlas = activeGuiAtlas.atlas?.json - // todo atlas holds all rendered blocks, not all possibly rendered item/block models, need to request this on demand instead (this is how vanilla works) - const tryGetAtlasTexture = (name?: string) => name && atlas?.textures[name.replace('minecraft:', '').replace('block/', '').replace('blocks/', '').replace('item/', '').replace('items/', '').replace('_inventory', '')] - const item = tryGetAtlasTexture(itemModelName) ?? tryGetAtlasTexture(model.originalItemName) - if (item) { - const x = item.u * atlas.width - const y = item.v * atlas.height - return { - texture: 'gui', - image: activeGuiAtlas.atlas!.image, - slice: [x, y, atlas.tileSize, atlas.tileSize], - scale: 0.25, - } - } - } - - const blockToTopTexture = (r) => r.top ?? r - - try { - assertDefined(appViewer.resourcesManager.currentResources?.itemsRenderer) - itemTexture = - appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) - ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined) - ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')! - } catch (err) { - inGameError(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`) - itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('errored')!) - } - - itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('unknown')!) - - - if ('type' in itemTexture) { - // is item - return { - texture: itemTexture.type, - slice: itemTexture.slice, - modelName: itemModelName - } - } else { - // is block - return { - texture: 'blocks', - blockData: itemTexture, - modelName: itemModelName - } - } -} - const getItemName = (slot: Item | RenderItem | null) => { const parsed = getItemNameRaw(slot, appViewer.resourcesManager) if (!parsed) return @@ -269,7 +197,7 @@ const itemToVisualKey = (slot: RenderItem | Item | null) => { slot['metadata'], slot.nbt ? JSON.stringify(slot.nbt) : '', slot['components'] ? JSON.stringify(slot['components']) : '', - activeGuiAtlas.version, + appViewer.resourcesManager.currentResources!.guiAtlasVersion, ].join('|') return keys } @@ -289,8 +217,8 @@ const mapSlots = (slots: Array, isJei = false) => { try { if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot - const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager) - const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, debugIsQuickbar) + const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, playerState) + const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, appViewer.resourcesManager, debugIsQuickbar) const itemCustomName = getItemName(slot) Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) //@ts-expect-error diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts index 8a4e1fcd..495c4f39 100644 --- a/src/mineflayer/items.ts +++ b/src/mineflayer/items.ts @@ -1,11 +1,10 @@ import mojangson from 'mojangson' import nbt from 'prismarine-nbt' import { fromFormattedString } from '@xmcl/text-component' -import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' +import { getItemSelector, ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' import { MessageFormatPart } from '../chatUtils' -import { ResourcesManager } from '../resourcesManager' -import { playerState } from './playerState' +import { ResourcesManager, ResourcesManagerCommon, ResourcesManagerTransferred } from '../resourcesManager' type RenderSlotComponent = { type: string, @@ -33,7 +32,7 @@ type PossibleItemProps = { display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"} } -export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManager) => { +export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManagerCommon) => { let customText = undefined as string | any | undefined let customModel = undefined as string | undefined @@ -91,7 +90,7 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour } -export const getItemNameRaw = (item: Pick | null, resourcesManager: ResourcesManager) => { +export const getItemNameRaw = (item: Pick | null, resourcesManager: ResourcesManagerCommon) => { if (!item) return '' const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager) if (!customText) return @@ -112,14 +111,14 @@ export const getItemNameRaw = (item: Pick } } -export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager) => { +export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerCommon, playerState: PlayerStateRenderer) => { let itemModelName = item.name const { customModel } = getItemMetadata(item, resourcesManager) if (customModel) { itemModelName = customModel } - const itemSelector = playerState.getItemSelector({ + const itemSelector = getItemSelector(playerState, { ...specificProps }) const modelFromDef = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, { diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index 16739c86..f80a6971 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -1,46 +1,26 @@ -import { EventEmitter } from 'events' -import { Vec3 } from 'vec3' -import { BasePlayerState, IPlayerState, ItemSpecificContextProperties, MovementState, PlayerStateEvents } from 'renderer/viewer/lib/basePlayerState' import { HandItemBlock } from 'renderer/viewer/three/holdingBlock' -import TypedEmitter from 'typed-emitter' -import { ItemSelector } from 'mc-assets/dist/itemDefinitions' -import { proxy } from 'valtio' +import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState' +import { subscribe } from 'valtio' +import { subscribeKey } from 'valtio/utils' import { gameAdditionalState } from '../globalState' -export class PlayerStateManager implements IPlayerState { +/** + * can be used only in main thread. Mainly for more convenient reactive state updates. + * In renderer/ directory, use PlayerStateControllerRenderer type or worldRenderer.playerState. + */ +export class PlayerStateControllerMain implements PlayerStateRenderer { disableStateUpdates = false - private static instance: PlayerStateManager - readonly events = new EventEmitter() as TypedEmitter - // Movement and physics state - private lastVelocity = new Vec3(0, 0, 0) - private movementState: MovementState = 'NOT_MOVING' private timeOffGround = 0 private lastUpdateTime = performance.now() // Held item state - private heldItem?: HandItemBlock - private offHandItem?: HandItemBlock - private itemUsageTicks = 0 private isUsingItem = false - private ready = false - public lightingDisabled = false - onlineMode = false - get username () { - return bot.username ?? '' - } + ready = false - reactive: IPlayerState['reactive'] = new BasePlayerState().reactive - - static getInstance (): PlayerStateManager { - if (!this.instance) { - this.instance = new PlayerStateManager() - } - return this.instance - } + reactive: PlayerStateRenderer['reactive'] constructor () { - this.updateState = this.updateState.bind(this) customEvents.on('mineflayerBotCreated', () => { this.ready = false bot.on('inject_allowed', () => { @@ -48,16 +28,27 @@ export class PlayerStateManager implements IPlayerState { this.ready = true this.botCreated() }) + bot.on('end', () => { + this.ready = false + }) }) } + private onBotCreatedOrGameJoined () { + this.reactive.username = bot.username ?? '' + } + private botCreated () { + console.log('bot created & plugins injected') + this.reactive = getInitialPlayerState() + this.onBotCreatedOrGameJoined() + const handleDimensionData = (data) => { let hasSkyLight = 1 try { hasSkyLight = data.dimension.value.has_skylight.value } catch {} - this.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight + this.reactive.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight } bot._client.on('login', (packet) => { @@ -68,7 +59,9 @@ export class PlayerStateManager implements IPlayerState { }) // Movement tracking - bot.on('move', this.updateState) + bot.on('move', () => { + this.updateMovementState() + }) // Item tracking bot.on('heldItemChanged', () => { @@ -77,8 +70,22 @@ export class PlayerStateManager implements IPlayerState { bot.inventory.on('updateSlot', (index) => { if (index === 45) this.updateHeldItem(true) }) + const updateSneakingOrFlying = () => { + this.updateMovementState() + this.reactive.sneaking = bot.controlState.sneak + this.reactive.flying = gameAdditionalState.isFlying + this.reactive.eyeHeight = bot.controlState.sneak && !gameAdditionalState.isFlying ? 1.27 : 1.62 + } bot.on('physicsTick', () => { - if (this.isUsingItem) this.itemUsageTicks++ + if (this.isUsingItem) this.reactive.itemUsageTicks++ + updateSneakingOrFlying() + }) + // todo move from gameAdditionalState to reactive directly + subscribeKey(gameAdditionalState, 'isSneaking', () => { + updateSneakingOrFlying() + }) + subscribeKey(gameAdditionalState, 'isFlying', () => { + updateSneakingOrFlying() }) // Initial held items setup @@ -89,14 +96,12 @@ export class PlayerStateManager implements IPlayerState { this.reactive.gameMode = bot.game.gameMode }) this.reactive.gameMode = bot.game?.gameMode - } - get shouldHideHand () { - return this.reactive.gameMode === 'spectator' + this.watchReactive() } // #region Movement and Physics State - private updateState () { + private updateMovementState () { if (!bot?.entity || this.disableStateUpdates) return const { velocity } = bot.entity @@ -109,7 +114,7 @@ export class PlayerStateManager implements IPlayerState { const deltaTime = now - this.lastUpdateTime this.lastUpdateTime = now - this.lastVelocity = velocity + // this.lastVelocity = velocity // Update time off ground if (isOnGround) { @@ -118,60 +123,26 @@ export class PlayerStateManager implements IPlayerState { this.timeOffGround += deltaTime } - if (this.isSneaking() || this.isFlying() || (this.timeOffGround > OFF_GROUND_THRESHOLD)) { - this.movementState = 'SNEAKING' + if (gameAdditionalState.isSneaking || gameAdditionalState.isFlying || (this.timeOffGround > OFF_GROUND_THRESHOLD)) { + this.reactive.movementState = 'SNEAKING' } else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) { - this.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY + this.reactive.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY ? 'SPRINTING' : 'WALKING' } else { - this.movementState = 'NOT_MOVING' + this.reactive.movementState = 'NOT_MOVING' } } - getMovementState (): MovementState { - return this.movementState - } - - getVelocity (): Vec3 { - return this.lastVelocity - } - - getEyeHeight (): number { - return bot.controlState.sneak && !this.isFlying() ? 1.27 : 1.62 - } - - isOnGround (): boolean { - return bot?.entity?.onGround ?? true - } - - isSneaking (): boolean { - return gameAdditionalState.isSneaking - } - - isFlying (): boolean { - return gameAdditionalState.isFlying - } - - isSprinting (): boolean { - return gameAdditionalState.isSprinting - } - - getPosition (): Vec3 { - return bot.entity?.position ?? new Vec3(0, 0, 0) - } - // #endregion - // #region Held Item State private updateHeldItem (isLeftHand: boolean) { const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem if (!newItem) { if (isLeftHand) { - this.offHandItem = undefined + this.reactive.heldItemOff = undefined } else { - this.heldItem = undefined + this.reactive.heldItemMain = undefined } - this.events.emit('heldItemChanged', undefined, isLeftHand) return } @@ -186,42 +157,36 @@ export class PlayerStateManager implements IPlayerState { } if (isLeftHand) { - this.offHandItem = item + this.reactive.heldItemOff = item } else { - this.heldItem = item + this.reactive.heldItemMain = item } - this.events.emit('heldItemChanged', item, isLeftHand) + // this.events.emit('heldItemChanged', item, isLeftHand) } startUsingItem () { if (this.isUsingItem) return this.isUsingItem = true - this.itemUsageTicks = 0 + this.reactive.itemUsageTicks = 0 } stopUsingItem () { this.isUsingItem = false - this.itemUsageTicks = 0 + this.reactive.itemUsageTicks = 0 } getItemUsageTicks (): number { - return this.itemUsageTicks + return this.reactive.itemUsageTicks } - getHeldItem (isLeftHand = false): HandItemBlock | undefined { - return isLeftHand ? this.offHandItem : this.heldItem + watchReactive () { + subscribeKey(this.reactive, 'eyeHeight', () => { + appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) + }) } - getItemSelector (specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item): ItemSelector['properties'] { - return { - ...specificProperties, - 'minecraft:date': new Date(), - // "minecraft:context_dimension": bot.entityp, - 'minecraft:time': bot.time.timeOfDay / 24_000, - } - } // #endregion } -export const playerState = PlayerStateManager.getInstance() +export const playerState = new PlayerStateControllerMain() window.playerState = playerState diff --git a/src/react/HotbarRenderApp.tsx b/src/react/HotbarRenderApp.tsx index 6e91180d..80f0a789 100644 --- a/src/react/HotbarRenderApp.tsx +++ b/src/react/HotbarRenderApp.tsx @@ -112,7 +112,7 @@ const HotbarInner = () => { inv.canvas.style.pointerEvents = 'auto' container.current.appendChild(inv.canvas) const upHotbarItems = () => { - if (!appViewer.resourcesManager.currentResources?.itemsAtlasParser) return + if (!appViewer.resourcesManager?.itemsAtlasParser) return upInventoryItems(true, inv) } diff --git a/src/react/Notification.tsx b/src/react/Notification.tsx index 7463c9a7..80354e83 100644 --- a/src/react/Notification.tsx +++ b/src/react/Notification.tsx @@ -1,14 +1,44 @@ import { motion, AnimatePresence } from 'framer-motion' -import PixelartIcon from './PixelartIcon' +import PixelartIcon, { pixelartIcons } from './PixelartIcon' +import { useUsingTouch } from './utilsApp' const duration = 0.2 // save pass: login -export default ({ type = 'message', message, subMessage = '', open, icon = '', action = undefined as (() => void) | undefined }) => { +const toastHeight = 32 + +interface NotificationProps { + open: boolean + message: string + type?: 'message' | 'error' | 'progress' + subMessage?: string + icon?: string + action?: () => void + topPosition?: number + + currentProgress?: number + totalProgress?: number +} + +export default ({ + type = 'message', + message, + subMessage = '', + open, + icon = '', + action = undefined as (() => void) | undefined, + topPosition = 0, + currentProgress, + totalProgress, +}: NotificationProps) => { + const isUsingTouch = useUsingTouch() const isError = type === 'error' icon ||= isError ? 'alert' : 'message' + const isLoader = type === 'progress' + + const top = (topPosition * toastHeight) + (isUsingTouch ? 18 : 0) // add space for mobile top buttons return {open && ( - +
- {message} + {translate(message)}
- {subMessage} + {translate(subMessage)}
+ {currentProgress !== undefined && totalProgress !== undefined && ( +
+
+
+ )}
)} diff --git a/src/react/RendererDebugMenu.tsx b/src/react/RendererDebugMenu.tsx index 2e6030c1..f4bf7876 100644 --- a/src/react/RendererDebugMenu.tsx +++ b/src/react/RendererDebugMenu.tsx @@ -8,7 +8,11 @@ import Slider from './Slider' import styles from './rendererDebugMenu.module.css' export default () => { - const worldRenderer = window.world as WorldRendererCommon + const worldRenderer = window.world as WorldRendererCommon | undefined + return worldRenderer ? : null +} + +const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererCommon }) => { const { reactiveDebugParams } = worldRenderer const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams) diff --git a/src/react/Singleplayer.tsx b/src/react/Singleplayer.tsx index 872a21ce..26d6427c 100644 --- a/src/react/Singleplayer.tsx +++ b/src/react/Singleplayer.tsx @@ -80,7 +80,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, }} onDoubleClick={() => onInteraction?.('enter')} > - world preview +
{title}
diff --git a/src/rendererUtils.ts b/src/rendererUtils.ts index 49a77801..0a49fc78 100644 --- a/src/rendererUtils.ts +++ b/src/rendererUtils.ts @@ -13,7 +13,7 @@ const BASE_MOVEMENT_SPEED = 0.1 // Default walking speed in Minecraft const FOV_EFFECT_SCALE = 1 // Equivalent to Minecraft's FOV Effects slider const updateFovAnimation = () => { - if (!bot) return + if (!playerState.ready) return // Calculate base FOV modifier let fovModifier = 1 @@ -39,10 +39,10 @@ const updateFovAnimation = () => { } // Item usage modifier - if (playerState.getHeldItem()) { - const heldItem = playerState.getHeldItem() - if (heldItem?.name === 'bow' && playerState.getItemUsageTicks() > 0) { - const ticksUsingItem = playerState.getItemUsageTicks() + if (playerState.reactive.heldItemMain) { + const heldItem = playerState.reactive.heldItemMain + if (heldItem?.name === 'bow' && playerState.reactive.itemUsageTicks > 0) { + const ticksUsingItem = playerState.reactive.itemUsageTicks let usageProgress = ticksUsingItem / 20 if (usageProgress > 1) { usageProgress = 1 @@ -88,8 +88,4 @@ export const watchFov = () => { customEvents.on('gameLoaded', () => { updateFovAnimation() }) - - subscribeKey(gameAdditionalState, 'isSneaking', () => { - appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) - }) } diff --git a/src/resourcePack.ts b/src/resourcePack.ts index 7c348cb2..e0f0fca4 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -32,7 +32,7 @@ const getLoadedImage = async (url: string) => { const resourcepackPackBasePath = '/data/resourcePacks/' export const uninstallResourcePack = async (name = 'default') => { if (await existsAsync('/resourcepack/pack.mcmeta')) { - await removeFileRecursiveAsync('/resourcepack') + await removeFileRecursiveAsync('/resourcepack', false) gameAdditionalState.usingServerResourcePack = false } const basePath = resourcepackPackBasePath + name @@ -212,7 +212,6 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', e if (!basePath) return let firstTextureSize: number | undefined const namespaces = await fs.promises.readdir(join(basePath, 'assets')) - progressReporter.beginStage(`generate-atlas-texture-${type}`, `Generating atlas texture for ${type}`) const textures = {} as Record let path @@ -420,6 +419,7 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) = } } catch (err) { console.error('Failed to read some of resource pack blockstates and models', err) + currentErrors.push('Failed to read blockstates/models') resources.customBlockStates = undefined resources.customModels = undefined resources.customItemModelNames = {} @@ -439,8 +439,10 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres console.log('Downloading server resource pack', url) console.time('downloadServerResourcePack') const response = await fetch(url).catch((err) => { - console.log(`Ensure server on ${url} support CORS which is not required for regular client, but is required for the web client`) console.error(err) + if (err.message === 'Failed to fetch') { + err.message = `Check internet connection and ensure server on ${url} support CORS which is not required for the vanilla client, but is required for the web client.` + } progressReporter.error('Failed to download resource pack: ' + err.message) }) console.timeEnd('downloadServerResourcePack') @@ -475,6 +477,7 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres showNotification('Failed to install resource pack: ' + err.message) }) } catch (err) { + console.error('Could not install resource pack', err) progressReporter.error('Could not install resource pack: ' + err.message) } finally { progressReporter.endStage('download-resource-pack') @@ -513,21 +516,19 @@ export const onAppLoad = () => { cancel: !forced, minecraftJsonMessage: promptMessagePacket, }) - if (Date.now() - start < 700) { // wait for state protocol switch - await new Promise(resolve => { + if (Date.now() - start < 700) { + void new Promise(resolve => { + // wait for state protocol switch setTimeout(resolve, 700) + }).then(() => { + if (choice === false || choice === 'Pretend Installed (not recommended)' || choice === 'Download & Install (recommended)' || choice) { + console.log('accepting resource pack') + bot.acceptResourcePack() + } else { + bot.denyResourcePack() + } }) } - if (choice === false) { - bot.acceptResourcePack() - return - } - if (!choice) { - bot.denyResourcePack() - return - } - console.log('accepting resource pack') - bot.acceptResourcePack() if (choice === true || choice === 'Download & Install (recommended)') { await downloadAndUseResourcePack(packet.url, createFullScreenProgressReporter()).catch((err) => { console.error(err) @@ -590,10 +591,17 @@ const updateTextures = async (progressReporter = createConsoleLogProgressReporte const origItemsFiles = Object.keys(appViewer.resourcesManager.sourceItemsAtlases.latest.textures) const origArmorFiles = Object.keys(armorTextures) const { usedBlockTextures, usedItemTextures } = await prepareBlockstatesAndModels(progressReporter) ?? {} - const blocksData = await getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter) - const itemsData = await getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter) - const armorData = await getResourcepackTiles('armor', origArmorFiles, progressReporter) - await updateAllReplacableTextures() + progressReporter.beginStage(`generate-atlas-texture-blocks`, `Generating atlas textures`) + const [ + blocksData, + itemsData, + armorData + ] = await Promise.all([ + getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter), + getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter), + getResourcepackTiles('armor', origArmorFiles, progressReporter), + updateAllReplacableTextures() + ]) resources.customTextures = {} if (blocksData) { diff --git a/src/resourcesManager.ts b/src/resourcesManager.ts index 4ee91ab4..ef3dd7ce 100644 --- a/src/resourcesManager.ts +++ b/src/resourcesManager.ts @@ -8,26 +8,25 @@ import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png' import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png' import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png' import christmasPack from 'mc-assets/dist/textureReplacements/christmas' -import { AtlasParser } from 'mc-assets/dist/atlasParser' +import { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets/dist/atlasParser' import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { getLoadedItemDefinitionsStore } from 'mc-assets' -import { getLoadedImage } from 'mc-assets/dist/utils' import { generateGuiAtlas } from 'renderer/viewer/lib/guiRenderer' import { importLargeData } from '../generated/large-data-aliases' -import { loadMinecraftData } from './connect' type ResourceManagerEvents = { assetsTexturesUpdated: () => void + assetsInventoryStarted: () => void assetsInventoryReady: () => void } -export class LoadedResources { +export class LoadedResourcesTransferrable { + allReady = false // Atlas parsers - itemsAtlasParser: AtlasParser - blocksAtlasParser: AtlasParser - itemsAtlasImage: HTMLImageElement - blocksAtlasImage: HTMLImageElement + itemsAtlasImage: ImageBitmap + blocksAtlasImage: ImageBitmap + blocksAtlasJson: ItemsAtlasesOutputJson // User data (specific to current resourcepack/version) customBlockStates?: Record customModels?: Record @@ -38,9 +37,11 @@ export class LoadedResources { blocks?: { tileSize: number | undefined, textures: Record } armor?: { tileSize: number | undefined, textures: Record } } = {} + guiAtlas: { json: any, image: ImageBitmap } | null = null + guiAtlasVersion = 0 itemsRenderer: ItemsRenderer - worldBlockProvider: WorldBlockProvider + worldBlockProvider?: WorldBlockProvider blockstatesModels: any = null version: string @@ -59,8 +60,17 @@ export interface UpdateAssetsRequest { _?: false } +export interface ResourcesManagerTransferred extends TypedEmitter { + currentResources: LoadedResourcesTransferrable +} +export interface ResourcesManagerCommon extends TypedEmitter { + currentResources: LoadedResourcesTransferrable | undefined +} + const STABLE_MODELS_VERSION = '1.21.4' export class ResourcesManager extends (EventEmitter as new () => TypedEmitter) { + static restorerName = 'ResourcesManager' + // Source data (imported, not changing) sourceBlockStatesModels: any = null readonly sourceBlocksAtlases: any = blocksAtlases @@ -68,7 +78,9 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter() @@ -76,17 +88,12 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter TypedEmitter TypedEmitter { - const texture = resources.customTextures.items?.textures[textureName] - if (!texture) return - return texture - }, - resources.customTextures.items?.tileSize, - undefined, - customItemTextures - ) - console.timeEnd('createItemsAtlas') - - resources.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL()) - resources.itemsAtlasImage = await getLoadedImage(itemsCanvas.toDataURL()) - - if (resources.version && resources.blockstatesModels && resources.itemsAtlasParser && resources.blocksAtlasParser) { + if (resources.version && resources.blockstatesModels && this.itemsAtlasParser && this.blocksAtlasParser) { resources.itemsRenderer = new ItemsRenderer( resources.version, resources.blockstatesModels, - resources.itemsAtlasParser, - resources.blocksAtlasParser + this.itemsAtlasParser, + this.blocksAtlasParser ) } if (abortController.signal.aborted) return this.currentResources = resources + resources.allReady = true if (!unstableSkipEvent) { // todo rework resourcepack optimization this.emit('assetsTexturesUpdated') } @@ -157,6 +151,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter { if (abortController.signal.aborted) return if (!unstableSkipEvent) { @@ -167,7 +162,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter const date = new Date() if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) { @@ -194,16 +189,36 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter { + const texture = resources.customTextures.items?.textures[textureName] + if (!texture) return + return texture + }, + resources.customTextures.items?.tileSize, + undefined, + customItemTextures + ) + + this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL()) + resources.itemsAtlasImage = await createImageBitmap(itemsCanvas) + } + async generateGuiTextures () { await generateGuiAtlas() } @@ -211,7 +226,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter