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
server-jar
config.local.json
logs/
src/react/npmReactComponents.ts

View file

@ -35,6 +35,10 @@ const buildOptions = {
define: {
'process.env.BROWSER': '"true"',
},
loader: {
'.png': 'dataurl',
'.obj': 'text'
},
plugins: [
...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 {
world: {
chunksLoaded: new Set(),
heightmaps: new Map(),
chunksTotalNumber: 0,
allChunksLoaded: true,
mesherWork: false,
intersectMedia: null
reactive: {
world: {
chunksLoaded: new Set(),
heightmaps: new Map(),
allChunksLoaded: true,
mesherWork: false,
intersectMedia: null
},
renderer: '',
preventEscapeMenu: false
},
renderer: '',
preventEscapeMenu: false
nonReactive: {
world: {
chunksLoaded: new Set(),
chunksTotalNumber: 0,
}
}
}
}

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 { proxy, ref } from 'valtio'
import { GameMode } from 'mineflayer'
import { HandItemBlock } from '../three/holdingBlock'
import { proxy } from 'valtio'
import type { HandItemBlock } from '../three/holdingBlock'
export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
export type ItemSpecificContextProperties = Partial<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 BlocksShapes = BlockShape[]
export interface IPlayerState {
getEyeHeight(): number
getMovementState(): MovementState
getVelocity(): Vec3
isOnGround(): boolean
isSneaking(): boolean
isFlying(): boolean
isSprinting (): boolean
getItemUsageTicks?(): number
getPosition(): Vec3
// isUsingItem?(): boolean
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
username?: string
onlineMode?: boolean
lightingDisabled?: boolean
shouldHideHand?: boolean
// edit src/mineflayer/playerState.ts for implementation of player state from mineflayer
export const getInitialPlayerState = () => proxy({
playerSkin: undefined as string | undefined,
inWater: false,
waterBreathing: false,
backgroundColor: [0, 0, 0] as [number, number, number],
ambientLight: 0,
directionalLight: 0,
eyeHeight: 0,
gameMode: undefined as GameMode | undefined,
lookingAtBlock: undefined as {
x: number
y: number
z: number
face?: number
shapes: BlocksShapes
} | undefined,
diggingBlock: undefined as {
x: number
y: number
z: number
stage: number
face?: number
mergedShape: BlockShape | undefined
} | undefined,
movementState: 'NOT_MOVING' as MovementState,
onGround: true,
sneaking: false,
flying: false,
sprinting: false,
itemUsageTicks: 0,
username: '',
onlineMode: false,
lightingDisabled: false,
shouldHideHand: false,
heldItemMain: undefined as HandItemBlock | undefined,
heldItemOff: undefined as HandItemBlock | undefined,
})
events: TypedEmitter<PlayerStateEvents>
export const getInitialPlayerStateRenderer = () => ({
reactive: getInitialPlayerState()
})
reactive: {
playerSkin: string | undefined
inWater: boolean
waterBreathing: boolean
backgroundColor: [number, number, number]
ambientLight: number
directionalLight: number
gameMode?: GameMode
lookingAtBlock?: {
x: number
y: number
z: number
face?: number
shapes: BlocksShapes
}
diggingBlock?: {
x: number
y: number
z: number
stage: number
face?: number
mergedShape?: BlockShape
}
}
export type PlayerStateReactive = ReturnType<typeof getInitialPlayerState>
export interface PlayerStateRenderer {
reactive: PlayerStateReactive
}
export class BasePlayerState implements IPlayerState {
reactive = proxy({
playerSkin: undefined as string | undefined,
inWater: false,
waterBreathing: false,
backgroundColor: ref([0, 0, 0]) as [number, number, number],
ambientLight: 0,
directionalLight: 0,
})
protected movementState: MovementState = 'NOT_MOVING'
protected velocity = new Vec3(0, 0, 0)
protected onGround = true
protected sneaking = false
protected flying = false
protected sprinting = false
readonly events = new EventEmitter() as TypedEmitter<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)
export const getItemSelector = (playerState: PlayerStateRenderer, specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item) => {
return {
...specificProperties,
'minecraft:date': new Date(),
// "minecraft:context_dimension": bot.entityp,
// 'minecraft:time': bot.time.timeOfDay / 24_000,
}
}

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import * as THREE from 'three'
import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './utils/skins'
let textureCache: Record<string, 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]
if (!cached) {
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
}

View file

@ -1,18 +1,43 @@
import { loadSkinToCanvas } from 'skinview-utils'
import * as THREE from 'three'
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
import { getLoadedImage } from 'mc-assets/dist/utils'
// eslint-disable-next-line unicorn/prefer-export-from
export const stevePngUrl = stevePng
export const steveTexture = new THREE.TextureLoader().loadAsync(stevePng)
export async function loadImageFromUrl (imageUrl: string): Promise<HTMLImageElement> {
const img = new Image()
img.src = imageUrl
await new Promise<void>(resolve => {
img.onload = () => resolve()
export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => {
const texture = new THREE.Texture()
const promise = getLoadedImage(imageUrl).then(image => {
texture.image = image
texture.needsUpdate = true
return texture
})
return img
return {
texture,
promise
}
}
export const loadThreeJsTextureFromUrl = async (imageUrl: string) => {
const loaded = new THREE.TextureLoader().loadAsync(imageUrl)
return loaded
}
export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => {
const canvas = new OffscreenCanvas(image.width, image.height)
const ctx = canvas.getContext('2d')!
ctx.drawImage(image, 0, 0)
const texture = new THREE.Texture(canvas)
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
return texture
}
export const stevePngUrl = stevePng
export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
export async function loadImageFromUrl (imageUrl: string): Promise<ImageBitmap> {
const response = await fetch(imageUrl)
const blob = await response.blob()
return createImageBitmap(blob)
}
const config = {
@ -52,13 +77,13 @@ export const parseSkinTexturesValue = (value: string) => {
return decodedData.textures?.SKIN?.url
}
export async function loadSkinImage (skinUrl: string): Promise<{ canvas: HTMLCanvasElement, image: HTMLImageElement }> {
export async function loadSkinImage (skinUrl: string): Promise<{ canvas: OffscreenCanvas, image: ImageBitmap }> {
if (!skinUrl.startsWith('data:')) {
skinUrl = await fetchAndConvertBase64Skin(skinUrl.replace('http://', 'https://'))
}
const image = await loadImageFromUrl(skinUrl)
const skinCanvas = document.createElement('canvas')
const skinCanvas = new OffscreenCanvas(64, 64)
loadSkinToCanvas(skinCanvas, image)
return { canvas: skinCanvas, image }
}

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
target.addEventListener('message', (event: any) => {
const { type, args } = event.data
const { type, args, msgId } = event.data
if (handlers[type]) {
handlers[type](...args)
const result = handlers[type](...args)
if (result instanceof Promise) {
void result.then((result) => {
target.postMessage({
type: 'result',
msgId,
args: [result]
})
})
}
}
})
return null as any
@ -23,6 +34,7 @@ export function createWorkerProxy<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'] & {
transfer: (...args: Transferable[]) => T['__workerProxy']
} => {
let messageId = 0
// in main thread
return new Proxy({} as any, {
get (target, prop) {
@ -41,11 +53,30 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg
}
}
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({
type: prop,
msgId,
args,
}, transfer as any[])
}, transfer)
return {
// eslint-disable-next-line unicorn/no-thenable
then (onfulfilled: (value: any) => void) {
const handler = ({ data }: MessageEvent): void => {
if (data.type === 'result' && data.msgId === msgId) {
onfulfilled(data.args[0])
worker.removeEventListener('message', handler as EventListener)
}
}
worker.addEventListener('message', handler as EventListener)
}
}
}
}
})

View file

@ -7,9 +7,7 @@ import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer'
import { proxy } from 'valtio'
import TypedEmitter from 'typed-emitter'
import { getItemFromBlock } from '../../../src/chatUtils'
import { delayedIterator } from '../../playground/shared'
import { playerState } from '../../../src/mineflayer/playerState'
import { chunkPos } from './simpleUtils'
export type ChunkPosKey = string // like '16,16'
@ -23,7 +21,6 @@ export type WorldDataEmitterEvents = {
time: (data: number) => void
renderDistance: (viewDistance: number) => void
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
listening: () => void
markAsLoaded: (data: { x: number, z: number }) => void
unloadChunk: (data: { x: number, z: number }) => void
loadChunk: (data: { x: number, z: number, chunk: string, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void
@ -32,10 +29,10 @@ export type WorldDataEmitterEvents = {
end: () => void
}
/**
* Usually connects to mineflayer bot and emits world data (chunks, entities)
* It's up to the consumer to serialize the data if needed
*/
export class WorldDataEmitterWorker extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
static readonly restorerName = 'WorldDataEmitterWorker'
}
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
loadedChunks: Record<ChunkPosKey, boolean>
readonly lastPos: Vec3
@ -57,11 +54,6 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
/* config */ isPlayground = false
/* 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)) {
// eslint-disable-next-line constructor-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)) {
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)
}
}
}
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) {
@ -213,6 +197,8 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
async init (pos: Vec3) {
this.updateViewDistance(this.viewDistance)
this.emitter.emit('chunkPosUpdate', { pos })
this.emitter.emit('time', bot.time.timeOfDay)
this.emitterGotConnected()
const [botX, botZ] = chunkPos(pos)
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 */
import { EventEmitter } from 'events'
import { Vec3 } from 'vec3'
import * as THREE from 'three'
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
import TypedEmitter from 'typed-emitter'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { subscribeKey } from 'valtio/utils'
import { proxy } from 'valtio'
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajorVersion } from '../../../src/utils'
import { ResourcesManager } from '../../../src/resourcesManager'
import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
import { SoundSystem } from '../three/threeJsSound'
import { buildCleanupDecorator } from './cleanupDecorator'
import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, removeStat, updatePanesVisibility, updateStatText } from './ui/newStats'
import { WorldDataEmitter } from './worldDataEmitter'
import { IPlayerState } from './basePlayerState'
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
import { WorldDataEmitterWorker } from './worldDataEmitter'
import { PlayerStateRenderer } from './basePlayerState'
import { MesherLogReader } from './mesherlogReader'
import { setSkinsConfig } from './utils/skins'
@ -27,6 +24,11 @@ function mod (x, n) {
return ((x % n) + n) % n
}
const toMajorVersion = version => {
const [a, b] = (String(version)).split('.')
return `${a}.${b}`
}
export const worldCleanup = buildCleanupDecorator('resetWorld')
export const defaultWorldRendererConfig = {
@ -52,7 +54,8 @@ export const defaultWorldRendererConfig = {
foreground: true,
enableDebugOverlay: false,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false
_renderByChunks: false,
volume: 1
}
export type WorldRendererConfig = typeof defaultWorldRendererConfig
@ -153,7 +156,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
abstract changeBackgroundColor (color: [number, number, number]): void
worldRendererConfig: WorldRendererConfig
playerState: IPlayerState
playerState: PlayerStateRenderer
reactiveState: RendererReactiveState
mesherLogReader: MesherLogReader | undefined
forceCallFromMesherReplayer = false
@ -169,6 +172,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
currentRenderedFrames = 0
fpsAverage = 0
lastFps = 0
fpsWorst = undefined as number | undefined
fpsSamples = 0
mainThreadRendering = true
@ -184,7 +188,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
return (this.initOptions.config.statsVisible ?? 0) > 1
}
constructor (public readonly resourcesManager: ResourcesManager, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
constructor (public readonly resourcesManager: ResourcesManagerTransferred, public displayOptions: DisplayWorldOptions, public initOptions: GraphicsInitOptions) {
this.snapshotInitialValues()
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
this.playerState = displayOptions.playerState
@ -221,6 +225,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
} else {
this.fpsWorst = Math.min(this.fpsWorst, this.currentRenderedFrames)
}
this.lastFps = this.currentRenderedFrames
this.currentRenderedFrames = 0
}
@ -231,15 +236,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
async init () {
if (this.active) throw new Error('WorldRendererCommon is already initialized')
await this.resourcesManager.loadMcData(this.version)
if (!this.resourcesManager.currentResources) {
await this.resourcesManager.updateAssetsData({ })
}
await Promise.all([
this.resetWorkers(),
(async () => {
if (this.resourcesManager.currentResources) {
if (this.resourcesManager.currentResources?.allReady) {
await this.updateAssetsData()
}
})()
@ -291,35 +292,22 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
initWorkers (numWorkers = this.worldRendererConfig.mesherWorkers) {
// init workers
for (let i = 0; i < numWorkers + 1; i++) {
// Node environment needs an absolute path, but browser needs the url of the file
const workerName = 'mesher.js'
// eslint-disable-next-line node/no-path-concat
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
let worker: any
if (process.env.SINGLE_FILE_BUILD) {
const workerCode = document.getElementById('mesher-worker-code')!.textContent!
const blob = new Blob([workerCode], { type: 'text/javascript' })
worker = new Worker(window.URL.createObjectURL(blob))
} else {
worker = new Worker(src)
}
worker.onmessage = ({ data }) => {
const worker = initMesherWorker((data) => {
if (Array.isArray(data)) {
this.messageQueue.push(...data)
} else {
this.messageQueue.push(data)
}
void this.processMessageQueue('worker')
}
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
})
this.workers.push(worker)
}
}
onReactiveValueUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void) {
callback(this.displayOptions.playerState.reactive[key])
onReactivePlayerStateUpdated<T extends keyof typeof this.displayOptions.playerState.reactive>(key: T, callback: (value: typeof this.displayOptions.playerState.reactive[T]) => void, initial = true) {
if (initial) {
callback(this.displayOptions.playerState.reactive[key])
}
subscribeKey(this.displayOptions.playerState.reactive, key, callback)
}
@ -334,7 +322,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
watchReactivePlayerState () {
this.onReactiveValueUpdated('backgroundColor', (value) => {
this.onReactivePlayerStateUpdated('backgroundColor', (value) => {
this.changeBackgroundColor(value)
})
}
@ -466,7 +454,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
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()
// for workers in single file build
if (document?.readyState === 'loading') {
if (typeof document !== 'undefined' && document?.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve)
})
@ -575,7 +563,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
skyLight,
smoothLighting: this.worldRendererConfig.smoothLighting,
outputFormat: this.outputFormat,
textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
// textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
debugModelVariant: undefined,
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers,
@ -600,7 +588,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
async updateAssetsData () {
const resources = this.resourcesManager.currentResources!
const resources = this.resourcesManager.currentResources
if (this.workers.length === 0) throw new Error('workers not initialized yet')
for (const [i, worker] of this.workers.entries()) {
@ -610,7 +598,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
type: 'mesherData',
workerIndex: i,
blocksAtlas: {
latest: resources.blocksAtlasParser.atlas.latest
latest: resources.blocksAtlasJson
},
blockstatesModels,
config: this.getMesherConfig(),
@ -733,7 +721,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
lightUpdate (chunkX: number, chunkZ: number) { }
connect (worldView: WorldDataEmitter) {
connect (worldView: WorldDataEmitterWorker) {
const worldEmitter = worldView
worldEmitter.on('entity', (e) => {
@ -812,7 +800,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
})
worldEmitter.on('onWorldSwitch', () => {
for (const fn of this.onWorldSwitched) fn()
for (const fn of this.onWorldSwitched) {
try {
fn()
} catch (e) {
setTimeout(() => {
console.log('[Renderer Backend] Error in onWorldSwitched:')
throw e
}, 0)
}
}
})
worldEmitter.on('time', (timeOfDay) => {
@ -830,8 +827,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// (this).rerenderAllChunks?.()
// }
})
worldEmitter.emit('listening')
}
setBlockStateIdInner (pos: Vec3, stateId: number | undefined, needAoRecalculation = true) {
@ -1029,3 +1024,37 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
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 { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState'
import { renderSlot } from '../../../src/inventoryWindows'
import { ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items'
import { ResourcesManager } from '../../../src/resourcesManager'
import { ResourcesManager, ResourcesManagerTransferred } from '../../../src/resourcesManager'
import { renderSlot } from './renderSlot'
export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager): {
export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManagerTransferred, playerState: PlayerStateRenderer): {
u: number
v: number
su: number
sv: number
renderInfo?: ReturnType<typeof renderSlot>
texture: HTMLImageElement
// texture: ImageBitmap
modelName: string
} | {
resolvedModel: BlockModel
@ -30,11 +30,11 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
const model = getItemModelName({
...item,
name,
} as GeneralInputItem, specificProps, resourcesManager)
} as GeneralInputItem, specificProps, resourcesManager, playerState)
const renderInfo = renderSlot({
modelName: model,
}, false, true)
}, resourcesManager, false, true)
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
@ -53,7 +53,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
return {
u, v, su, sv,
renderInfo,
texture: img,
// texture: img,
modelName: renderInfo.modelName!
}
}
@ -67,7 +67,7 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
v: 0,
su: 16 / resources.blocksAtlasImage.width,
sv: 16 / resources.blocksAtlasImage.width,
texture: resources.blocksAtlasImage,
// texture: resources.blocksAtlasImage,
modelName: 'missing'
}
}

View file

@ -6,15 +6,20 @@ import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appView
import { WorldRendererConfig } from '../lib/worldrendererCommon'
export class DocumentRenderer {
readonly canvas = document.createElement('canvas')
canvas: HTMLCanvasElement | OffscreenCanvas
readonly renderer: THREE.WebGLRenderer
private animationFrameId?: number
private timeoutId?: number
private lastRenderTime = 0
private previousWindowWidth = window.innerWidth
private previousWindowHeight = window.innerHeight
private previousCanvasWidth = 0
private previousCanvasHeight = 0
private currentWidth = 0
private currentHeight = 0
private renderedFps = 0
private fpsInterval: any
private readonly stats: TopRightStats
private readonly stats: TopRightStats | undefined
private paused = false
disconnected = false
preRender = () => { }
@ -26,9 +31,16 @@ export class DocumentRenderer {
onRender = [] as Array<(sizeChanged: boolean) => void>
inWorldRenderingConfig: WorldRendererConfig | undefined
constructor (initOptions: GraphicsInitOptions) {
constructor (initOptions: GraphicsInitOptions, public externalCanvas?: OffscreenCanvas) {
this.config = initOptions.config
// Handle canvas creation/transfer based on context
if (externalCanvas) {
this.canvas = externalCanvas
} else {
this.addToPage()
}
try {
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
@ -37,17 +49,24 @@ export class DocumentRenderer {
powerPreference: this.config.powerPreference
})
} catch (err) {
initOptions.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`))
initOptions.callbacks.displayCriticalError(new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`))
throw err
}
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
this.updatePixelRatio()
this.updateSize()
this.addToPage()
if (!externalCanvas) {
this.updatePixelRatio()
}
this.sizeUpdated()
// Initialize previous dimensions
this.previousCanvasWidth = this.canvas.width
this.previousCanvasHeight = this.canvas.height
this.stats = new TopRightStats(this.canvas, this.config.statsVisible)
// Only initialize stats and DOM-related features in main thread
if (!externalCanvas) {
this.stats = new TopRightStats(this.canvas as HTMLCanvasElement, this.config.statsVisible)
this.setupFpsTracking()
}
this.setupFpsTracking()
this.startRenderLoop()
}
@ -59,15 +78,33 @@ export class DocumentRenderer {
this.renderer.setPixelRatio(pixelRatio)
}
updateSize () {
this.renderer.setSize(window.innerWidth, window.innerHeight)
sizeUpdated () {
this.renderer.setSize(this.currentWidth, this.currentHeight, false)
}
private addToPage () {
this.canvas.id = 'viewer-canvas'
this.canvas.style.width = '100%'
this.canvas.style.height = '100%'
document.body.appendChild(this.canvas)
this.canvas = addCanvasToPage()
this.updateCanvasSize()
}
updateSizeExternal (newWidth: number, newHeight: number, pixelRatio: number) {
this.currentWidth = newWidth
this.currentHeight = newHeight
this.renderer.setPixelRatio(pixelRatio)
this.sizeUpdated()
}
private updateCanvasSize () {
if (!this.externalCanvas) {
const innnerWidth = window.innerWidth
const innnerHeight = window.innerHeight
if (this.currentWidth !== innnerWidth) {
this.currentWidth = innnerWidth
}
if (this.currentHeight !== innnerHeight) {
this.currentHeight = innnerHeight
}
}
}
private setupFpsTracking () {
@ -81,20 +118,15 @@ export class DocumentRenderer {
}, 1000)
}
// private handleResize () {
// const width = window.innerWidth
// const height = window.innerHeight
// viewer.camera.aspect = width / height
// viewer.camera.updateProjectionMatrix()
// this.renderer.setSize(width, height)
// viewer.world.handleResize()
// }
private startRenderLoop () {
const animate = () => {
if (this.disconnected) return
this.animationFrameId = requestAnimationFrame(animate)
if (this.config.timeoutRendering) {
this.timeoutId = setTimeout(animate, this.config.fpsLimit ? 1000 / this.config.fpsLimit : 0) as unknown as number
} else {
this.animationFrameId = requestAnimationFrame(animate)
}
if (this.paused || (this.renderer.xr.isPresenting && !this.inWorldRenderingConfig?.vrPageGameRendering)) return
@ -112,18 +144,19 @@ export class DocumentRenderer {
}
let sizeChanged = false
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) {
this.previousWindowWidth = window.innerWidth
this.previousWindowHeight = window.innerHeight
this.updateSize()
this.updateCanvasSize()
if (this.previousCanvasWidth !== this.currentWidth || this.previousCanvasHeight !== this.currentHeight) {
this.previousCanvasWidth = this.currentWidth
this.previousCanvasHeight = this.currentHeight
this.sizeUpdated()
sizeChanged = true
}
this.frameRender(sizeChanged)
// Update stats visibility each frame
// Update stats visibility each frame (main thread only)
if (this.config.statsVisible !== undefined) {
this.stats.setVisibility(this.config.statsVisible)
this.stats?.setVisibility(this.config.statsVisible)
}
}
@ -132,16 +165,16 @@ export class DocumentRenderer {
frameRender (sizeChanged: boolean) {
this.preRender()
this.stats.markStart()
this.stats?.markStart()
tween.update()
if (!window.freezeRender) {
if (!globalThis.freezeRender) {
this.render(sizeChanged)
}
for (const fn of this.onRender) {
fn(sizeChanged)
}
this.renderedFps++
this.stats.markEnd()
this.stats?.markEnd()
this.postRender()
}
@ -154,10 +187,15 @@ export class DocumentRenderer {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
}
this.canvas.remove()
this.renderer.dispose()
if (this.timeoutId) {
clearTimeout(this.timeoutId)
}
if (this.canvas instanceof HTMLCanvasElement) {
this.canvas.remove()
}
clearInterval(this.fpsInterval)
this.stats.dispose()
this.stats?.dispose()
this.renderer.dispose()
}
}
@ -250,3 +288,40 @@ class TopRightStats {
this.statsGl.container.remove()
}
}
const addCanvasToPage = () => {
const canvas = document.createElement('canvas')
canvas.id = 'viewer-canvas'
document.body.appendChild(canvas)
return canvas
}
export const addCanvasForWorker = () => {
const canvas = addCanvasToPage()
const transferred = canvas.transferControlToOffscreen()
let removed = false
let onSizeChanged = (w, h) => { }
let oldSize = { width: 0, height: 0 }
const checkSize = () => {
if (removed) return
if (oldSize.width !== window.innerWidth || oldSize.height !== window.innerHeight) {
onSizeChanged(window.innerWidth, window.innerHeight)
oldSize = { width: window.innerWidth, height: window.innerHeight }
}
requestAnimationFrame(checkSize)
}
requestAnimationFrame(checkSize)
return {
canvas: transferred,
destroy () {
removed = true
canvas.remove()
},
onSizeChanged (cb: (width: number, height: number) => void) {
onSizeChanged = cb
},
get size () {
return { width: window.innerWidth, height: window.innerHeight }
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -3,17 +3,16 @@ import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js'
import { subscribeKey } from 'valtio/utils'
import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils'
import { WorldRendererCommon } from '../lib/worldrendererCommon'
import { addNewStat, removeAllStats } from '../lib/ui/newStats'
import { addNewStat } from '../lib/ui/newStats'
import { MesherGeometryOutput } from '../lib/mesher/shared'
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { getMyHand } from '../lib/hand'
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
import { sendVideoPlay, sendVideoStop } from '../../../src/customChannels'
import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins'
import HoldingBlock from './holdingBlock'
import { getMesh } from './entity/EntityMesh'
import { armorModel } from './entity/armorModels'
@ -44,7 +43,7 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraGroupVr?: THREE.Object3D
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
itemsTexture: THREE.Texture
cursorBlock = new CursorBlock(this)
cursorBlock: CursorBlock
onRender: Array<() => void> = []
cameraShake: CameraShake
media: ThreeJsMedia
@ -82,8 +81,10 @@ export class WorldRendererThree extends WorldRendererCommon {
if (!initOptions.resourcesManager) throw new Error('resourcesManager is required')
super(initOptions.resourcesManager, displayOptions, initOptions)
this.renderer = renderer
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
this.starField = new StarField(this.scene)
this.cursorBlock = new CursorBlock(this)
this.holdingBlock = new HoldingBlock(this)
this.holdingBlockLeft = new HoldingBlock(this, true)
@ -148,21 +149,21 @@ export class WorldRendererThree extends WorldRendererCommon {
override watchReactivePlayerState () {
super.watchReactivePlayerState()
this.onReactiveValueUpdated('inWater', (value) => {
this.onReactivePlayerStateUpdated('inWater', (value) => {
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.displayOptions.playerState.reactive.waterBreathing ? 100 : 20) : null
})
this.onReactiveValueUpdated('ambientLight', (value) => {
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
if (!value) return
this.ambientLight.intensity = value
})
this.onReactiveValueUpdated('directionalLight', (value) => {
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
if (!value) return
this.directionalLight.intensity = value
})
this.onReactiveValueUpdated('lookingAtBlock', (value) => {
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
})
this.onReactiveValueUpdated('diggingBlock', (value) => {
this.onReactivePlayerStateUpdated('diggingBlock', (value) => {
this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape)
})
}
@ -184,20 +185,18 @@ export class WorldRendererThree extends WorldRendererCommon {
}
async updateAssetsData (): Promise<void> {
const resources = this.resourcesManager.currentResources!
const resources = this.resourcesManager.currentResources
const oldTexture = this.material.map
const oldItemsTexture = this.itemsTexture
const texture = await new THREE.TextureLoader().loadAsync(resources.blocksAtlasParser.latestImage)
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
const texture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage)
texture.needsUpdate = true
texture.flipY = false
this.material.map = texture
const itemsTexture = await new THREE.TextureLoader().loadAsync(resources.itemsAtlasParser.latestImage)
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage)
itemsTexture.needsUpdate = true
itemsTexture.flipY = false
this.itemsTexture = itemsTexture
@ -239,7 +238,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
return getItemUv(item, specificProps, this.resourcesManager)
return getItemUv(item, specificProps, this.resourcesManager, this.playerState)
}
async demoModel () {
@ -431,7 +430,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
const yOffset = this.displayOptions.playerState.getEyeHeight()
const yOffset = this.displayOptions.playerState.reactive.eyeHeight
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
this.media.tryIntersectMedia()
@ -495,7 +494,8 @@ export class WorldRendererThree extends WorldRendererCommon {
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
if (sizeOrFovChanged) {
this.camera.aspect = window.innerWidth / window.innerHeight
const size = this.renderer.getSize(new THREE.Vector2())
this.camera.aspect = size.width / size.height
this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov
this.camera.updateProjectionMatrix()
}
@ -508,7 +508,7 @@ export class WorldRendererThree extends WorldRendererCommon {
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
this.renderer.render(this.scene, cam)
if (this.displayOptions.inWorldRenderingConfig.showHand && !this.playerState.shouldHideHand /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) {
if (this.displayOptions.inWorldRenderingConfig.showHand && this.playerState.reactive.gameMode !== 'spectator' /* && !this.freeFlyMode */ && !this.renderer.xr.isPresenting) {
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
}
@ -710,6 +710,18 @@ export class WorldRendererThree extends WorldRendererCommon {
super.destroy()
}
shouldObjectVisible (object: THREE.Object3D) {
// Get chunk coordinates
const chunkX = Math.floor(object.position.x / 16) * 16
const chunkZ = Math.floor(object.position.z / 16) * 16
const sectionY = Math.floor(object.position.y / 16) * 16
const chunkKey = `${chunkX},${chunkZ}`
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
}
updateSectionOffsets () {
const currentTime = performance.now()
for (const [key, anim] of Object.entries(this.sectionsOffsetsAnimations)) {
@ -756,6 +768,10 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}
}
reloadWorld () {
this.entities.reloadEntities()
}
}
class StarField {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
import mojangson from 'mojangson'
import nbt from 'prismarine-nbt'
import { fromFormattedString } from '@xmcl/text-component'
import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState'
import { getItemSelector, ItemSpecificContextProperties, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
import { MessageFormatPart } from '../chatUtils'
import { ResourcesManager } from '../resourcesManager'
import { playerState } from './playerState'
import { ResourcesManager, ResourcesManagerCommon, ResourcesManagerTransferred } from '../resourcesManager'
type RenderSlotComponent = {
type: string,
@ -33,7 +32,7 @@ type PossibleItemProps = {
display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"}
}
export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManager) => {
export const getItemMetadata = (item: GeneralInputItem, resourcesManager: ResourcesManagerCommon) => {
let customText = undefined as string | any | undefined
let customModel = undefined as string | undefined
@ -91,7 +90,7 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour
}
export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | null, resourcesManager: ResourcesManager) => {
export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'> | null, resourcesManager: ResourcesManagerCommon) => {
if (!item) return ''
const { customText } = getItemMetadata(item as GeneralInputItem, resourcesManager)
if (!customText) return
@ -112,14 +111,14 @@ export const getItemNameRaw = (item: Pick<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
const { customModel } = getItemMetadata(item, resourcesManager)
if (customModel) {
itemModelName = customModel
}
const itemSelector = playerState.getItemSelector({
const itemSelector = getItemSelector(playerState, {
...specificProps
})
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 TypedEmitter from 'typed-emitter'
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
import { proxy } from 'valtio'
import { getInitialPlayerState, PlayerStateRenderer } from 'renderer/viewer/lib/basePlayerState'
import { subscribe } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { gameAdditionalState } from '../globalState'
export class PlayerStateManager implements IPlayerState {
/**
* can be used only in main thread. Mainly for more convenient reactive state updates.
* In renderer/ directory, use PlayerStateControllerRenderer type or worldRenderer.playerState.
*/
export class PlayerStateControllerMain implements PlayerStateRenderer {
disableStateUpdates = false
private static instance: PlayerStateManager
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
// Movement and physics state
private lastVelocity = new Vec3(0, 0, 0)
private movementState: MovementState = 'NOT_MOVING'
private timeOffGround = 0
private lastUpdateTime = performance.now()
// Held item state
private heldItem?: HandItemBlock
private offHandItem?: HandItemBlock
private itemUsageTicks = 0
private isUsingItem = false
private ready = false
public lightingDisabled = false
onlineMode = false
get username () {
return bot.username ?? ''
}
ready = false
reactive: IPlayerState['reactive'] = new BasePlayerState().reactive
static getInstance (): PlayerStateManager {
if (!this.instance) {
this.instance = new PlayerStateManager()
}
return this.instance
}
reactive: PlayerStateRenderer['reactive']
constructor () {
this.updateState = this.updateState.bind(this)
customEvents.on('mineflayerBotCreated', () => {
this.ready = false
bot.on('inject_allowed', () => {
@ -48,16 +28,27 @@ export class PlayerStateManager implements IPlayerState {
this.ready = true
this.botCreated()
})
bot.on('end', () => {
this.ready = false
})
})
}
private onBotCreatedOrGameJoined () {
this.reactive.username = bot.username ?? ''
}
private botCreated () {
console.log('bot created & plugins injected')
this.reactive = getInitialPlayerState()
this.onBotCreatedOrGameJoined()
const handleDimensionData = (data) => {
let hasSkyLight = 1
try {
hasSkyLight = data.dimension.value.has_skylight.value
} catch {}
this.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight
this.reactive.lightingDisabled = bot.game.dimension === 'the_nether' || bot.game.dimension === 'the_end' || !hasSkyLight
}
bot._client.on('login', (packet) => {
@ -68,7 +59,9 @@ export class PlayerStateManager implements IPlayerState {
})
// Movement tracking
bot.on('move', this.updateState)
bot.on('move', () => {
this.updateMovementState()
})
// Item tracking
bot.on('heldItemChanged', () => {
@ -77,8 +70,22 @@ export class PlayerStateManager implements IPlayerState {
bot.inventory.on('updateSlot', (index) => {
if (index === 45) this.updateHeldItem(true)
})
const updateSneakingOrFlying = () => {
this.updateMovementState()
this.reactive.sneaking = bot.controlState.sneak
this.reactive.flying = gameAdditionalState.isFlying
this.reactive.eyeHeight = bot.controlState.sneak && !gameAdditionalState.isFlying ? 1.27 : 1.62
}
bot.on('physicsTick', () => {
if (this.isUsingItem) this.itemUsageTicks++
if (this.isUsingItem) this.reactive.itemUsageTicks++
updateSneakingOrFlying()
})
// todo move from gameAdditionalState to reactive directly
subscribeKey(gameAdditionalState, 'isSneaking', () => {
updateSneakingOrFlying()
})
subscribeKey(gameAdditionalState, 'isFlying', () => {
updateSneakingOrFlying()
})
// Initial held items setup
@ -89,14 +96,12 @@ export class PlayerStateManager implements IPlayerState {
this.reactive.gameMode = bot.game.gameMode
})
this.reactive.gameMode = bot.game?.gameMode
}
get shouldHideHand () {
return this.reactive.gameMode === 'spectator'
this.watchReactive()
}
// #region Movement and Physics State
private updateState () {
private updateMovementState () {
if (!bot?.entity || this.disableStateUpdates) return
const { velocity } = bot.entity
@ -109,7 +114,7 @@ export class PlayerStateManager implements IPlayerState {
const deltaTime = now - this.lastUpdateTime
this.lastUpdateTime = now
this.lastVelocity = velocity
// this.lastVelocity = velocity
// Update time off ground
if (isOnGround) {
@ -118,60 +123,26 @@ export class PlayerStateManager implements IPlayerState {
this.timeOffGround += deltaTime
}
if (this.isSneaking() || this.isFlying() || (this.timeOffGround > OFF_GROUND_THRESHOLD)) {
this.movementState = 'SNEAKING'
if (gameAdditionalState.isSneaking || gameAdditionalState.isFlying || (this.timeOffGround > OFF_GROUND_THRESHOLD)) {
this.reactive.movementState = 'SNEAKING'
} else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) {
this.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY
this.reactive.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY
? 'SPRINTING'
: 'WALKING'
} else {
this.movementState = 'NOT_MOVING'
this.reactive.movementState = 'NOT_MOVING'
}
}
getMovementState (): MovementState {
return this.movementState
}
getVelocity (): Vec3 {
return this.lastVelocity
}
getEyeHeight (): number {
return bot.controlState.sneak && !this.isFlying() ? 1.27 : 1.62
}
isOnGround (): boolean {
return bot?.entity?.onGround ?? true
}
isSneaking (): boolean {
return gameAdditionalState.isSneaking
}
isFlying (): boolean {
return gameAdditionalState.isFlying
}
isSprinting (): boolean {
return gameAdditionalState.isSprinting
}
getPosition (): Vec3 {
return bot.entity?.position ?? new Vec3(0, 0, 0)
}
// #endregion
// #region Held Item State
private updateHeldItem (isLeftHand: boolean) {
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
if (!newItem) {
if (isLeftHand) {
this.offHandItem = undefined
this.reactive.heldItemOff = undefined
} else {
this.heldItem = undefined
this.reactive.heldItemMain = undefined
}
this.events.emit('heldItemChanged', undefined, isLeftHand)
return
}
@ -186,42 +157,36 @@ export class PlayerStateManager implements IPlayerState {
}
if (isLeftHand) {
this.offHandItem = item
this.reactive.heldItemOff = item
} else {
this.heldItem = item
this.reactive.heldItemMain = item
}
this.events.emit('heldItemChanged', item, isLeftHand)
// this.events.emit('heldItemChanged', item, isLeftHand)
}
startUsingItem () {
if (this.isUsingItem) return
this.isUsingItem = true
this.itemUsageTicks = 0
this.reactive.itemUsageTicks = 0
}
stopUsingItem () {
this.isUsingItem = false
this.itemUsageTicks = 0
this.reactive.itemUsageTicks = 0
}
getItemUsageTicks (): number {
return this.itemUsageTicks
return this.reactive.itemUsageTicks
}
getHeldItem (isLeftHand = false): HandItemBlock | undefined {
return isLeftHand ? this.offHandItem : this.heldItem
watchReactive () {
subscribeKey(this.reactive, 'eyeHeight', () => {
appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
})
}
getItemSelector (specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item): ItemSelector['properties'] {
return {
...specificProperties,
'minecraft:date': new Date(),
// "minecraft:context_dimension": bot.entityp,
'minecraft:time': bot.time.timeOfDay / 24_000,
}
}
// #endregion
}
export const playerState = PlayerStateManager.getInstance()
export const playerState = new PlayerStateControllerMain()
window.playerState = playerState

View file

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

View file

@ -1,14 +1,44 @@
import { motion, AnimatePresence } from 'framer-motion'
import PixelartIcon from './PixelartIcon'
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import { useUsingTouch } from './utilsApp'
const duration = 0.2
// save pass: login
export default ({ type = 'message', message, subMessage = '', open, icon = '', action = undefined as (() => void) | undefined }) => {
const toastHeight = 32
interface NotificationProps {
open: boolean
message: string
type?: 'message' | 'error' | 'progress'
subMessage?: string
icon?: string
action?: () => void
topPosition?: number
currentProgress?: number
totalProgress?: number
}
export default ({
type = 'message',
message,
subMessage = '',
open,
icon = '',
action = undefined as (() => void) | undefined,
topPosition = 0,
currentProgress,
totalProgress,
}: NotificationProps) => {
const isUsingTouch = useUsingTouch()
const isError = type === 'error'
icon ||= isError ? 'alert' : 'message'
const isLoader = type === 'progress'
const top = (topPosition * toastHeight) + (isUsingTouch ? 18 : 0) // add space for mobile top buttons
return <AnimatePresence>
{open && (
<motion.div
@ -20,7 +50,7 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a
onClick={action}
style={{
position: 'fixed',
top: 0,
top,
right: 0,
width: '180px',
whiteSpace: 'nowrap',
@ -28,31 +58,54 @@ export default ({ type = 'message', message, subMessage = '', open, icon = '', a
display: 'flex',
gap: 4,
alignItems: 'center',
padding: '3px 5px',
padding: '4px 5px',
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',
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={{
display: 'flex',
flexDirection: 'column',
gap: 2,
width: '100%',
}}>
<div style={{
whiteSpace: 'normal',
}}>
{message}
{translate(message)}
</div>
<div style={{
fontSize: '7px',
whiteSpace: 'nowrap',
color: 'lightgray',
marginTop: 3,
}}>
{subMessage}
{translate(subMessage)}
</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>
</motion.div>
)}

View file

@ -8,7 +8,11 @@ import Slider from './Slider'
import styles from './rendererDebugMenu.module.css'
export default () => {
const worldRenderer = window.world as WorldRendererCommon
const worldRenderer = window.world as WorldRendererCommon | undefined
return worldRenderer ? <RendererDebugMenu worldRenderer={worldRenderer} /> : null
}
const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererCommon }) => {
const { reactiveDebugParams } = worldRenderer
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')}
>
<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_title}>
<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 updateFovAnimation = () => {
if (!bot) return
if (!playerState.ready) return
// Calculate base FOV modifier
let fovModifier = 1
@ -39,10 +39,10 @@ const updateFovAnimation = () => {
}
// Item usage modifier
if (playerState.getHeldItem()) {
const heldItem = playerState.getHeldItem()
if (heldItem?.name === 'bow' && playerState.getItemUsageTicks() > 0) {
const ticksUsingItem = playerState.getItemUsageTicks()
if (playerState.reactive.heldItemMain) {
const heldItem = playerState.reactive.heldItemMain
if (heldItem?.name === 'bow' && playerState.reactive.itemUsageTicks > 0) {
const ticksUsingItem = playerState.reactive.itemUsageTicks
let usageProgress = ticksUsingItem / 20
if (usageProgress > 1) {
usageProgress = 1
@ -88,8 +88,4 @@ export const watchFov = () => {
customEvents.on('gameLoaded', () => {
updateFovAnimation()
})
subscribeKey(gameAdditionalState, 'isSneaking', () => {
appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
})
}

View file

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

View file

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