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