Compare commits

...
Sign in to create a new pull request.

13 commits

Author SHA1 Message Date
Vitaly Turovsky
d3b4ebe934 ok finally finish all regressions seems fixed 2025-06-18 08:14:19 +03:00
Vitaly Turovsky
c03525b1ca fix 2025-06-18 04:32:14 +03:00
Vitaly Turovsky
d2e1222e6c add cursor rules :/ 2025-06-18 03:11:12 +03:00
Vitaly Turovsky
fc5a1846b0 reload entities on assets updates and f3+a 2025-06-16 17:48:12 +03:00
Vitaly Turovsky
c481ab307c do lint 2025-06-14 15:42:43 +03:00
Vitaly
ee16377d18
Update src/appViewer.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-14 15:36:53 +03:00
Vitaly Turovsky
25e55cabf5 update hand 2025-06-14 15:35:13 +03:00
Vitaly Turovsky
084228d6e1 restore data, restore hand, lint 2025-06-14 15:34:44 +03:00
Vitaly Turovsky
25be265940 Merge remote-tracking branch 'upstream/fix-frames' into renderer-cleanup 2025-06-14 15:12:50 +03:00
Vitaly Turovsky
abaaeaef4e restore textures / panorama
todo restore loadedData and hand
2025-06-14 15:11:22 +03:00
Vitaly Turovsky
a8c77b0e0a big renderer codebase cleanup: rework player state
use less dom things for better portabilitity / testing

pick almost all changes
update notification base
2025-06-14 15:00:18 +03:00
Vitaly Turovsky
02a1be8eea revert custom channel change 2025-06-13 13:26:00 +03:00
Vitaly Turovsky
b3807fff65 fix: fix rotation of item frames & items within it 2025-06-13 08:05:17 +03:00
37 changed files with 977 additions and 710 deletions

View file

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

1
.gitignore vendored
View file

@ -19,5 +19,6 @@ generated
storybook-static storybook-static
server-jar server-jar
config.local.json config.local.json
logs/
src/react/npmReactComponents.ts src/react/npmReactComponents.ts

View file

@ -35,6 +35,10 @@ const buildOptions = {
define: { define: {
'process.env.BROWSER': '"true"', 'process.env.BROWSER': '"true"',
}, },
loader: {
'.png': 'dataurl',
'.obj': 'text'
},
plugins: [ plugins: [
...mesherSharedPlugins, ...mesherSharedPlugins,
{ {

View file

@ -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 { return {
world: { reactive: {
chunksLoaded: new Set(), world: {
heightmaps: new Map(), chunksLoaded: new Set(),
chunksTotalNumber: 0, heightmaps: new Map(),
allChunksLoaded: true, allChunksLoaded: true,
mesherWork: false, mesherWork: false,
intersectMedia: null intersectMedia: null
},
renderer: '',
preventEscapeMenu: false
}, },
renderer: '', nonReactive: {
preventEscapeMenu: false world: {
chunksLoaded: new Set(),
chunksTotalNumber: 0,
}
}
} }
} }

View file

@ -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 { ItemSelector } from 'mc-assets/dist/itemDefinitions'
import { proxy, ref } from 'valtio'
import { GameMode } from 'mineflayer' 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 MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>> export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>>
export type PlayerStateEvents = {
heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void
}
export type BlockShape = { position: any; width: any; height: any; depth: any; } export type BlockShape = { position: any; width: any; height: any; depth: any; }
export type BlocksShapes = BlockShape[] export type BlocksShapes = BlockShape[]
export interface IPlayerState { // edit src/mineflayer/playerState.ts for implementation of player state from mineflayer
getEyeHeight(): number export const getInitialPlayerState = () => proxy({
getMovementState(): MovementState playerSkin: undefined as string | undefined,
getVelocity(): Vec3 inWater: false,
isOnGround(): boolean waterBreathing: false,
isSneaking(): boolean backgroundColor: [0, 0, 0] as [number, number, number],
isFlying(): boolean ambientLight: 0,
isSprinting (): boolean directionalLight: 0,
getItemUsageTicks?(): number eyeHeight: 0,
getPosition(): Vec3 gameMode: undefined as GameMode | undefined,
// isUsingItem?(): boolean lookingAtBlock: undefined as {
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined x: number
username?: string y: number
onlineMode?: boolean z: number
lightingDisabled?: boolean face?: number
shouldHideHand?: boolean 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<PlayerStateEvents> export const getInitialPlayerStateRenderer = () => ({
reactive: getInitialPlayerState()
})
reactive: { export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState>
playerSkin: string | undefined
inWater: boolean export interface PlayerStateRenderer {
waterBreathing: boolean reactive: PlayerStateReactive
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 class BasePlayerState implements IPlayerState { export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => {
reactive = proxy({ return {
playerSkin: undefined as string | undefined, ...specificProperties,
inWater: false, 'minecraft:date': new Date(),
waterBreathing: false, // "minecraft:context_dimension": bot.entityp,
backgroundColor: ref([0, 0, 0]) as [number, number, number], // 'minecraft:time': bot.time.timeOfDay / 24_000,
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<PlayerStateEvents>
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)
} }
} }

View file

@ -9,11 +9,6 @@ import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
import { proxy, ref } from 'valtio' import { proxy, ref } from 'valtio'
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions' import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
export const activeGuiAtlas = proxy({
atlas: null as null | { json, image },
version: 0
})
export const getNonFullBlocksModels = () => { export const getNonFullBlocksModels = () => {
let version = appViewer.resourcesManager.currentResources!.version ?? 'latest' let version = appViewer.resourcesManager.currentResources!.version ?? 'latest'
if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13' if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13'
@ -122,18 +117,18 @@ const RENDER_SIZE = 64
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => { const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
const { currentResources } = appViewer.resourcesManager 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') const canvasTemp = document.createElement('canvas')
canvasTemp.width = img.width canvasTemp.width = imgBitmap.width
canvasTemp.height = img.height canvasTemp.height = imgBitmap.height
canvasTemp.style.imageRendering = 'pixelated' canvasTemp.style.imageRendering = 'pixelated'
const ctx = canvasTemp.getContext('2d')! const ctx = canvasTemp.getContext('2d')!
ctx.imageSmoothingEnabled = false 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( 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]) => { Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
return [key, [ return [key, [
value.u, value.u,
@ -243,6 +238,9 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
return images return images
} }
/**
* @mainThread
*/
const generateAtlas = async (images: Record<string, HTMLImageElement>) => { const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
const atlas = makeTextureAtlas({ const atlas = makeTextureAtlas({
input: Object.keys(images), input: Object.keys(images),
@ -260,9 +258,9 @@ const generateAtlas = async (images: Record<string, HTMLImageElement>) => {
// a.download = 'blocks_atlas.png' // a.download = 'blocks_atlas.png'
// a.click() // a.click()
activeGuiAtlas.atlas = { appViewer.resourcesManager.currentResources!.guiAtlas = {
json: atlas.json, json: atlas.json,
image: ref(await getLoadedImage(atlas.canvas.toDataURL())), image: await createImageBitmap(atlas.canvas),
} }
return atlas return atlas
@ -279,6 +277,6 @@ export const generateGuiAtlas = async () => {
const itemImages = await generateItemsGui(itemsModelsResolved, true) const itemImages = await generateItemsGui(itemsModelsResolved, true)
console.timeEnd('generate items gui atlas') console.timeEnd('generate items gui atlas')
await generateAtlas({ ...blockImages, ...itemImages }) await generateAtlas({ ...blockImages, ...itemImages })
activeGuiAtlas.version++ appViewer.resourcesManager.currentResources!.guiAtlasVersion++
// await generateAtlas(blockImages) // await generateAtlas(blockImages)
} }

View file

@ -77,6 +77,7 @@ const handleMessage = data => {
if (data.type === 'mcData') { if (data.type === 'mcData') {
globalVar.mcData = data.mcData globalVar.mcData = data.mcData
globalVar.loadedData = data.mcData
} }
if (data.config) { if (data.config) {
@ -138,6 +139,7 @@ const handleMessage = data => {
dirtySections = new Map() dirtySections = new Map()
// todo also remove cached // todo also remove cached
globalVar.mcData = null globalVar.mcData = null
globalVar.loadedData = null
allDataReady = false allDataReady = false
break break

View file

@ -9,7 +9,7 @@ export const defaultMesherConfig = {
skyLight: 15, skyLight: 15,
smoothLighting: true, smoothLighting: true,
outputFormat: 'threeJs' as 'threeJs' | 'webgpu', outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
textureSize: 1024, // for testing // textureSize: 1024, // for testing
debugModelVariant: undefined as undefined | number[], debugModelVariant: undefined as undefined | number[],
clipWorldBelowY: undefined as undefined | number, clipWorldBelowY: undefined as undefined | number,
disableSignsMapsSupport: false disableSignsMapsSupport: false

View file

@ -1,4 +1,5 @@
import * as THREE from 'three' import * as THREE from 'three'
import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './utils/skins'
let textureCache: Record<string, THREE.Texture> = {} let textureCache: Record<string, THREE.Texture> = {}
let imagesPromises: Record<string, Promise<THREE.Texture>> = {} let imagesPromises: Record<string, Promise<THREE.Texture>> = {}
@ -7,7 +8,9 @@ export async function loadTexture (texture: string, cb: (texture: THREE.Texture)
const cached = textureCache[texture] const cached = textureCache[texture]
if (!cached) { if (!cached) {
const { promise, resolve } = Promise.withResolvers<THREE.Texture>() const { promise, resolve } = Promise.withResolvers<THREE.Texture>()
textureCache[texture] = new THREE.TextureLoader().load(texture, resolve) const t = loadThreeJsTextureFromUrlSync(texture)
textureCache[texture] = t.texture
void t.promise.then(resolve)
imagesPromises[texture] = promise imagesPromises[texture] = promise
} }

View file

@ -1,18 +1,43 @@
import { loadSkinToCanvas } from 'skinview-utils' import { loadSkinToCanvas } from 'skinview-utils'
import * as THREE from 'three' import * as THREE from 'three'
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png' 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 loadThreeJsTextureFromUrlSync = (imageUrl: string) => {
export const stevePngUrl = stevePng const texture = new THREE.Texture()
export const steveTexture = new THREE.TextureLoader().loadAsync(stevePng) const promise = getLoadedImage(imageUrl).then(image => {
texture.image = image
export async function loadImageFromUrl (imageUrl: string): Promise<HTMLImageElement> { texture.needsUpdate = true
const img = new Image() return texture
img.src = imageUrl
await new Promise<void>(resolve => {
img.onload = () => resolve()
}) })
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<ImageBitmap> {
const response = await fetch(imageUrl)
const blob = await response.blob()
return createImageBitmap(blob)
} }
const config = { const config = {
@ -52,13 +77,13 @@ export const parseSkinTexturesValue = (value: string) => {
return decodedData.textures?.SKIN?.url 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:')) { if (!skinUrl.startsWith('data:')) {
skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://')) skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://'))
} }
const image = await loadImageFromUrl(skinUrl) const image = await loadImageFromUrl(skinUrl)
const skinCanvas = document.createElement('canvas') const skinCanvas = new OffscreenCanvas(64, 64)
loadSkinToCanvas(skinCanvas, image) loadSkinToCanvas(skinCanvas, image)
return { canvas: skinCanvas, image } return { canvas: skinCanvas, image }
} }

View file

@ -1,9 +1,20 @@
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { import { proxy, getVersion, subscribe } from 'valtio'
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void | Promise<any>>> (handlers: T, channel?: MessagePort): { __workerProxy: T } {
const target = channel ?? globalThis const target = channel ?? globalThis
target.addEventListener('message', (event: any) => { target.addEventListener('message', (event: any) => {
const { type, args } = event.data const { type, args, msgId } = event.data
if (handlers[type]) { 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 return null as any
@ -23,6 +34,7 @@ export function createWorkerProxy<T extends Record<string, (...args: any[]) => v
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & { export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & {
transfer: (...args: Transferable[]) => T['__workerProxy'] transfer: (...args: Transferable[]) => T['__workerProxy']
} => { } => {
let messageId = 0
// in main thread // in main thread
return new Proxy({} as any, { return new Proxy({} as any, {
get (target, prop) { get (target, prop) {
@ -41,11 +53,30 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg
} }
} }
return (...args: any[]) => { return (...args: any[]) => {
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({ worker.postMessage({
type: prop, type: prop,
msgId,
args, 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)
}
}
} }
} }
}) })

View file

@ -7,9 +7,7 @@ import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer' import { BotEvents } from 'mineflayer'
import { proxy } from 'valtio' import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter' import TypedEmitter from 'typed-emitter'
import { getItemFromBlock } from '../../../src/chatUtils'
import { delayedIterator } from '../../playground/shared' import { delayedIterator } from '../../playground/shared'
import { playerState } from '../../../src/mineflayer/playerState'
import { chunkPos } from './simpleUtils' import { chunkPos } from './simpleUtils'
export type ChunkPosKey = string // like '16,16' export type ChunkPosKey = string // like '16,16'
@ -23,7 +21,6 @@ export type WorldDataEmitterEvents = {
time: (data: number) => void time: (data: number) => void
renderDistance: (viewDistance: number) => void renderDistance: (viewDistance: number) => void
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
listening: () => void
markAsLoaded: (data: { x: number, z: number }) => void markAsLoaded: (data: { x: number, z: number }) => void
unloadChunk: (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 loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void
@ -32,10 +29,10 @@ export type WorldDataEmitterEvents = {
end: () => void end: () => void
} }
/** export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
* Usually connects to mineflayer bot and emits world data (chunks, entities) static readonly restorerName = 'WorldDataEmitterWorker'
* It's up to the consumer to serialize the data if needed }
*/
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) { export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
loadedChunks: Record<ChunkPosKey, boolean> loadedChunks: Record<ChunkPosKey, boolean>
readonly lastPos: Vec3 readonly lastPos: Vec3
@ -57,11 +54,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
/* config */ isPlayground = false /* config */ isPlayground = false
/* config */ allowPositionUpdate = true /* config */ allowPositionUpdate = true
public reactive = proxy({
cursorBlock: null as Vec3 | null,
cursorBlockBreakingStage: null as number | null,
})
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
// eslint-disable-next-line constructor-super // eslint-disable-next-line constructor-super
super() super()
@ -171,22 +163,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
} }
}) })
this.emitter.on('listening', () => {
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)) { for (const [evt, listener] of Object.entries(this.eventListeners)) {
bot.on(evt as any, listener) bot.on(evt as any, listener)
} }
@ -200,8 +176,16 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
console.error('error processing entity', err) console.error('error processing entity', err)
} }
} }
}
void this.init(bot.entity.position) emitterGotConnected () {
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
},
}))
} }
removeListenersFromBot (bot: import('mineflayer').Bot) { removeListenersFromBot (bot: import('mineflayer').Bot) {
@ -213,6 +197,8 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
async init (pos: Vec3) { async init (pos: Vec3) {
this.updateViewDistance(this.viewDistance) this.updateViewDistance(this.viewDistance)
this.emitter.emit('chunkPosUpdate', { pos }) this.emitter.emit('chunkPosUpdate', { pos })
this.emitter.emit('time', bot.time.timeOfDay)
this.emitterGotConnected()
const [botX, botZ] = chunkPos(pos) const [botX, botZ] = chunkPos(pos)
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16)) const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16))

View file

@ -1,25 +1,22 @@
/* eslint-disable guard-for-in */ /* eslint-disable guard-for-in */
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { Vec3 } from 'vec3' import { Vec3 } from 'vec3'
import * as THREE from 'three'
import mcDataRaw from 'minecraft-data/data.js' // note: using alias import mcDataRaw from 'minecraft-data/data.js' // note: using alias
import TypedEmitter from 'typed-emitter' import TypedEmitter from 'typed-emitter'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { generateSpiralMatrix } from 'flying-squid/dist/utils' import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { subscribeKey } from 'valtio/utils' import { subscribeKey } from 'valtio/utils'
import { proxy } from 'valtio' import { proxy } from 'valtio'
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs' import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajorVersion } from '../../../src/utils' import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
import { ResourcesManager } from '../../../src/resourcesManager'
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer' import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
import { SoundSystem } from '../three/threeJsSound' import { SoundSystem } from '../three/threeJsSound'
import { buildCleanupDecorator } from './cleanupDecorator' 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 { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats' import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
import { WorldDataEmitter } from './worldDataEmitter' import { WorldDataEmitterWorker } from './worldDataEmitter'
import { IPlayerState } from './basePlayerState' import { PlayerStateRenderer } from './basePlayerState'
import { MesherLogReader } from './mesherlogReader' import { MesherLogReader } from './mesherlogReader'
import { setSkinsConfig } from './utils/skins' import { setSkinsConfig } from './utils/skins'
@ -27,6 +24,11 @@ function mod (x, n) {
return ((x % n) + n) % 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 worldCleanup = buildCleanupDecorator('resetWorld')
export const defaultWorldRendererConfig = { export const defaultWorldRendererConfig = {
@ -52,7 +54,8 @@ export const defaultWorldRendererConfig = {
foreground: true, foreground: true,
enableDebugOverlay: false, enableDebugOverlay: false,
_experimentalSmoothChunkLoading: true, _experimentalSmoothChunkLoading: true,
_renderByChunks: false _renderByChunks: false,
volume: 1
} }
export type WorldRendererConfig = typeof defaultWorldRendererConfig export type WorldRendererConfig = typeof defaultWorldRendererConfig
@ -153,7 +156,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
abstract changeBackgroundColor (color: [number, number, number]): void abstract changeBackgroundColor (color: [number, number, number]): void
worldRendererConfig: WorldRendererConfig worldRendererConfig: WorldRendererConfig
playerState: IPlayerState playerState: PlayerStateRenderer
reactiveState: RendererReactiveState reactiveState: RendererReactiveState
mesherLogReader: MesherLogReader | undefined mesherLogReader: MesherLogReader | undefined
forceCallFromMesherReplayer = false forceCallFromMesherReplayer = false
@ -169,6 +172,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
} }
currentRenderedFrames = 0 currentRenderedFrames = 0
fpsAverage = 0 fpsAverage = 0
lastFps = 0
fpsWorst = undefined as number | undefined fpsWorst = undefined as number | undefined
fpsSamples = 0 fpsSamples = 0
mainThreadRendering = true mainThreadRendering = true
@ -184,7 +188,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
return (this.initOptions.config.statsVisible ?? 0) > 1 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.snapshotInitialValues()
this.worldRendererConfig = displayOptions.inWorldRenderingConfig this.worldRendererConfig = displayOptions.inWorldRenderingConfig
this.playerState = displayOptions.playerState this.playerState = displayOptions.playerState
@ -221,6 +225,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
} else { } else {
this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames) this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames)
} }
this.lastFps = this.currentRenderedFrames
this.currentRenderedFrames = 0 this.currentRenderedFrames = 0
} }
@ -231,15 +236,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
async init () { async init () {
if (this.active) throw new Error('WorldRendererCommon is already initialized') 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([ await Promise.all([
this.resetWorkers(), this.resetWorkers(),
(async () => { (async () => {
if (this.resourcesManager.currentResources) { if (this.resourcesManager.currentResources?.allReady) {
await this.updateAssetsData() await this.updateAssetsData()
} }
})() })()
@ -291,35 +292,22 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) { initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) {
// init workers // init workers
for (let i = 0; i < numWorkers + 1; i++) { for (let i = 0; i < numWorkers + 1; i++) {
// Node environment needs an absolute path, but browser needs the url of the file const worker = initMesherWorker((data) => {
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 }) => {
if (Array.isArray(data)) { if (Array.isArray(data)) {
this.messageQueue.push(...data) this.messageQueue.push(...data)
} else { } else {
this.messageQueue.push(data) this.messageQueue.push(data)
} }
void this.processMessageQueue('worker') void this.processMessageQueue('worker')
} })
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
this.workers.push(worker) this.workers.push(worker)
} }
} }
onReactiveValueUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) { onReactivePlayerStateUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void, initial = true) {
callback(this.displayOptions.playerState.reactive[key]) if (initial) {
callback(this.displayOptions.playerState.reactive[key])
}
subscribeKey(this.displayOptions.playerState.reactive, key, callback) subscribeKey(this.displayOptions.playerState.reactive, key, callback)
} }
@ -334,7 +322,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
} }
watchReactivePlayerState () { watchReactivePlayerState () {
this.onReactiveValueUpdated('backgroundColor', (value) => { this.onReactivePlayerStateUpdated('backgroundColor', (value) => {
this.changeBackgroundColor(value) this.changeBackgroundColor(value)
}) })
} }
@ -466,7 +454,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
} }
if (data.type === 'heightmap') { 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<WorkerSend = any, WorkerReceive = any>
this.resetWorld() this.resetWorld()
// for workers in single file build // for workers in single file build
if (document?.readyState === 'loading') { if (typeof document !== 'undefined' && document?.readyState === 'loading') {
await new Promise(resolve => { await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve) document.addEventListener('DOMContentLoaded', resolve)
}) })
@ -575,7 +563,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
skyLight, skyLight,
smoothLighting: this.worldRendererConfig.smoothLighting, smoothLighting: this.worldRendererConfig.smoothLighting,
outputFormat: this.outputFormat, outputFormat: this.outputFormat,
textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width, // textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
debugModelVariant: undefined, debugModelVariant: undefined,
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY, clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
@ -600,7 +588,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
} }
async updateAssetsData () { async updateAssetsData () {
const resources = this.resourcesManager.currentResources! const resources = this.resourcesManager.currentResources
if (this.workers.length === 0) throw new Error('workers not initialized yet') if (this.workers.length === 0) throw new Error('workers not initialized yet')
for (const [i, worker] of this.workers.entries()) { for (const [i, worker] of this.workers.entries()) {
@ -610,7 +598,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
type: 'mesherData', type: 'mesherData',
workerIndex: i, workerIndex: i,
blocksAtlas: { blocksAtlas: {
latest: resources.blocksAtlasParser.atlas.latest latest: resources.blocksAtlasJson
}, },
blockstatesModels, blockstatesModels,
config: this.getMesherConfig(), config: this.getMesherConfig(),
@ -733,7 +721,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
lightUpdate (chunkX: number, chunkZ: number) { } lightUpdate (chunkX: number, chunkZ: number) { }
connect (worldView: WorldDataEmitter) { connect (worldView: WorldDataEmitterWorker) {
const worldEmitter = worldView const worldEmitter = worldView
worldEmitter.on('entity', (e) => { worldEmitter.on('entity', (e) => {
@ -812,7 +800,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}) })
worldEmitter.on('onWorldSwitch', () => { 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) => { worldEmitter.on('time', (timeOfDay) => {
@ -830,8 +827,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// (this).rerenderAllChunks?.() // (this).rerenderAllChunks?.()
// } // }
}) })
worldEmitter.emit('listening')
} }
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) { setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
@ -1029,3 +1024,37 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
removeAllStats() 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<string, any>) => {
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 })
}
}

View file

@ -1,16 +1,16 @@
import { BlockModel } from 'mc-assets/dist/types' import { BlockModel } from 'mc-assets/dist/types'
import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState' import { ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
import { renderSlot } from '../../../src/inventoryWindows'
import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items' 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<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager): { export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): {
u: number u: number
v: number v: number
su: number su: number
sv: number sv: number
renderInfo?: ReturnType<typeof renderSlot> renderInfo?: ReturnType<typeof renderSlot>
texture: HTMLImageElement // texture: ImageBitmap
modelName: string modelName: string
} | { } | {
resolvedModel: BlockModel resolvedModel: BlockModel
@ -30,11 +30,11 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
const model = getItemModelName({ const model = getItemModelName({
...item, ...item,
name, name,
} as GeneralInputItem, specificProps, resourcesManager) } as GeneralInputItem, specificProps, resourcesManager, playerState)
const renderInfo = renderSlot({ const renderInfo = renderSlot({
modelName: model, modelName: model,
}, false, true) }, resourcesManager, false, true)
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`) if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
@ -53,7 +53,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
return { return {
u, v, su, sv, u, v, su, sv,
renderInfo, renderInfo,
texture: img, // texture: img,
modelName: renderInfo.modelName! modelName: renderInfo.modelName!
} }
} }
@ -67,7 +67,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
v: 0, v: 0,
su: 16 / resources.blocksAtlasImage.width, su: 16 / resources.blocksAtlasImage.width,
sv: 16 / resources.blocksAtlasImage.width, sv: 16 / resources.blocksAtlasImage.width,
texture: resources.blocksAtlasImage, // texture: resources.blocksAtlasImage,
modelName: 'missing' modelName: 'missing'
} }
} }

View file

@ -6,15 +6,20 @@ import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appView
import { WorldRendererConfig } from '../lib/worldrendererCommon' import { WorldRendererConfig } from '../lib/worldrendererCommon'
export class DocumentRenderer { export class DocumentRenderer {
readonly canvas = document.createElement('canvas') canvas: HTMLCanvasElement | OffscreenCanvas
readonly renderer: THREE.WebGLRenderer readonly renderer: THREE.WebGLRenderer
private animationFrameId?: number private animationFrameId?: number
private timeoutId?: number
private lastRenderTime = 0 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 renderedFps = 0
private fpsInterval: any private fpsInterval: any
private readonly stats: TopRightStats private readonly stats: TopRightStats | undefined
private paused = false private paused = false
disconnected = false disconnected = false
preRender = () => { } preRender = () => { }
@ -26,9 +31,16 @@ export class DocumentRenderer {
onRender = [] as Array<(sizeChanged: boolean) => void> onRender = [] as Array<(sizeChanged: boolean) => void>
inWorldRenderingConfig: WorldRendererConfig | undefined inWorldRenderingConfig: WorldRendererConfig | undefined
constructor (initOptions: GraphicsInitOptions) { constructor (initOptions: GraphicsInitOptions, public externalCanvas?: OffscreenCanvas) {
this.config = initOptions.config this.config = initOptions.config
// Handle canvas creation/transfer based on context
if (externalCanvas) {
this.canvas = externalCanvas
} else {
this.addToPage()
}
try { try {
this.renderer = new THREE.WebGLRenderer({ this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas, canvas: this.canvas,
@ -37,17 +49,24 @@ export class DocumentRenderer {
powerPreference: this.config.powerPreference powerPreference: this.config.powerPreference
}) })
} catch (err) { } 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 throw err
} }
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
this.updatePixelRatio() if (!externalCanvas) {
this.updateSize() this.updatePixelRatio()
this.addToPage() }
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() this.startRenderLoop()
} }
@ -59,15 +78,33 @@ export class DocumentRenderer {
this.renderer.setPixelRatio(pixelRatio) this.renderer.setPixelRatio(pixelRatio)
} }
updateSize () { sizeUpdated () {
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setSize(this.currentWidth, this.currentHeight, false)
} }
private addToPage () { private addToPage () {
this.canvas.id = 'viewer-canvas' this.canvas = addCanvasToPage()
this.canvas.style.width = '100%' this.updateCanvasSize()
this.canvas.style.height = '100%' }
document.body.appendChild(this.canvas)
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 () { private setupFpsTracking () {
@ -81,20 +118,15 @@ export class DocumentRenderer {
}, 1000) }, 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 () { private startRenderLoop () {
const animate = () => { const animate = () => {
if (this.disconnected) return 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 if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return
@ -112,18 +144,19 @@ export class DocumentRenderer {
} }
let sizeChanged = false let sizeChanged = false
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) { this.updateCanvasSize()
this.previousWindowWidth = window.innerWidth if (this.previousCanvasWidth !== this.currentWidth || this.previousCanvasHeight !== this.currentHeight) {
this.previousWindowHeight = window.innerHeight this.previousCanvasWidth = this.currentWidth
this.updateSize() this.previousCanvasHeight = this.currentHeight
this.sizeUpdated()
sizeChanged = true sizeChanged = true
} }
this.frameRender(sizeChanged) this.frameRender(sizeChanged)
// Update stats visibility each frame // Update stats visibility each frame (main thread only)
if (this.config.statsVisible !== undefined) { 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) { frameRender (sizeChanged: boolean) {
this.preRender() this.preRender()
this.stats.markStart() this.stats?.markStart()
tween.update() tween.update()
if (!window.freezeRender) { if (!globalThis.freezeRender) {
this.render(sizeChanged) this.render(sizeChanged)
} }
for (const fn of this.onRender) { for (const fn of this.onRender) {
fn(sizeChanged) fn(sizeChanged)
} }
this.renderedFps++ this.renderedFps++
this.stats.markEnd() this.stats?.markEnd()
this.postRender() this.postRender()
} }
@ -154,10 +187,15 @@ export class DocumentRenderer {
if (this.animationFrameId) { if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId) cancelAnimationFrame(this.animationFrameId)
} }
this.canvas.remove() if (this.timeoutId) {
this.renderer.dispose() clearTimeout(this.timeoutId)
}
if (this.canvas instanceof HTMLCanvasElement) {
this.canvas.remove()
}
clearInterval(this.fpsInterval) clearInterval(this.fpsInterval)
this.stats.dispose() this.stats?.dispose()
this.renderer.dispose()
} }
} }
@ -250,3 +288,40 @@ class TopRightStats {
this.statsGl.container.remove() 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 }
}
}
}

View file

@ -96,7 +96,7 @@ function getUsernameTexture ({
nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
nameTagTextOpacity = 255 nameTagTextOpacity = 255
}: any, { fontFamily = 'sans-serif' }: any) { }: any, { fontFamily = 'sans-serif' }: any) {
const canvas = document.createElement('canvas') const canvas = new OffscreenCanvas(64, 64)
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Could not get 2d context') 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() 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) { if (entity.name) {
try { try {
// https://github.com/PrismarineJS/prismarine-viewer/pull/410 // https://github.com/PrismarineJS/prismarine-viewer/pull/410
@ -209,6 +209,7 @@ export type SceneEntity = THREE.Object3D & {
username?: string username?: string
uuid?: string uuid?: string
additionalCleanup?: () => void additionalCleanup?: () => void
originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name }
} }
export class Entities { export class Entities {
@ -250,6 +251,7 @@ export class Entities {
constructor (public worldRenderer: WorldRendererThree) { constructor (public worldRenderer: WorldRendererThree) {
this.debugMode = 'none' this.debugMode = 'none'
this.onSkinUpdate = () => { } this.onSkinUpdate = () => { }
this.watchResourcesUpdates()
} }
clear () { clear () {
@ -260,6 +262,20 @@ export class Entities {
this.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) { setDebugMode (mode: string, entity: THREE.Object3D | null = null) {
this.debugMode = mode this.debugMode = mode
for (const mesh of entity ? [entity] : Object.values(this.entities)) { for (const mesh of entity ? [entity] : Object.values(this.entities)) {
@ -291,7 +307,7 @@ export class Entities {
const dt = this.clock.getDelta() const dt = this.clock.getDelta()
const botPos = this.worldRenderer.viewerPosition const botPos = this.worldRenderer.viewerPosition
const VISIBLE_DISTANCE = 8 * 8 const VISIBLE_DISTANCE = 10 * 10
for (const entityId of Object.keys(this.entities)) { for (const entityId of Object.keys(this.entities)) {
const entity = this.entities[entityId] const entity = this.entities[entityId]
@ -312,13 +328,8 @@ export class Entities {
const dz = entity.position.z - botPos.z const dz = entity.position.z - botPos.z
const distanceSquared = dx * dx + dy * dy + dz * dz const distanceSquared = dx * dx + dy * dy + dz * dz
// Get chunk coordinates // Entity is visible if within 20 blocks OR in a finished chunk
const chunkX = Math.floor(entity.position.x / 16) * 16 entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity))
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])
this.maybeRenderPlayerSkin(entityId) this.maybeRenderPlayerSkin(entityId)
} }
@ -467,16 +478,16 @@ export class Entities {
if (!playerObject) return if (!playerObject) return
try { try {
let playerCustomSkinImage: HTMLImageElement | undefined let playerCustomSkinImage: ImageBitmap | undefined
playerObject = this.getPlayerObject(entityId) playerObject = this.getPlayerObject(entityId)
if (!playerObject) return if (!playerObject) return
let skinTexture: THREE.Texture let skinTexture: THREE.Texture
let skinCanvas: HTMLCanvasElement let skinCanvas: OffscreenCanvas
if (skinUrl === stevePngUrl) { if (skinUrl === stevePngUrl) {
skinTexture = await steveTexture skinTexture = await steveTexture
const canvas = document.createElement('canvas') const canvas = new OffscreenCanvas(64, 64)
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Failed to get context') if (!ctx) throw new Error('Failed to get context')
ctx.drawImage(skinTexture.image, 0, 0) 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') { playAnimation (entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
const playerObject = this.getPlayerObject(entityPlayerId) const playerObject = this.getPlayerObject(entityPlayerId)
if (!playerObject) return if (!playerObject) return
@ -594,7 +611,7 @@ export class Entities {
if (previousModel && previousModel === textureUv?.modelName) return undefined if (previousModel && previousModel === textureUv?.modelName) return undefined
if (textureUv && 'resolvedModel' in textureUv) { 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 let SCALE = 1
if (specificProps['minecraft:display_context'] === 'ground') { if (specificProps['minecraft:display_context'] === 'ground') {
SCALE = 0.5 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 justAdded = !this.entities[entity.id]
const isPlayerModel = entity.name === 'player' const isPlayerModel = entity.name === 'player'
@ -703,9 +720,10 @@ export class Entities {
return return
} }
let mesh let mesh: THREE.Object3D | undefined
if (e === 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') { if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block') {
const item = entity.name === 'tnt' const item = entity.name === 'tnt'
? { name: 'tnt' } ? { name: 'tnt' }
@ -732,7 +750,7 @@ export class Entities {
if (entity.name === 'item') { if (entity.name === 'item') {
mesh.onBeforeRender = () => { mesh.onBeforeRender = () => {
const delta = clock.getDelta() const delta = clock.getDelta()
mesh.rotation.y += delta mesh!.rotation.y += delta
} }
} }
@ -756,7 +774,6 @@ export class Entities {
// } // }
// } // }
//@ts-expect-error
group.additionalCleanup = () => { group.additionalCleanup = () => {
// important: avoid texture memory leak and gpu slowdown // important: avoid texture memory leak and gpu slowdown
object.itemsTexture?.dispose() object.itemsTexture?.dispose()
@ -795,7 +812,6 @@ export class Entities {
wrapper.add(nameTag) wrapper.add(nameTag)
} }
//@ts-expect-error
group.playerObject = playerObject group.playerObject = playerObject
wrapper.rotation.set(0, Math.PI, 0) wrapper.rotation.set(0, Math.PI, 0)
mesh = wrapper mesh = wrapper
@ -808,7 +824,8 @@ export class Entities {
if (!mesh) return if (!mesh) return
mesh.name = 'mesh' mesh.name = 'mesh'
// set initial position so there are no weird jumps update after // 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 // todo use width and height instead
const boxHelper = new THREE.BoxHelper( const boxHelper = new THREE.BoxHelper(
@ -856,7 +873,7 @@ export class Entities {
//@ts-expect-error //@ts-expect-error
// set visibility // set visibility
const isInvisible = entity.metadata?.[0] & 0x20 const isInvisible = entity.metadata?.[0] & 0x20
for (const child of mesh.children ?? []) { for (const child of mesh!.children ?? []) {
if (child.name !== 'nametag') { if (child.name !== 'nametag') {
child.visible = !isInvisible child.visible = !isInvisible
} }
@ -895,8 +912,8 @@ export class Entities {
const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0 const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0
const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0 const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0
const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0 const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0
mesh.castShadow = !isMarker mesh!.castShadow = !isMarker
mesh.receiveShadow = !isMarker mesh!.receiveShadow = !isMarker
if (isSmall) { if (isSmall) {
e.scale.set(0.5, 0.5, 0.5) e.scale.set(0.5, 0.5, 0.5)
} else { } else {
@ -965,7 +982,9 @@ export class Entities {
// TODO: fix type // TODO: fix type
// todo! fix errors in mc-data (no entities data prior 1.18.2) // 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 } } } } 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.rotation.x = -entity.pitch
e.children.find(c => { e.children.find(c => {
if (c.name.startsWith('map_')) { if (c.name.startsWith('map_')) {
@ -982,25 +1001,33 @@ export class Entities {
} }
return false return false
})?.removeFromParent() })?.removeFromParent()
if (item && (item.itemId ?? item.blockId ?? 0) !== 0) { 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 rotation = (itemFrameMeta.rotation as any as number) ?? 0
const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data
if (mapNumber) { if (mapNumber) {
// TODO: Use proper larger item frame model when a map exists // 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) this.addMapModel(e, mapNumber, rotation)
} else { } else {
// Handle regular item rotation (8 possibilities, 45° increments)
const itemMesh = this.getItemMesh(item, { const itemMesh = this.getItemMesh(item, {
'minecraft:display_context': 'fixed', 'minecraft:display_context': 'fixed',
}) })
if (itemMesh) { 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) { if (itemMesh.isBlock) {
itemMesh.mesh.scale.set(0.25, 0.25, 0.25) itemMesh.mesh.scale.set(0.25, 0.25, 0.25)
} else { } else {
itemMesh.mesh.scale.set(0.5, 0.5, 0.5) itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
} }
// Rotate 180° around Y axis first
itemMesh.mesh.rotateY(Math.PI) itemMesh.mesh.rotateY(Math.PI)
// Then apply the 45° increment rotation
itemMesh.mesh.rotateZ(-rotation * Math.PI / 4) itemMesh.mesh.rotateZ(-rotation * Math.PI / 4)
itemMesh.mesh.name = 'item' itemMesh.mesh.name = 'item'
e.add(itemMesh.mesh) e.add(itemMesh.mesh)
@ -1115,6 +1142,7 @@ export class Entities {
} else { } else {
mapMesh.position.set(0, 0, 0.437) 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.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
mapMesh.name = `map_${mapNumber}` mapMesh.name = `map_${mapNumber}`
@ -1267,7 +1295,7 @@ function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Obj
if (!texturePath) { if (!texturePath) {
// TODO: Support mirroring on certain parts of the model // TODO: Support mirroring on certain parts of the model
const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}` 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]) { if (!texturePath || !armorModel[slotType]) {
removeArmorModel(entityMesh, slotType) removeArmorModel(entityMesh, slotType)

View file

@ -238,10 +238,11 @@ export function getMesh (
if (useBlockTexture) { if (useBlockTexture) {
if (!worldRenderer) throw new Error('worldRenderer is required for block textures') if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
const blockName = texture.slice(6) const blockName = texture.slice(6)
const textureInfo = worldRenderer.resourcesManager.currentResources!.blocksAtlasParser.getTextureInfo(blockName) const textureInfo = worldRenderer.resourcesManager.currentResources.blocksAtlasJson.textures[blockName]
if (textureInfo) { if (textureInfo) {
textureWidth = blocksTexture?.image.width ?? textureWidth textureWidth = blocksTexture?.image.width ?? textureWidth
textureHeight = blocksTexture?.image.height ?? textureHeight textureHeight = blocksTexture?.image.height ?? textureHeight
// todo support su/sv
textureOffset = [textureInfo.u, textureInfo.v] textureOffset = [textureInfo.u, textureInfo.v]
} else { } else {
console.error(`Unknown block ${blockName}`) console.error(`Unknown block ${blockName}`)
@ -546,4 +547,4 @@ export class EntityMesh {
} }
} }
} }
window.EntityMesh = EntityMesh globalThis.EntityMesh = EntityMesh

View file

@ -5,6 +5,7 @@ import { ProgressReporter } from '../../../src/core/progressReporter'
import { showNotification } from '../../../src/react/NotificationProvider' import { showNotification } from '../../../src/react/NotificationProvider'
import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug' import { displayEntitiesDebugList } from '../../playground/allEntitiesDebug'
import supportedVersions from '../../../src/supportedVersions.mjs' import supportedVersions from '../../../src/supportedVersions.mjs'
import { ResourcesManager } from '../../../src/resourcesManager'
import { WorldRendererThree } from './worldrendererThree' import { WorldRendererThree } from './worldrendererThree'
import { DocumentRenderer } from './documentRenderer' import { DocumentRenderer } from './documentRenderer'
import { PanoramaRenderer } from './panorama' 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 // https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
THREE.ColorManagement.enabled = false THREE.ColorManagement.enabled = false
window.THREE = THREE globalThis.THREE = THREE
const getBackendMethods = (worldRenderer: WorldRendererThree) => { const getBackendMethods = (worldRenderer: WorldRendererThree) => {
return { return {
@ -24,7 +25,7 @@ const getBackendMethods = (worldRenderer: WorldRendererThree) => {
updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities), updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities),
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer), changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer), getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
rerenderAllChunks: worldRenderer.rerenderAllChunks.bind(worldRenderer), reloadWorld: worldRenderer.reloadWorld.bind(worldRenderer),
addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media), addMedia: worldRenderer.media.addMedia.bind(worldRenderer.media),
destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media), destroyMedia: worldRenderer.media.destroyMedia.bind(worldRenderer.media),
@ -57,31 +58,27 @@ const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitO
let worldRenderer: WorldRendererThree | null = null let worldRenderer: WorldRendererThree | null = null
const startPanorama = async () => { const startPanorama = async () => {
if (!documentRenderer) throw new Error('Document renderer not initialized')
if (worldRenderer) return if (worldRenderer) return
const qs = new URLSearchParams(window.location.search) const qs = new URLSearchParams(location.search)
if (qs.get('debugEntities')) { if (qs.get('debugEntities')) {
initOptions.resourcesManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true } const fullResourceManager = initOptions.resourcesManager as ResourcesManager
await initOptions.resourcesManager.updateAssetsData({ }) fullResourceManager.currentConfig = { version: qs.get('version') || supportedVersions.at(-1)!, noInventoryGui: true }
await fullResourceManager.updateAssetsData({ })
displayEntitiesDebugList(initOptions.resourcesManager.currentConfig.version) displayEntitiesDebugList(fullResourceManager.currentConfig.version)
return return
} }
if (!panoramaRenderer) { if (!panoramaRenderer) {
panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE) panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE)
window.panoramaRenderer = panoramaRenderer globalThis.panoramaRenderer = panoramaRenderer
callModsMethod('panoramaCreated', panoramaRenderer) callModsMethod('panoramaCreated', panoramaRenderer)
await panoramaRenderer.start() await panoramaRenderer.start()
callModsMethod('panoramaReady', panoramaRenderer) callModsMethod('panoramaReady', panoramaRenderer)
} }
} }
let version = ''
const prepareResources = async (ver: string, progressReporter: ProgressReporter): Promise<void> => {
version = ver
await initOptions.resourcesManager.updateAssetsData({ })
}
const startWorld = async (displayOptions: DisplayWorldOptions) => { const startWorld = async (displayOptions: DisplayWorldOptions) => {
if (panoramaRenderer) { if (panoramaRenderer) {
panoramaRenderer.dispose() panoramaRenderer.dispose()

View file

@ -5,7 +5,7 @@ import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBloc
import { BlockModel } from 'mc-assets' import { BlockModel } from 'mc-assets'
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer' import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer'
import { getMyHand } from '../lib/hand' import { getMyHand } from '../lib/hand'
import { IPlayerState, MovementState } from '../lib/basePlayerState' import { MovementState, PlayerStateRenderer } from '../lib/basePlayerState'
import { DebugGui } from '../lib/DebugGui' import { DebugGui } from '../lib/DebugGui'
import { SmoothSwitcher } from '../lib/smoothSwitcher' import { SmoothSwitcher } from '../lib/smoothSwitcher'
import { watchProperty } from '../lib/utils/proxy' import { watchProperty } from '../lib/utils/proxy'
@ -116,16 +116,22 @@ export default class HoldingBlock {
offHandModeLegacy = false offHandModeLegacy = false
swingAnimator: HandSwingAnimator | undefined swingAnimator: HandSwingAnimator | undefined
playerState: IPlayerState playerState: PlayerStateRenderer
config: WorldRendererConfig config: WorldRendererConfig
constructor (public worldRenderer: WorldRendererThree, public offHand = false) { constructor (public worldRenderer: WorldRendererThree, public offHand = false) {
this.initCameraGroup() this.initCameraGroup()
this.playerState = worldRenderer.displayOptions.playerState this.playerState = worldRenderer.displayOptions.playerState
this.playerState.events.on('heldItemChanged', (_, isOffHand) => { this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
if (this.offHand !== isOffHand) return if (!this.offHand) {
this.updateItem() this.updateItem()
}) }
}, false)
this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => {
if (this.offHand) {
this.updateItem()
}
}, false)
this.config = worldRenderer.displayOptions.inWorldRenderingConfig this.config = worldRenderer.displayOptions.inWorldRenderingConfig
this.offHandDisplay = this.offHand this.offHandDisplay = this.offHand
@ -134,17 +140,21 @@ export default class HoldingBlock {
// load default hand // load default hand
void getMyHand().then((hand) => { void getMyHand().then((hand) => {
this.playerHand = hand this.playerHand = hand
// trigger update
this.updateItem()
}).then(() => { }).then(() => {
// now watch over the player skin // now watch over the player skin
watchProperty( watchProperty(
async () => { 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, this.playerState.reactive,
'playerSkin', 'playerSkin',
(newHand) => { (newHand) => {
if (newHand) { if (newHand) {
this.playerHand = newHand this.playerHand = newHand
// trigger update
this.updateItem()
} }
}, },
(oldHand) => { (oldHand) => {
@ -156,8 +166,8 @@ export default class HoldingBlock {
} }
updateItem () { updateItem () {
if (!this.ready || !this.playerState.getHeldItem) return if (!this.ready) return
const item = this.playerState.getHeldItem(this.offHand) const item = this.offHand ? this.playerState.reactive.heldItemOff : this.playerState.reactive.heldItemMain
if (item) { if (item) {
void this.setNewItem(item) void this.setNewItem(item)
} else if (this.offHand) { } else if (this.offHand) {
@ -347,8 +357,8 @@ export default class HoldingBlock {
itemId: handItem.id, itemId: handItem.id,
}, { }, {
'minecraft:display_context': 'firstperson', 'minecraft:display_context': 'firstperson',
'minecraft:use_duration': this.playerState.getItemUsageTicks?.(), 'minecraft:use_duration': this.playerState.reactive.itemUsageTicks,
'minecraft:using_item': !!this.playerState.getItemUsageTicks?.(), 'minecraft:using_item': !!this.playerState.reactive.itemUsageTicks,
}, this.lastItemModelName) }, this.lastItemModelName)
if (result) { if (result) {
const { mesh: itemMesh, isBlock, modelName } = result const { mesh: itemMesh, isBlock, modelName } = result
@ -546,7 +556,7 @@ class HandIdleAnimator {
private readonly debugGui: DebugGui private readonly debugGui: DebugGui
constructor (public handMesh: THREE.Object3D, public playerState: IPlayerState) { constructor (public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) {
this.handMesh = handMesh this.handMesh = handMesh
this.globalTime = 0 this.globalTime = 0
this.currentState = 'NOT_MOVING' this.currentState = 'NOT_MOVING'
@ -700,7 +710,7 @@ class HandIdleAnimator {
// Check for state changes from player state // Check for state changes from player state
if (this.playerState) { if (this.playerState) {
const newState = this.playerState.getMovementState() const newState = this.playerState.reactive.movementState
if (newState !== this.targetState) { if (newState !== this.targetState) {
this.setState(newState) this.setState(newState)
} }

View file

@ -6,8 +6,10 @@ import * as tweenJs from '@tweenjs/tween.js'
import type { GraphicsInitOptions } from '../../../src/appViewer' import type { GraphicsInitOptions } from '../../../src/appViewer'
import { WorldDataEmitter } from '../lib/worldDataEmitter' import { WorldDataEmitter } from '../lib/worldDataEmitter'
import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon' import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon'
import { BasePlayerState } from '../lib/basePlayerState'
import { getDefaultRendererState } from '../baseGraphicsBackend' 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 { WorldRendererThree } from './worldrendererThree'
import { EntityMesh } from './entity/EntityMesh' import { EntityMesh } from './entity/EntityMesh'
import { DocumentRenderer } from './documentRenderer' import { DocumentRenderer } from './documentRenderer'
@ -48,7 +50,7 @@ export class PanoramaRenderer {
this.directionalLight.castShadow = true this.directionalLight.castShadow = true
this.scene.add(this.directionalLight) 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.position.set(0, 0, 0)
this.camera.rotation.set(0, 0, 0) this.camera.rotation.set(0, 0, 0)
} }
@ -63,47 +65,57 @@ export class PanoramaRenderer {
this.documentRenderer.render = (sizeChanged = false) => { this.documentRenderer.render = (sizeChanged = false) => {
if (sizeChanged) { if (sizeChanged) {
this.camera.aspect = window.innerWidth / window.innerHeight this.camera.aspect = this.documentRenderer.canvas.width / this.documentRenderer.canvas.height
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
} }
this.documentRenderer.renderer.render(this.scene, this.camera) 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 () { addClassicPanorama () {
const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000) const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
const loader = new THREE.TextureLoader()
const panorMaterials = [] as THREE.MeshBasicMaterial[] const panorMaterials = [] as THREE.MeshBasicMaterial[]
const fadeInDuration = 200 const fadeInDuration = 200
for (const file of panoramaFiles) { // void this.debugImageInFrontOfCamera()
// eslint-disable-next-line prefer-const
let material: THREE.MeshBasicMaterial 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 // Start fade-in when texture is loaded
this.startTimes.set(material, Date.now()) this.startTimes.set(material, Date.now())
}) panorMaterials.push(material)
}
// Instead of using repeat/offset to flip, we'll use the texture matrix void load()
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)
} }
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials) const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
@ -145,8 +157,9 @@ export class PanoramaRenderer {
async worldBlocksPanorama () { async worldBlocksPanorama () {
const version = '1.21.4' const version = '1.21.4'
this.options.resourcesManager.currentConfig = { version, noInventoryGui: true, } const fullResourceManager = this.options.resourcesManager as ResourcesManager
await this.options.resourcesManager.updateAssetsData({ }) fullResourceManager.currentConfig = { version, noInventoryGui: true, }
await fullResourceManager.updateAssetsData({ })
if (this.abortController.signal.aborted) return if (this.abortController.signal.aborted) return
console.time('load panorama scene') console.time('load panorama scene')
const world = getSyncWorld(version) const world = getSyncWorld(version)
@ -184,9 +197,9 @@ export class PanoramaRenderer {
version, version,
worldView, worldView,
inWorldRenderingConfig: defaultWorldRendererConfig, inWorldRenderingConfig: defaultWorldRendererConfig,
playerState: new BasePlayerState(), playerState: getInitialPlayerStateRenderer(),
rendererState: getDefaultRendererState(), rendererState: getDefaultRendererState().reactive,
nonReactiveState: getDefaultRendererState() nonReactiveState: getDefaultRendererState().nonReactive
} }
) )
if (this.worldRenderer instanceof WorldRendererThree) { if (this.worldRenderer instanceof WorldRendererThree) {

View file

@ -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<string, { slice, path }> & { 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
}
}
}

View file

@ -15,6 +15,7 @@ import destroyStage6 from '../../../../assets/destroy_stage_6.png'
import destroyStage7 from '../../../../assets/destroy_stage_7.png' import destroyStage7 from '../../../../assets/destroy_stage_7.png'
import destroyStage8 from '../../../../assets/destroy_stage_8.png' import destroyStage8 from '../../../../assets/destroy_stage_8.png'
import destroyStage9 from '../../../../assets/destroy_stage_9.png' import destroyStage9 from '../../../../assets/destroy_stage_9.png'
import { loadThreeJsTextureFromUrl } from '../../lib/utils/skins'
export class CursorBlock { export class CursorBlock {
_cursorLinesHidden = false _cursorLinesHidden = false
@ -36,17 +37,17 @@ export class CursorBlock {
constructor (public readonly worldRenderer: WorldRendererThree) { constructor (public readonly worldRenderer: WorldRendererThree) {
// Initialize break mesh and textures // Initialize break mesh and textures
const loader = new THREE.TextureLoader()
const destroyStagesImages = [ const destroyStagesImages = [
destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4, destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4,
destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9 destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9
] ]
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const texture = loader.load(destroyStagesImages[i]) void loadThreeJsTextureFromUrl(destroyStagesImages[i]).then((texture) => {
texture.magFilter = THREE.NearestFilter texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter texture.minFilter = THREE.NearestFilter
this.breakTextures.push(texture) this.breakTextures.push(texture)
})
} }
const breakMaterial = new THREE.MeshBasicMaterial({ const breakMaterial = new THREE.MeshBasicMaterial({

View file

@ -3,17 +3,16 @@ import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt' import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat' import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js' import * as tweenJs from '@tweenjs/tween.js'
import { subscribeKey } from 'valtio/utils'
import { renderSign } from '../sign-renderer' 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 { chunkPos, sectionPos } from '../lib/simpleUtils'
import { WorldRendererCommon } from '../lib/worldrendererCommon' 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 { MesherGeometryOutput } from '../lib/mesher/shared'
import { ItemSpecificContextProperties } from '../lib/basePlayerState' import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { getMyHand } from '../lib/hand' import { getMyHand } from '../lib/hand'
import { setBlockPosition } from '../lib/mesher/standaloneRenderer' import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
import { sendVideoPlay, sendVideoStop } from '../../../src/customChannels' import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins'
import HoldingBlock from './holdingBlock' import HoldingBlock from './holdingBlock'
import { getMesh } from './entity/EntityMesh' import { getMesh } from './entity/EntityMesh'
import { armorModel } from './entity/armorModels' import { armorModel } from './entity/armorModels'
@ -44,7 +43,7 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraGroupVr?: THREE.Object3D cameraGroupVr?: THREE.Object3D
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 }) material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
itemsTexture: THREE.Texture itemsTexture: THREE.Texture
cursorBlock = new CursorBlock(this) cursorBlock: CursorBlock
onRender: Array<() => void> = [] onRender: Array<() => void> = []
cameraShake: CameraShake cameraShake: CameraShake
media: ThreeJsMedia media: ThreeJsMedia
@ -82,8 +81,10 @@ export class WorldRendererThree extends WorldRendererCommon {
if (!initOptions.resourcesManager) throw new Error('resourcesManager is required') if (!initOptions.resourcesManager) throw new Error('resourcesManager is required')
super(initOptions.resourcesManager, displayOptions, initOptions) super(initOptions.resourcesManager, displayOptions, initOptions)
this.renderer = renderer
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...' displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
this.starField = new StarField(this.scene) this.starField = new StarField(this.scene)
this.cursorBlock = new CursorBlock(this)
this.holdingBlock = new HoldingBlock(this) this.holdingBlock = new HoldingBlock(this)
this.holdingBlockLeft = new HoldingBlock(this, true) this.holdingBlockLeft = new HoldingBlock(this, true)
@ -148,21 +149,21 @@ export class WorldRendererThree extends WorldRendererCommon {
override watchReactivePlayerState () { override watchReactivePlayerState () {
super.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.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 if (!value) return
this.ambientLight.intensity = value this.ambientLight.intensity = value
}) })
this.onReactiveValueUpdated('directionalLight', (value) => { this.onReactivePlayerStateUpdated('directionalLight', (value) => {
if (!value) return if (!value) return
this.directionalLight.intensity = value 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.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) 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<void> { async updateAssetsData (): Promise<void> {
const resources = this.resourcesManager.currentResources! const resources = this.resourcesManager.currentResources
const oldTexture = this.material.map const oldTexture = this.material.map
const oldItemsTexture = this.itemsTexture const oldItemsTexture = this.itemsTexture
const texture = await new THREE.TextureLoader().loadAsync(resources.blocksAtlasParser.latestImage) const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage)
texture.magFilter = THREE.NearestFilter texture.needsUpdate = true
texture.minFilter = THREE.NearestFilter
texture.flipY = false texture.flipY = false
this.material.map = texture this.material.map = texture
const itemsTexture = await new THREE.TextureLoader().loadAsync(resources.itemsAtlasParser.latestImage) const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage)
itemsTexture.magFilter = THREE.NearestFilter itemsTexture.needsUpdate = true
itemsTexture.minFilter = THREE.NearestFilter
itemsTexture.flipY = false itemsTexture.flipY = false
this.itemsTexture = itemsTexture this.itemsTexture = itemsTexture
@ -239,7 +238,7 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) { getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
return getItemUv(item, specificProps, this.resourcesManager) return getItemUv(item, specificProps, this.resourcesManager, this.playerState)
} }
async demoModel () { async demoModel () {
@ -431,7 +430,7 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) { 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.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
this.media.tryIntersectMedia() this.media.tryIntersectMedia()
@ -495,7 +494,8 @@ export class WorldRendererThree extends WorldRendererCommon {
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
if (sizeOrFovChanged) { 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.fov = this.displayOptions.inWorldRenderingConfig.fov
this.camera.updateProjectionMatrix() 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 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) 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.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
this.holdingBlockLeft.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() 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 () { updateSectionOffsets () {
const currentTime = performance.now() const currentTime = performance.now()
for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) { for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) {
@ -756,6 +768,10 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
} }
} }
reloadWorld () {
this.entities.reloadEntities()
}
} }
class StarField { class StarField {

View file

@ -1,25 +1,26 @@
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter' import { WorldDataEmitter, WorldDataEmitterWorker } from 'renderer/viewer/lib/worldDataEmitter'
import { BasePlayerState, IPlayerState } from 'renderer/viewer/lib/basePlayerState' import { PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
import { subscribeKey } from 'valtio/utils' import { subscribeKey } from 'valtio/utils'
import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon' import { defaultWorldRendererConfig, WorldRendererConfig } from 'renderer/viewer/lib/worldrendererCommon'
import { Vec3 } from 'vec3' import { Vec3 } from 'vec3'
import { SoundSystem } from 'renderer/viewer/three/threeJsSound' import { SoundSystem } from 'renderer/viewer/three/threeJsSound'
import { proxy } from 'valtio' import { proxy, subscribe } from 'valtio'
import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend' import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend'
import { getSyncWorld } from 'renderer/playground/shared' import { getSyncWorld } from 'renderer/playground/shared'
import { MaybePromise } from 'contro-max/build/types/store'
import { playerState } from './mineflayer/playerState' import { playerState } from './mineflayer/playerState'
import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter' import { createNotificationProgressReporter, ProgressReporter } from './core/progressReporter'
import { setLoadingScreenStatus } from './appStatus' import { setLoadingScreenStatus } from './appStatus'
import { activeModalStack, miscUiState } from './globalState' import { activeModalStack, miscUiState } from './globalState'
import { options } from './optionsStorage' import { options } from './optionsStorage'
import { ResourcesManager } from './resourcesManager' import { ResourcesManager, ResourcesManagerTransferred } from './resourcesManager'
import { watchOptionsAfterWorldViewInit } from './watchOptions' import { watchOptionsAfterWorldViewInit } from './watchOptions'
export interface RendererReactiveState { export interface RendererReactiveState {
world: { world: {
chunksLoaded: Set<string> chunksLoaded: Set<string>
// chunksTotalNumber: number
heightmaps: Map<string, Uint8Array> heightmaps: Map<string, Uint8Array>
chunksTotalNumber: number
allChunksLoaded: boolean allChunksLoaded: boolean
mesherWork: boolean mesherWork: boolean
intersectMedia: { id: string, x: number, y: number } | null intersectMedia: { id: string, x: number, y: number } | null
@ -31,9 +32,6 @@ export interface NonReactiveState {
world: { world: {
chunksLoaded: Set<string> chunksLoaded: Set<string>
chunksTotalNumber: number 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' powerPreference?: 'high-performance' | 'low-power'
statsVisible?: number statsVisible?: number
sceneBackground: string sceneBackground: string
timeoutRendering?: boolean
} }
const defaultGraphicsBackendConfig: GraphicsBackendConfig = { const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
fpsLimit: undefined, fpsLimit: undefined,
powerPreference: undefined, powerPreference: undefined,
sceneBackground: 'lightblue' sceneBackground: 'lightblue',
timeoutRendering: false
} }
export interface GraphicsInitOptions<S = any> { export interface GraphicsInitOptions<S = any> {
resourcesManager: ResourcesManager resourcesManager: ResourcesManagerTransferred
config: GraphicsBackendConfig config: GraphicsBackendConfig
rendererSpecificSettings: S rendererSpecificSettings: S
displayCriticalError: (error: Error) => void callbacks: {
setRendererSpecificSettings: (key: string, value: any) => void displayCriticalError: (error: Error) => void
setRendererSpecificSettings: (key: string, value: any) => void
fireCustomEvent: (eventName: string, ...args: any[]) => void
}
} }
export interface DisplayWorldOptions { export interface DisplayWorldOptions {
version: string version: string
worldView: WorldDataEmitter worldView: WorldDataEmitterWorker
inWorldRenderingConfig: WorldRendererConfig inWorldRenderingConfig: WorldRendererConfig
playerState: IPlayerState playerState: PlayerStateRenderer
rendererState: RendererReactiveState rendererState: RendererReactiveState
nonReactiveState: NonReactiveState nonReactiveState: NonReactiveState
} }
export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => GraphicsBackend) & { export type GraphicsBackendLoader = ((options: GraphicsInitOptions) => MaybePromise<GraphicsBackend>) & {
id: string id: string
} }
@ -108,8 +112,8 @@ export class AppViewer {
inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig) inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig)
lastCamUpdate = 0 lastCamUpdate = 0
playerState = playerState playerState = playerState
rendererState = proxy(getDefaultRendererState()) rendererState = proxy(getDefaultRendererState().reactive)
nonReactiveState: NonReactiveState = getDefaultRendererState() nonReactiveState: NonReactiveState = getDefaultRendererState().nonReactive
worldReady: Promise<void> worldReady: Promise<void>
private resolveWorldReady: () => void private resolveWorldReady: () => void
@ -133,19 +137,24 @@ export class AppViewer {
rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key] rendererSpecificSettings[key.slice(rendererSettingsKey.length + 1)] = options[key]
} }
} }
const loaderOptions: GraphicsInitOptions = { const loaderOptions: GraphicsInitOptions = { // todo!
resourcesManager: this.resourcesManager, resourcesManager: this.resourcesManager as ResourcesManagerTransferred,
config: this.config, config: this.config,
displayCriticalError (error) { callbacks: {
console.error(error) displayCriticalError (error) {
setLoadingScreenStatus(error.message, true) 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, rendererSpecificSettings,
setRendererSpecificSettings (key: string, value: any) {
options[`${rendererSettingsKey}.${key}`] = value
}
} }
this.backend = loader(loaderOptions) this.backend = await loader(loaderOptions)
// if (this.resourcesManager.currentResources) { // if (this.resourcesManager.currentResources) {
// void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter()) // void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter())
@ -156,9 +165,13 @@ export class AppViewer {
const { method, args } = this.currentState const { method, args } = this.currentState
this.backend[method](...args) this.backend[method](...args)
if (method === 'startWorld') { if (method === 'startWorld') {
void this.worldView!.init(bot.entity.position)
// void this.worldView!.init(args[0].playerState.getPosition()) // void this.worldView!.init(args[0].playerState.getPosition())
} }
} }
// todo
modalStackUpdateChecks()
} }
async startWithBot () { async startWithBot () {
@ -167,10 +180,10 @@ export class AppViewer {
this.worldView!.listenToBot(bot) 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') if (this.currentDisplay === 'world') throw new Error('World already started')
this.currentDisplay = 'world' this.currentDisplay = 'world'
const startPosition = playerStateSend.getPosition() const startPosition = bot.entity?.position ?? new Vec3(0, 64, 0)
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition) this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
window.worldView = this.worldView window.worldView = this.worldView
watchOptionsAfterWorldViewInit(this.worldView) watchOptionsAfterWorldViewInit(this.worldView)
@ -238,7 +251,8 @@ export class AppViewer {
const { promise, resolve } = Promise.withResolvers<void>() const { promise, resolve } = Promise.withResolvers<void>()
this.worldReady = promise this.worldReady = promise
this.resolveWorldReady = resolve this.resolveWorldReady = resolve
this.rendererState = proxy(getDefaultRendererState()) this.rendererState = proxy(getDefaultRendererState().reactive)
this.nonReactiveState = getDefaultRendererState().nonReactive
// this.queuedDisplay = undefined // 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() export const appViewer = new AppViewer()
window.appViewer = appViewer window.appViewer = appViewer
@ -284,7 +299,7 @@ window.initialMenuStart = initialMenuStart
const modalStackUpdateChecks = () => { const modalStackUpdateChecks = () => {
// maybe start panorama // maybe start panorama
if (activeModalStack.length === 0 && !miscUiState.gameLoaded) { if (!miscUiState.gameLoaded) {
void initialMenuStart() void initialMenuStart()
} }
@ -295,5 +310,4 @@ const modalStackUpdateChecks = () => {
appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0 appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0
} }
subscribeKey(activeModalStack, 'length', modalStackUpdateChecks) subscribe(activeModalStack, modalStackUpdateChecks)
modalStackUpdateChecks()

View file

@ -263,7 +263,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string)
return true return true
} }
export async function removeFileRecursiveAsync (path) { export async function removeFileRecursiveAsync (path, removeDirectoryItself = true) {
const errors = [] as Array<[string, Error]> const errors = [] as Array<[string, Error]>
try { try {
const files = await fs.promises.readdir(path) 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 // After removing all files/directories, remove the current directory
await fs.promises.rmdir(path) if (removeDirectoryItself) {
await fs.promises.rmdir(path)
}
} catch (error) { } catch (error) {
errors.push([path, error]) errors.push([path, error])
} }

View file

@ -661,6 +661,9 @@ export const f3Keybinds: Array<{
localServer.players[0].world.columns = {} localServer.players[0].world.columns = {}
} }
void reloadChunks() void reloadChunks()
if (appViewer.backend?.backendMethods && typeof appViewer.backend.backendMethods.reloadWorld === 'function') {
appViewer.backend.backendMethods.reloadWorld()
}
}, },
mobileTitle: 'Reload chunks', mobileTitle: 'Reload chunks',
}, },

View file

@ -74,7 +74,7 @@ import { showNotification } from './react/NotificationProvider'
import { saveToBrowserMemory } from './react/PauseScreen' import { saveToBrowserMemory } from './react/PauseScreen'
import './devReload' import './devReload'
import './water' import './water'
import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect' import { ConnectOptions, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData, loadMinecraftData } from './connect'
import { ref, subscribe } from 'valtio' import { ref, subscribe } from 'valtio'
import { signInMessageState } from './react/SignInMessageProvider' import { signInMessageState } from './react/SignInMessageProvider'
import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage' import { findServerPassword, updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage'
@ -331,6 +331,7 @@ export async function connect (connectOptions: ConnectOptions) {
await progress.executeWithMessage( await progress.executeWithMessage(
'Processing downloaded Minecraft data', 'Processing downloaded Minecraft data',
async () => { async () => {
await loadMinecraftData(version)
await appViewer.resourcesManager.loadSourceData(version) await appViewer.resourcesManager.loadSourceData(version)
} }
) )
@ -448,17 +449,20 @@ export async function connect (connectOptions: ConnectOptions) {
let newTokensCacheResult = null as any let newTokensCacheResult = null as any
const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {} const cachedTokens = typeof connectOptions.authenticatedAccount === 'object' ? connectOptions.authenticatedAccount.cachedTokens : {}
const authData = connectOptions.authenticatedAccount ? await microsoftAuthflow({ let authData: Awaited<ReturnType<typeof microsoftAuthflow>> | undefined
tokenCaches: cachedTokens, if (connectOptions.authenticatedAccount) {
proxyBaseUrl: connectOptions.proxy, authData = await microsoftAuthflow({
setProgressText (text) { tokenCaches: cachedTokens,
progress.setMessage(text) proxyBaseUrl: connectOptions.proxy,
}, setProgressText (text) {
setCacheResult (result) { progress.setMessage(text)
newTokensCacheResult = result },
}, setCacheResult (result) {
connectingServer: server.host newTokensCacheResult = result
}) : undefined },
connectingServer: server.host
})
}
if (p2pMultiplayer) { if (p2pMultiplayer) {
clientDataStream = await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions) 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 // "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 }) as unknown as typeof __type_bot
window.bot = bot window.bot = bot
if (connectOptions.viewerWsConnect) { if (connectOptions.viewerWsConnect) {
void onBotCreatedViewerHandler() void onBotCreatedViewerHandler()
} }
@ -691,6 +696,7 @@ export async function connect (connectOptions: ConnectOptions) {
onBotCreate() onBotCreate()
bot.once('login', () => { bot.once('login', () => {
errorAbortController.abort()
loadingTimerState.networkOnlyStart = 0 loadingTimerState.networkOnlyStart = 0
progress.setMessage('Loading world') progress.setMessage('Loading world')
}) })
@ -708,7 +714,7 @@ export async function connect (connectOptions: ConnectOptions) {
resolve() resolve()
unsub() unsub()
} else { } 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) progress?.reportProgress('chunks', perc / 100)
} }
}) })
@ -727,9 +733,12 @@ export async function connect (connectOptions: ConnectOptions) {
}) })
await appViewer.resourcesManager.promiseAssetsReady await appViewer.resourcesManager.promiseAssetsReady
} }
errorAbortController.abort()
if (appStatusState.isError) return if (appStatusState.isError) return
if (!appViewer.resourcesManager.currentResources?.itemsRenderer) {
await appViewer.resourcesManager.updateAssetsData({})
}
const loadWorldStart = Date.now() const loadWorldStart = Date.now()
console.log('try to focus window') console.log('try to focus window')
window.focus?.() window.focus?.()
@ -741,7 +750,7 @@ export async function connect (connectOptions: ConnectOptions) {
try { try {
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
playerState.onlineMode = !!connectOptions.authenticatedAccount playerState.reactive.onlineMode = !!connectOptions.authenticatedAccount
progress.setMessage('Placing blocks (starting viewer)') progress.setMessage('Placing blocks (starting viewer)')
if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) { 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') console.log('bot spawned - starting viewer')
await appViewer.startWorld(bot.world, renderDistance) await appViewer.startWorld(bot.world, renderDistance)
appViewer.worldView!.listenToBot(bot) appViewer.worldView!.listenToBot(bot)
if (appViewer.backend) {
void appViewer.worldView!.init(bot.entity.position)
}
initMotionTracking() initMotionTracking()
dayCycle() dayCycle()
@ -975,7 +987,7 @@ if (!reconnectOptions) {
} }
}) })
if (appQueryParams.serversList) { if (appQueryParams.serversList && !appQueryParams.ip) {
showModal({ reactType: 'serversList' }) showModal({ reactType: 'serversList' })
} }

View file

@ -10,7 +10,7 @@ import { versionToNumber } from 'renderer/viewer/common/utils'
import { getRenamedData } from 'flying-squid/dist/blockRenames' import { getRenamedData } from 'flying-squid/dist/blockRenames'
import PrismarineChatLoader from 'prismarine-chat' import PrismarineChatLoader from 'prismarine-chat'
import { BlockModel } from 'mc-assets' 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 Generic95 from '../assets/generic_95.png'
import { appReplacableResources } from './generated/resources' import { appReplacableResources } from './generated/resources'
import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState'
@ -21,6 +21,7 @@ import { currentScaling } from './scaleInterface'
import { getItemDescription } from './itemsDescriptions' import { getItemDescription } from './itemsDescriptions'
import { MessageFormatPart } from './chatUtils' import { MessageFormatPart } from './chatUtils'
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items' import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
import { playerState } from './mineflayer/playerState'
const loadedImagesCache = new Map<string, HTMLImageElement>() const loadedImagesCache = new Map<string, HTMLImageElement>()
const cleanLoadedImagesCache = () => { const cleanLoadedImagesCache = () => {
@ -134,8 +135,8 @@ export const onGameLoad = () => {
const getImageSrc = (path): string | HTMLImageElement => { const getImageSrc = (path): string | HTMLImageElement => {
switch (path) { switch (path) {
case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content
case 'blocks': return appViewer.resourcesManager.currentResources!.blocksAtlasParser.latestImage case 'blocks': return appViewer.resourcesManager.blocksAtlasParser.latestImage
case 'items': return appViewer.resourcesManager.currentResources!.itemsAtlasParser.latestImage case 'items': return appViewer.resourcesManager.itemsAtlasParser.latestImage
case 'gui/container/dispenser': return appReplacableResources.latest_gui_container_dispenser.content 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/furnace': return appReplacableResources.latest_gui_container_furnace.content
case 'gui/container/crafting_table': return appReplacableResources.latest_gui_container_crafting_table.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) return loadedImagesCache.get(loadPath)
} }
export type ResolvedItemModelRender = {
modelName: string,
originalItemName?: string
}
export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = false, fullBlockModelSupport = false): {
texture: string,
blockData?: Record<string, { slice, path }> & { 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 getItemName = (slot: Item | RenderItem | null) => {
const parsed = getItemNameRaw(slot, appViewer.resourcesManager) const parsed = getItemNameRaw(slot, appViewer.resourcesManager)
if (!parsed) return if (!parsed) return
@ -269,7 +197,7 @@ const itemToVisualKey = (slot: RenderItem | Item | null) => {
slot['metadata'], slot['metadata'],
slot.nbt ? JSON.stringify(slot.nbt) : '', slot.nbt ? JSON.stringify(slot.nbt) : '',
slot['components'] ? JSON.stringify(slot['components']) : '', slot['components'] ? JSON.stringify(slot['components']) : '',
activeGuiAtlas.version, appViewer.resourcesManager.currentResources!.guiAtlasVersion,
].join('|') ].join('|')
return keys return keys
} }
@ -289,8 +217,8 @@ const mapSlots = (slots: Array<RenderItem | Item | null>, isJei = false) => {
try { try {
if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability)
const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot
const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager) const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }, appViewer.resourcesManager, playerState)
const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, debugIsQuickbar) const slotCustomProps = renderSlot({ modelName, originalItemName: slot.name }, appViewer.resourcesManager, debugIsQuickbar)
const itemCustomName = getItemName(slot) const itemCustomName = getItemName(slot)
Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName }) Object.assign(slot, { ...slotCustomProps, displayName: itemCustomName ?? slot.displayName })
//@ts-expect-error //@ts-expect-error

View file

@ -1,11 +1,10 @@
import mojangson from 'mojangson' import mojangson from 'mojangson'
import nbt from 'prismarine-nbt' import nbt from 'prismarine-nbt'
import { fromFormattedString } from '@xmcl/text-component' 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 { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
import { MessageFormatPart } from '../chatUtils' import { MessageFormatPart } from '../chatUtils'
import { ResourcesManager } from '../resourcesManager' import { ResourcesManager, ResourcesManagerCommon, ResourcesManagerTransferred } from '../resourcesManager'
import { playerState } from './playerState'
type RenderSlotComponent = { type RenderSlotComponent = {
type: string, type: string,
@ -33,7 +32,7 @@ type PossibleItemProps = {
display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"} 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 customText = undefined as string | any | undefined
let customModel = undefined as string | undefined let customModel = undefined as string | undefined
@ -91,7 +90,7 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour
} }
export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | null, resourcesManager: ResourcesManager) => { export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | null, resourcesManager: ResourcesManagerCommon) => {
if (!item) return '' if (!item) return ''
const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager) const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager)
if (!customText) return if (!customText) return
@ -112,14 +111,14 @@ export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'>
} }
} }
export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager) => { export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerCommon, playerState: PlayerStateRenderer) => {
let itemModelName = item.name let itemModelName = item.name
const { customModel } = getItemMetadata(item, resourcesManager) const { customModel } = getItemMetadata(item, resourcesManager)
if (customModel) { if (customModel) {
itemModelName = customModel itemModelName = customModel
} }
const itemSelector = playerState.getItemSelector({ const itemSelector = getItemSelector(playerState, {
...specificProps ...specificProps
}) })
const modelFromDef = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, { const modelFromDef = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, {

View file

@ -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 { HandItemBlock } from 'renderer/viewer/three/holdingBlock'
import TypedEmitter from 'typed-emitter' import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
import { ItemSelector } from 'mc-assets/dist/itemDefinitions' import { subscribe } from 'valtio'
import { proxy } from 'valtio' import { subscribeKey } from 'valtio/utils'
import { gameAdditionalState } from '../globalState' 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 disableStateUpdates = false
private static instance: PlayerStateManager
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
// Movement and physics state
private lastVelocity = new Vec3(0, 0, 0)
private movementState: MovementState = 'NOT_MOVING'
private timeOffGround = 0 private timeOffGround = 0
private lastUpdateTime = performance.now() private lastUpdateTime = performance.now()
// Held item state // Held item state
private heldItem?: HandItemBlock
private offHandItem?: HandItemBlock
private itemUsageTicks = 0
private isUsingItem = false private isUsingItem = false
private ready = false ready = false
public lightingDisabled = false
onlineMode = false
get username () {
return bot.username ?? ''
}
reactive: IPlayerState['reactive'] = new BasePlayerState().reactive reactive: PlayerStateRenderer['reactive']
static getInstance (): PlayerStateManager {
if (!this.instance) {
this.instance = new PlayerStateManager()
}
return this.instance
}
constructor () { constructor () {
this.updateState = this.updateState.bind(this)
customEvents.on('mineflayerBotCreated', () => { customEvents.on('mineflayerBotCreated', () => {
this.ready = false this.ready = false
bot.on('inject_allowed', () => { bot.on('inject_allowed', () => {
@ -48,16 +28,27 @@ export class PlayerStateManager implements IPlayerState {
this.ready = true this.ready = true
this.botCreated() this.botCreated()
}) })
bot.on('end', () => {
this.ready = false
})
}) })
} }
private onBotCreatedOrGameJoined () {
this.reactive.username = bot.username ?? ''
}
private botCreated () { private botCreated () {
console.log('bot created & plugins injected')
this.reactive = getInitialPlayerState()
this.onBotCreatedOrGameJoined()
const handleDimensionData = (data) => { const handleDimensionData = (data) => {
let hasSkyLight = 1 let hasSkyLight = 1
try { try {
hasSkyLight = data.dimension.value.has_skylight.value hasSkyLight = data.dimension.value.has_skylight.value
} catch {} } 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) => { bot._client.on('login', (packet) => {
@ -68,7 +59,9 @@ export class PlayerStateManager implements IPlayerState {
}) })
// Movement tracking // Movement tracking
bot.on('move', this.updateState) bot.on('move', () => {
this.updateMovementState()
})
// Item tracking // Item tracking
bot.on('heldItemChanged', () => { bot.on('heldItemChanged', () => {
@ -77,8 +70,22 @@ export class PlayerStateManager implements IPlayerState {
bot.inventory.on('updateSlot', (index) => { bot.inventory.on('updateSlot', (index) => {
if (index === 45) this.updateHeldItem(true) 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', () => { 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 // 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
}) })
this.reactive.gameMode = bot.game?.gameMode this.reactive.gameMode = bot.game?.gameMode
}
get shouldHideHand () { this.watchReactive()
return this.reactive.gameMode === 'spectator'
} }
// #region Movement and Physics State // #region Movement and Physics State
private updateState () { private updateMovementState () {
if (!bot?.entity || this.disableStateUpdates) return if (!bot?.entity || this.disableStateUpdates) return
const { velocity } = bot.entity const { velocity } = bot.entity
@ -109,7 +114,7 @@ export class PlayerStateManager implements IPlayerState {
const deltaTime = now - this.lastUpdateTime const deltaTime = now - this.lastUpdateTime
this.lastUpdateTime = now this.lastUpdateTime = now
this.lastVelocity = velocity // this.lastVelocity = velocity
// Update time off ground // Update time off ground
if (isOnGround) { if (isOnGround) {
@ -118,60 +123,26 @@ export class PlayerStateManager implements IPlayerState {
this.timeOffGround += deltaTime this.timeOffGround += deltaTime
} }
if (this.isSneaking() || this.isFlying() || (this.timeOffGround > OFF_GROUND_THRESHOLD)) { if (gameAdditionalState.isSneaking || gameAdditionalState.isFlying || (this.timeOffGround > OFF_GROUND_THRESHOLD)) {
this.movementState = 'SNEAKING' this.reactive.movementState = 'SNEAKING'
} else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) { } 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' ? 'SPRINTING'
: 'WALKING' : 'WALKING'
} else { } 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 // #region Held Item State
private updateHeldItem (isLeftHand: boolean) { private updateHeldItem (isLeftHand: boolean) {
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
if (!newItem) { if (!newItem) {
if (isLeftHand) { if (isLeftHand) {
this.offHandItem = undefined this.reactive.heldItemOff = undefined
} else { } else {
this.heldItem = undefined this.reactive.heldItemMain = undefined
} }
this.events.emit('heldItemChanged', undefined, isLeftHand)
return return
} }
@ -186,42 +157,36 @@ export class PlayerStateManager implements IPlayerState {
} }
if (isLeftHand) { if (isLeftHand) {
this.offHandItem = item this.reactive.heldItemOff = item
} else { } else {
this.heldItem = item this.reactive.heldItemMain = item
} }
this.events.emit('heldItemChanged', item, isLeftHand) // this.events.emit('heldItemChanged', item, isLeftHand)
} }
startUsingItem () { startUsingItem () {
if (this.isUsingItem) return if (this.isUsingItem) return
this.isUsingItem = true this.isUsingItem = true
this.itemUsageTicks = 0 this.reactive.itemUsageTicks = 0
} }
stopUsingItem () { stopUsingItem () {
this.isUsingItem = false this.isUsingItem = false
this.itemUsageTicks = 0 this.reactive.itemUsageTicks = 0
} }
getItemUsageTicks (): number { getItemUsageTicks (): number {
return this.itemUsageTicks return this.reactive.itemUsageTicks
} }
getHeldItem (isLeftHand = false): HandItemBlock | undefined { watchReactive () {
return isLeftHand ? this.offHandItem : this.heldItem 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 // #endregion
} }
export const playerState = PlayerStateManager.getInstance() export const playerState = new PlayerStateControllerMain()
window.playerState = playerState window.playerState = playerState

View file

@ -112,7 +112,7 @@ const HotbarInner = () => {
inv.canvas.style.pointerEvents = 'auto' inv.canvas.style.pointerEvents = 'auto'
container.current.appendChild(inv.canvas) container.current.appendChild(inv.canvas)
const upHotbarItems = () => { const upHotbarItems = () => {
if (!appViewer.resourcesManager.currentResources?.itemsAtlasParser) return if (!appViewer.resourcesManager?.itemsAtlasParser) return
upInventoryItems(true, inv) upInventoryItems(true, inv)
} }

View file

@ -1,14 +1,44 @@
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import PixelartIcon from './PixelartIcon' import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import { useUsingTouch } from './utilsApp'
const duration = 0.2 const duration = 0.2
// save pass: login // 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' const isError = type === 'error'
icon ||= isError ? 'alert' : 'message' icon ||= isError ? 'alert' : 'message'
const isLoader = type === 'progress'
const top = (topPosition * toastHeight) + (isUsingTouch ? 18 : 0) // add space for mobile top buttons
return <AnimatePresence> return <AnimatePresence>
{open && ( {open && (
<motion.div <motion.div
@ -20,7 +50,7 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a
onClick={action} onClick={action}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top,
right: 0, right: 0,
width: '180px', width: '180px',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -28,31 +58,54 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a
display: 'flex', display: 'flex',
gap: 4, gap: 4,
alignItems: 'center', alignItems: 'center',
padding: '3px 5px', padding: '4px 5px',
background: isError ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.7)', background: isError ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.7)',
borderRadius: '0 0 0 5px', borderRadius: top === 0 ? '0 0 0 5px' : '5px',
pointerEvents: action ? 'auto' : 'none', pointerEvents: action ? 'auto' : 'none',
zIndex: 1200, zIndex: isLoader ? 10 : 1200,
}} }}
> >
<PixelartIcon iconName={icon} styles={{ fontSize: 12 }} /> <PixelartIcon
iconName={icon}
styles={{
fontSize: isLoader ? 15 : 12,
animation: isLoader ? 'rotation 6s linear infinite' : 'none',
}}
/>
<div style={{ <div style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 2, width: '100%',
}}> }}>
<div style={{ <div style={{
whiteSpace: 'normal', whiteSpace: 'normal',
}}> }}>
{message} {translate(message)}
</div> </div>
<div style={{ <div style={{
fontSize: '7px', fontSize: '7px',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
color: 'lightgray', color: 'lightgray',
marginTop: 3,
}}> }}>
{subMessage} {translate(subMessage)}
</div> </div>
{currentProgress !== undefined && totalProgress !== undefined && (
<div style={{
width: '100%',
height: '2px',
background: 'rgba(128, 128, 128, 0.5)',
marginTop: '2px',
overflow: 'hidden',
}}>
<div style={{
width: `${Math.min(100, (totalProgress ? currentProgress / totalProgress : 0) * 100)}%`,
height: '100%',
background: 'white',
transition: 'width 0.2s ease-out',
}} />
</div>
)}
</div> </div>
</motion.div> </motion.div>
)} )}

View file

@ -8,7 +8,11 @@ import Slider from './Slider'
import styles from './rendererDebugMenu.module.css' import styles from './rendererDebugMenu.module.css'
export default () => { export default () => {
const worldRenderer = window.world as WorldRendererCommon const worldRenderer = window.world as WorldRendererCommon | undefined
return worldRenderer ? <RendererDebugMenu worldRenderer={worldRenderer} /> : null
}
const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererCommon }) => {
const { reactiveDebugParams } = worldRenderer const { reactiveDebugParams } = worldRenderer
const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams) const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams)

View file

@ -80,7 +80,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
}} }}
onDoubleClick={() => onInteraction?.('enter')} onDoubleClick={() => onInteraction?.('enter')}
> >
<img className={`${styles.world_image} ${iconSrc ? '' : styles.image_missing}`} src={iconSrc ?? missingWorldPreview} alt='world preview' /> <img className={`${styles.world_image} ${iconSrc ? '' : styles.image_missing}`} src={iconSrc ?? missingWorldPreview} alt='' />
<div className={styles.world_info}> <div className={styles.world_info}>
<div className={styles.world_title}> <div className={styles.world_title}>
<div>{title}</div> <div>{title}</div>

View file

@ -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 FOV_EFFECT_SCALE = 1 // Equivalent to Minecraft's FOV Effects slider
const updateFovAnimation = () => { const updateFovAnimation = () => {
if (!bot) return if (!playerState.ready) return
// Calculate base FOV modifier // Calculate base FOV modifier
let fovModifier = 1 let fovModifier = 1
@ -39,10 +39,10 @@ const updateFovAnimation = () => {
} }
// Item usage modifier // Item usage modifier
if (playerState.getHeldItem()) { if (playerState.reactive.heldItemMain) {
const heldItem = playerState.getHeldItem() const heldItem = playerState.reactive.heldItemMain
if (heldItem?.name === 'bow' && playerState.getItemUsageTicks() > 0) { if (heldItem?.name === 'bow' && playerState.reactive.itemUsageTicks > 0) {
const ticksUsingItem = playerState.getItemUsageTicks() const ticksUsingItem = playerState.reactive.itemUsageTicks
let usageProgress = ticksUsingItem / 20 let usageProgress = ticksUsingItem / 20
if (usageProgress > 1) { if (usageProgress > 1) {
usageProgress = 1 usageProgress = 1
@ -88,8 +88,4 @@ export const watchFov = () => {
customEvents.on('gameLoaded', () => { customEvents.on('gameLoaded', () => {
updateFovAnimation() updateFovAnimation()
}) })
subscribeKey(gameAdditionalState, 'isSneaking', () => {
appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
})
} }

View file

@ -32,7 +32,7 @@ const getLoadedImage = async (url: string) => {
const resourcepackPackBasePath = '/data/resourcePacks/' const resourcepackPackBasePath = '/data/resourcePacks/'
export const uninstallResourcePack = async (name = 'default') => { export const uninstallResourcePack = async (name = 'default') => {
if (await existsAsync('/resourcepack/pack.mcmeta')) { if (await existsAsync('/resourcepack/pack.mcmeta')) {
await removeFileRecursiveAsync('/resourcepack') await removeFileRecursiveAsync('/resourcepack', false)
gameAdditionalState.usingServerResourcePack = false gameAdditionalState.usingServerResourcePack = false
} }
const basePath = resourcepackPackBasePath + name const basePath = resourcepackPackBasePath + name
@ -212,7 +212,6 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', e
if (!basePath) return if (!basePath) return
let firstTextureSize: number | undefined let firstTextureSize: number | undefined
const namespaces = await fs.promises.readdir(join(basePath, 'assets')) const namespaces = await fs.promises.readdir(join(basePath, 'assets'))
progressReporter.beginStage(`generate-atlas-texture-${type}`, `Generating atlas texture for ${type}`)
const textures = {} as Record<string, HTMLImageElement> const textures = {} as Record<string, HTMLImageElement>
let path let path
@ -420,6 +419,7 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) =
} }
} catch (err) { } catch (err) {
console.error('Failed to read some of resource pack blockstates and models', 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.customBlockStates = undefined
resources.customModels = undefined resources.customModels = undefined
resources.customItemModelNames = {} resources.customItemModelNames = {}
@ -439,8 +439,10 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres
console.log('Downloading server resource pack', url) console.log('Downloading server resource pack', url)
console.time('downloadServerResourcePack') console.time('downloadServerResourcePack')
const response = await fetch(url).catch((err) => { 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) 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) progressReporter.error('Failed to download resource pack: ' + err.message)
}) })
console.timeEnd('downloadServerResourcePack') console.timeEnd('downloadServerResourcePack')
@ -475,6 +477,7 @@ const downloadAndUseResourcePack = async (url: string, progressReporter: Progres
showNotification('Failed to install resource pack: ' + err.message) showNotification('Failed to install resource pack: ' + err.message)
}) })
} catch (err) { } catch (err) {
console.error('Could not install resource pack', err)
progressReporter.error('Could not install resource pack: ' + err.message) progressReporter.error('Could not install resource pack: ' + err.message)
} finally { } finally {
progressReporter.endStage('download-resource-pack') progressReporter.endStage('download-resource-pack')
@ -513,21 +516,19 @@ export const onAppLoad = () => {
cancel: !forced, cancel: !forced,
minecraftJsonMessage: promptMessagePacket, minecraftJsonMessage: promptMessagePacket,
}) })
if (Date.now() - start < 700) { // wait for state protocol switch if (Date.now() - start < 700) {
await new Promise(resolve => { void new Promise(resolve => {
// wait for state protocol switch
setTimeout(resolve, 700) 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)') { if (choice === true || choice === 'Download & Install (recommended)') {
await downloadAndUseResourcePack(packet.url, createFullScreenProgressReporter()).catch((err) => { await downloadAndUseResourcePack(packet.url, createFullScreenProgressReporter()).catch((err) => {
console.error(err) console.error(err)
@ -590,10 +591,17 @@ const updateTextures = async (progressReporter = createConsoleLogProgressReporte
const origItemsFiles = Object.keys(appViewer.resourcesManager.sourceItemsAtlases.latest.textures) const origItemsFiles = Object.keys(appViewer.resourcesManager.sourceItemsAtlases.latest.textures)
const origArmorFiles = Object.keys(armorTextures) const origArmorFiles = Object.keys(armorTextures)
const { usedBlockTextures, usedItemTextures } = await prepareBlockstatesAndModels(progressReporter) ?? {} const { usedBlockTextures, usedItemTextures } = await prepareBlockstatesAndModels(progressReporter) ?? {}
const blocksData = await getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter) progressReporter.beginStage(`generate-atlas-texture-blocks`, `Generating atlas textures`)
const itemsData = await getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter) const [
const armorData = await getResourcepackTiles('armor', origArmorFiles, progressReporter) blocksData,
await updateAllReplacableTextures() itemsData,
armorData
] = await Promise.all([
getResourcepackTiles('blocks', [...origBlocksFiles, ...usedBlockTextures ?? []], progressReporter),
getResourcepackTiles('items', [...origItemsFiles, ...usedItemTextures ?? []], progressReporter),
getResourcepackTiles('armor', origArmorFiles, progressReporter),
updateAllReplacableTextures()
])
resources.customTextures = {} resources.customTextures = {}
if (blocksData) { if (blocksData) {

View file

@ -8,26 +8,25 @@ import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png' import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png' import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
import christmasPack from 'mc-assets/dist/textureReplacements/christmas' 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 worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import { getLoadedItemDefinitionsStore } from 'mc-assets' import { getLoadedItemDefinitionsStore } from 'mc-assets'
import { getLoadedImage } from 'mc-assets/dist/utils'
import { generateGuiAtlas } from 'renderer/viewer/lib/guiRenderer' import { generateGuiAtlas } from 'renderer/viewer/lib/guiRenderer'
import { importLargeData } from '../generated/large-data-aliases' import { importLargeData } from '../generated/large-data-aliases'
import { loadMinecraftData } from './connect'
type ResourceManagerEvents = { type ResourceManagerEvents = {
assetsTexturesUpdated: () => void assetsTexturesUpdated: () => void
assetsInventoryStarted: () => void
assetsInventoryReady: () => void assetsInventoryReady: () => void
} }
export class LoadedResources { export class LoadedResourcesTransferrable {
allReady = false
// Atlas parsers // Atlas parsers
itemsAtlasParser: AtlasParser itemsAtlasImage: ImageBitmap
blocksAtlasParser: AtlasParser blocksAtlasImage: ImageBitmap
itemsAtlasImage: HTMLImageElement blocksAtlasJson: ItemsAtlasesOutputJson
blocksAtlasImage: HTMLImageElement
// User data (specific to current resourcepack/version) // User data (specific to current resourcepack/version)
customBlockStates?: Record<string, any> customBlockStates?: Record<string, any>
customModels?: Record<string, any> customModels?: Record<string, any>
@ -38,9 +37,11 @@ export class LoadedResources {
blocks?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> } blocks?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
armor?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> } armor?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
} = {} } = {}
guiAtlas: { json: any, image: ImageBitmap } | null = null
guiAtlasVersion = 0
itemsRenderer: ItemsRenderer itemsRenderer: ItemsRenderer
worldBlockProvider: WorldBlockProvider worldBlockProvider?: WorldBlockProvider
blockstatesModels: any = null blockstatesModels: any = null
version: string version: string
@ -59,8 +60,17 @@ export interface UpdateAssetsRequest {
_?: false _?: false
} }
export interface ResourcesManagerTransferred extends TypedEmitter<ResourceManagerEvents> {
currentResources: LoadedResourcesTransferrable
}
export interface ResourcesManagerCommon extends TypedEmitter<ResourceManagerEvents> {
currentResources: LoadedResourcesTransferrable | undefined
}
const STABLE_MODELS_VERSION = '1.21.4' const STABLE_MODELS_VERSION = '1.21.4'
export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) { export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) {
static restorerName = 'ResourcesManager'
// Source data (imported, not changing) // Source data (imported, not changing)
sourceBlockStatesModels: any = null sourceBlockStatesModels: any = null
readonly sourceBlocksAtlases: any = blocksAtlases readonly sourceBlocksAtlases: any = blocksAtlases
@ -68,7 +78,9 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
readonly sourceItemDefinitionsJson: any = itemDefinitionsJson readonly sourceItemDefinitionsJson: any = itemDefinitionsJson
readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson) readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
currentResources: LoadedResources | undefined currentResources: LoadedResourcesTransferrable | undefined
itemsAtlasParser: AtlasParser
blocksAtlasParser: AtlasParser
currentConfig: ResourcesCurrentConfig | undefined currentConfig: ResourcesCurrentConfig | undefined
abortController = new AbortController() abortController = new AbortController()
_promiseAssetsReadyResolvers = Promise.withResolvers<void>() _promiseAssetsReadyResolvers = Promise.withResolvers<void>()
@ -76,17 +88,12 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
return this._promiseAssetsReadyResolvers.promise return this._promiseAssetsReadyResolvers.promise
} }
async loadMcData (version: string) {
await loadMinecraftData(version)
}
async loadSourceData (version: string) { async loadSourceData (version: string) {
await this.loadMcData(version)
this.sourceBlockStatesModels ??= await importLargeData('blockStatesModels') this.sourceBlockStatesModels ??= await importLargeData('blockStatesModels')
} }
resetResources () { resetResources () {
this.currentResources = new LoadedResources() this.currentResources = new LoadedResourcesTransferrable()
} }
async updateAssetsData (request: UpdateAssetsRequest, unstableSkipEvent = false) { async updateAssetsData (request: UpdateAssetsRequest, unstableSkipEvent = false) {
@ -96,7 +103,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
await this.loadSourceData(this.currentConfig.version) await this.loadSourceData(this.currentConfig.version)
if (abortController.signal.aborted) return if (abortController.signal.aborted) return
const resources = this.currentResources ?? new LoadedResources() const resources = this.currentResources ?? new LoadedResourcesTransferrable()
resources.version = this.currentConfig.version resources.version = this.currentConfig.version
resources.texturesVersion = this.currentConfig.texturesVersion ?? resources.version resources.texturesVersion = this.currentConfig.texturesVersion ?? resources.version
@ -115,41 +122,28 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
...resources.customModels ...resources.customModels
} }
await this.recreateBlockAtlas(resources) console.time('recreateAtlases')
await Promise.all([
this.recreateBlockAtlas(resources),
this.recreateItemsAtlas(resources)
])
console.timeEnd('recreateAtlases')
if (abortController.signal.aborted) return if (abortController.signal.aborted) return
const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy) if (resources.version && resources.blockstatesModels && this.itemsAtlasParser && this.blocksAtlasParser) {
const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {})
console.time('createItemsAtlas')
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(
resources.texturesVersion,
(textureName) => {
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) {
resources.itemsRenderer = new ItemsRenderer( resources.itemsRenderer = new ItemsRenderer(
resources.version, resources.version,
resources.blockstatesModels, resources.blockstatesModels,
resources.itemsAtlasParser, this.itemsAtlasParser,
resources.blocksAtlasParser this.blocksAtlasParser
) )
} }
if (abortController.signal.aborted) return if (abortController.signal.aborted) return
this.currentResources = resources this.currentResources = resources
resources.allReady = true
if (!unstableSkipEvent) { // todo rework resourcepack optimization if (!unstableSkipEvent) { // todo rework resourcepack optimization
this.emit('assetsTexturesUpdated') this.emit('assetsTexturesUpdated')
} }
@ -157,6 +151,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
if (this.currentConfig.noInventoryGui) { if (this.currentConfig.noInventoryGui) {
this._promiseAssetsReadyResolvers.resolve() this._promiseAssetsReadyResolvers.resolve()
} else { } else {
this.emit('assetsInventoryStarted')
void this.generateGuiTextures().then(() => { void this.generateGuiTextures().then(() => {
if (abortController.signal.aborted) return if (abortController.signal.aborted) return
if (!unstableSkipEvent) { if (!unstableSkipEvent) {
@ -167,7 +162,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
} }
} }
async recreateBlockAtlas (resources: LoadedResources = this.currentResources!) { async recreateBlockAtlas (resources: LoadedResourcesTransferrable = this.currentResources!) {
const blockTexturesChanges = {} as Record<string, string> const blockTexturesChanges = {} as Record<string, string>
const date = new Date() const date = new Date()
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) { 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<Re
) )
console.timeEnd('createBlocksAtlas') console.timeEnd('createBlocksAtlas')
resources.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL()) this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
resources.blocksAtlasImage = await getLoadedImage(blocksCanvas.toDataURL()) resources.blocksAtlasImage = await createImageBitmap(blocksCanvas)
resources.blocksAtlasJson = this.blocksAtlasParser.atlas.latest
resources.worldBlockProvider = worldBlockProvider( resources.worldBlockProvider = worldBlockProvider(
resources.blockstatesModels, resources.blockstatesModels,
resources.blocksAtlasParser.atlas, this.blocksAtlasParser.atlas,
STABLE_MODELS_VERSION STABLE_MODELS_VERSION
) )
} }
async recreateItemsAtlas (resources: LoadedResourcesTransferrable = this.currentResources!) {
const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {})
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(
resources.texturesVersion,
(textureName) => {
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 () { async generateGuiTextures () {
await generateGuiAtlas() await generateGuiAtlas()
} }
@ -211,7 +226,7 @@ export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<Re
async downloadDebugAtlas (isItems = false) { async downloadDebugAtlas (isItems = false) {
const resources = this.currentResources const resources = this.currentResources
if (!resources) throw new Error('No resources loaded') if (!resources) throw new Error('No resources loaded')
const atlasParser = (isItems ? resources.itemsAtlasParser : resources.blocksAtlasParser)! const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)!
const dataUrl = await atlasParser.createDebugImage(true) const dataUrl = await atlasParser.createDebugImage(true)
const a = document.createElement('a') const a = document.createElement('a')
a.href = dataUrl a.href = dataUrl