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