feat: app <-> renderer REWORK. Add new layers for stability (#315)
This commit is contained in:
parent
df10bc6f1b
commit
0f3145bb8e
75 changed files with 2618 additions and 1929 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -10,7 +10,7 @@ localSettings.mjs
|
|||
dist*
|
||||
.DS_Store
|
||||
.idea/
|
||||
world
|
||||
/world
|
||||
data*.json
|
||||
out
|
||||
*.iml
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ For building the project yourself / contributing, see [Development, Debugging &
|
|||
- Custom protocol channel extensions (eg for custom block models in the world)
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- ~~Google Drive support for reading / saving worlds back to the cloud~~
|
||||
- Support for custom rendering 3D engines. Modular architecture.
|
||||
- even even more!
|
||||
|
||||
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
import '../../src/getCollisionShapes'
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { BasePlaygroundScene } from './baseScene'
|
||||
import { playgroundGlobalUiState } from './playgroundUi'
|
||||
import * as scenes from './scenes'
|
||||
if (!new URL(location.href).searchParams.get('playground')) location.href = '/?playground=true'
|
||||
// import { BasePlaygroundScene } from './baseScene'
|
||||
// import { playgroundGlobalUiState } from './playgroundUi'
|
||||
// import * as scenes from './scenes'
|
||||
|
||||
const qsScene = new URLSearchParams(window.location.search).get('scene')
|
||||
const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main
|
||||
playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization', 'allEntities']
|
||||
playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||
// const qsScene = new URLSearchParams(window.location.search).get('scene')
|
||||
// const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main
|
||||
// playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization', 'allEntities']
|
||||
// playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||
|
||||
const scene = new Scene()
|
||||
globalThis.scene = scene
|
||||
// const scene = new Scene()
|
||||
// globalThis.scene = scene
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { EntityDebugFlags, EntityMesh, rendererSpecialHandled } from '../../viewer/lib/entity/EntityMesh'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-nocheck
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import GUI, { Controller } from 'lil-gui'
|
||||
import * as THREE from 'three'
|
||||
|
|
@ -173,7 +174,6 @@ class MainScene extends BasePlaygroundScene {
|
|||
canvas.height = size
|
||||
renderer.setSize(size, size)
|
||||
|
||||
//@ts-expect-error
|
||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||
viewer.scene.background = null
|
||||
|
||||
|
|
|
|||
15
renderer/viewer/baseGraphicsBackend.ts
Normal file
15
renderer/viewer/baseGraphicsBackend.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { RendererReactiveState } from '../../src/appViewer'
|
||||
|
||||
export const getDefaultRendererState = (): RendererReactiveState => {
|
||||
return {
|
||||
world: {
|
||||
chunksLoaded: [],
|
||||
chunksTotalNumber: 0,
|
||||
allChunksLoaded: true,
|
||||
mesherWork: false,
|
||||
intersectMedia: null
|
||||
},
|
||||
renderer: '',
|
||||
preventEscapeMenu: false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
Viewer: require('./lib/viewer').Viewer,
|
||||
WorldDataEmitter: require('./lib/worldDataEmitter').WorldDataEmitter,
|
||||
Entity: require('./lib/entity/EntityMesh'),
|
||||
getBufferFromStream: require('./lib/simpleUtils').getBufferFromStream
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { Vec3 } from 'vec3'
|
|||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
import { proxy } from 'valtio'
|
||||
import { GameMode } from 'mineflayer'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
|
||||
export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
|
||||
|
|
@ -22,6 +23,7 @@ export interface IPlayerState {
|
|||
isFlying(): boolean
|
||||
isSprinting (): boolean
|
||||
getItemUsageTicks?(): number
|
||||
getPosition(): Vec3
|
||||
// isUsingItem?(): boolean
|
||||
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
|
||||
username?: string
|
||||
|
|
@ -31,12 +33,21 @@ export interface IPlayerState {
|
|||
|
||||
reactive: {
|
||||
playerSkin: string | undefined
|
||||
inWater: boolean
|
||||
backgroundColor: [number, number, number]
|
||||
ambientLight: number
|
||||
directionalLight: number
|
||||
gameMode?: GameMode
|
||||
}
|
||||
}
|
||||
|
||||
export class BasePlayerState implements IPlayerState {
|
||||
reactive = proxy({
|
||||
playerSkin: undefined
|
||||
playerSkin: undefined as string | undefined,
|
||||
inWater: false,
|
||||
backgroundColor: [0, 0, 0] as [number, number, number],
|
||||
ambientLight: 0,
|
||||
directionalLight: 0,
|
||||
})
|
||||
protected movementState: MovementState = 'NOT_MOVING'
|
||||
protected velocity = new Vec3(0, 0, 0)
|
||||
|
|
@ -74,6 +85,10 @@ export class BasePlayerState implements IPlayerState {
|
|||
return this.sprinting
|
||||
}
|
||||
|
||||
getPosition (): Vec3 {
|
||||
return new Vec3(0, 0, 0)
|
||||
}
|
||||
|
||||
// For testing purposes
|
||||
setState (state: Partial<{
|
||||
movementState: MovementState
|
||||
|
|
|
|||
|
|
@ -14,17 +14,19 @@ import mojangson from 'mojangson'
|
|||
import { snakeCase } from 'change-case'
|
||||
import { Item } from 'prismarine-item'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { EntityMetadataVersions } from '../../../src/mcDataTypes'
|
||||
import * as Entity from './entity/EntityMesh'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { WalkingGeneralSwing } from './entity/animations'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { armorModel, armorTextures } from './entity/armorModels'
|
||||
import { Viewer } from './viewer'
|
||||
import { getBlockMeshFromModel } from './holdingBlock'
|
||||
import { ItemSpecificContextProperties } from './basePlayerState'
|
||||
import { loadSkinImage, getLookupUrl, stevePngUrl, steveTexture } from './utils/skins'
|
||||
import { loadTexture } from './utils'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
||||
|
|
@ -167,7 +169,7 @@ const nametags = {}
|
|||
|
||||
const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
|
||||
|
||||
function getEntityMesh (entity, world, options, 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
|
||||
|
|
@ -183,6 +185,7 @@ function getEntityMesh (entity, world, options, overrides) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!isEntityAttackable(loadedData, entity)) return
|
||||
const geometry = new THREE.BoxGeometry(entity.width, entity.height, entity.width)
|
||||
geometry.translate(0, entity.height / 2, 0)
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0xff_00_ff })
|
||||
|
|
@ -206,30 +209,17 @@ export type SceneEntity = THREE.Object3D & {
|
|||
additionalCleanup?: () => void
|
||||
}
|
||||
|
||||
export class Entities extends EventEmitter {
|
||||
export class Entities {
|
||||
entities = {} as Record<string, SceneEntity>
|
||||
entitiesOptions: {
|
||||
fontFamily?: string
|
||||
} = {}
|
||||
entitiesOptions = {
|
||||
fontFamily: 'mojangles'
|
||||
}
|
||||
debugMode: string
|
||||
onSkinUpdate: () => void
|
||||
clock = new THREE.Clock()
|
||||
rendering = true
|
||||
itemsTexture: THREE.Texture | null = null
|
||||
currentlyRendering = true
|
||||
cachedMapsImages = {} as Record<number, string>
|
||||
itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
|
||||
getItemUv: undefined | ((item: Record<string, any>, specificProps: ItemSpecificContextProperties) => {
|
||||
texture: THREE.Texture;
|
||||
u: number;
|
||||
v: number;
|
||||
su?: number;
|
||||
sv?: number;
|
||||
size?: number;
|
||||
modelName?: string;
|
||||
} | {
|
||||
resolvedModel: BlockModel
|
||||
modelName: string
|
||||
} | undefined)
|
||||
|
||||
get entitiesByName (): Record<string, SceneEntity[]> {
|
||||
const byName: Record<string, SceneEntity[]> = {}
|
||||
|
|
@ -245,16 +235,14 @@ export class Entities extends EventEmitter {
|
|||
return Object.values(this.entities).filter(entity => entity.visible).length
|
||||
}
|
||||
|
||||
constructor (public viewer: Viewer) {
|
||||
super()
|
||||
this.entitiesOptions = {}
|
||||
constructor (public worldRenderer: WorldRendererThree) {
|
||||
this.debugMode = 'none'
|
||||
this.onSkinUpdate = () => { }
|
||||
}
|
||||
|
||||
clear () {
|
||||
for (const mesh of Object.values(this.entities)) {
|
||||
this.viewer.scene.remove(mesh)
|
||||
this.worldRenderer.scene.remove(mesh)
|
||||
disposeObject(mesh)
|
||||
}
|
||||
this.entities = {}
|
||||
|
|
@ -273,19 +261,24 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
|
||||
setRendering (rendering: boolean, entity: THREE.Object3D | null = null) {
|
||||
this.rendering = rendering
|
||||
this.currentlyRendering = rendering
|
||||
for (const ent of entity ? [entity] : Object.values(this.entities)) {
|
||||
if (rendering) {
|
||||
if (!this.viewer.scene.children.includes(ent)) this.viewer.scene.add(ent)
|
||||
if (!this.worldRenderer.scene.children.includes(ent)) this.worldRenderer.scene.add(ent)
|
||||
} else {
|
||||
this.viewer.scene.remove(ent)
|
||||
this.worldRenderer.scene.remove(ent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const renderEntitiesConfig = this.worldRenderer.worldRendererConfig.renderEntities
|
||||
if (renderEntitiesConfig !== this.currentlyRendering) {
|
||||
this.setRendering(renderEntitiesConfig)
|
||||
}
|
||||
|
||||
const dt = this.clock.getDelta()
|
||||
const botPos = this.viewer.world.viewerPosition
|
||||
const botPos = this.worldRenderer.viewerPosition
|
||||
const VISIBLE_DISTANCE = 8 * 8
|
||||
|
||||
for (const entityId of Object.keys(this.entities)) {
|
||||
|
|
@ -310,7 +303,7 @@ export class Entities extends EventEmitter {
|
|||
const chunkKey = `${chunkX},${chunkZ}`
|
||||
|
||||
// Entity is visible if within 16 blocks OR in a finished chunk
|
||||
entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.viewer.world.finishedChunks[chunkKey])
|
||||
entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.finishedChunks[chunkKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -349,7 +342,7 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
|
||||
if (typeof skinUrl !== 'string') throw new Error('Invalid skin url')
|
||||
const renderEars = this.viewer.world.config.renderEars || username === 'deadmau5'
|
||||
const renderEars = this.worldRenderer.worldRendererConfig.renderEars || username === 'deadmau5'
|
||||
void this.loadAndApplySkin(entityId, skinUrl, renderEars).then(() => {
|
||||
if (capeUrl) {
|
||||
if (capeUrl === true && username) {
|
||||
|
|
@ -500,11 +493,11 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
|
||||
getItemMesh (item, specificProps: ItemSpecificContextProperties, previousModel?: string) {
|
||||
const textureUv = this.getItemUv?.(item, specificProps)
|
||||
const textureUv = this.worldRenderer.getItemRenderData(item, specificProps)
|
||||
if (previousModel && previousModel === textureUv?.modelName) return undefined
|
||||
|
||||
if (textureUv && 'resolvedModel' in textureUv) {
|
||||
const mesh = getBlockMeshFromModel(this.viewer.world.material, textureUv.resolvedModel, textureUv.modelName)
|
||||
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
|
||||
|
|
@ -525,9 +518,11 @@ export class Entities extends EventEmitter {
|
|||
|
||||
// TODO: Render proper model (especially for blocks) instead of flat texture
|
||||
if (textureUv) {
|
||||
const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture
|
||||
// todo use geometry buffer uv instead!
|
||||
const { u, v, size, su, sv, texture } = textureUv
|
||||
const itemsTexture = texture.clone()
|
||||
const { u, v, su, sv } = textureUv
|
||||
const size = undefined
|
||||
const itemsTexture = textureThree.clone()
|
||||
itemsTexture.flipY = true
|
||||
const sizeY = (sv ?? size)!
|
||||
const sizeX = (su ?? size)!
|
||||
|
|
@ -584,6 +579,8 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
|
||||
update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) {
|
||||
const justAdded = !this.entities[entity.id]
|
||||
|
||||
const isPlayerModel = entity.name === 'player'
|
||||
if (entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
||||
|
|
@ -601,8 +598,8 @@ export class Entities extends EventEmitter {
|
|||
e.traverse(c => {
|
||||
if (c['additionalCleanup']) c['additionalCleanup']()
|
||||
})
|
||||
this.emit('remove', entity)
|
||||
this.viewer.scene.remove(e)
|
||||
this.onRemoveEntity(entity)
|
||||
this.worldRenderer.scene.remove(e)
|
||||
disposeObject(e)
|
||||
// todo dispose textures as well ?
|
||||
delete this.entities[entity.id]
|
||||
|
|
@ -675,7 +672,7 @@ export class Entities extends EventEmitter {
|
|||
//@ts-expect-error
|
||||
playerObject.animation.isMoving = false
|
||||
} else {
|
||||
mesh = getEntityMesh(entity, this.viewer.world, this.entitiesOptions, overrides)
|
||||
mesh = getEntityMesh(entity, this.worldRenderer, this.entitiesOptions, overrides)
|
||||
}
|
||||
if (!mesh) return
|
||||
mesh.name = 'mesh'
|
||||
|
|
@ -694,20 +691,20 @@ export class Entities extends EventEmitter {
|
|||
group.add(mesh)
|
||||
group.add(boxHelper)
|
||||
boxHelper.visible = false
|
||||
this.viewer.scene.add(group)
|
||||
this.worldRenderer.scene.add(group)
|
||||
|
||||
e = group
|
||||
e.name = 'entity'
|
||||
e['realName'] = entity.name
|
||||
this.entities[entity.id] = e
|
||||
|
||||
this.emit('add', entity)
|
||||
this.onAddEntity(entity)
|
||||
|
||||
if (isPlayerModel) {
|
||||
this.updatePlayerSkin(entity.id, entity.username, entity.uuid, overrides?.texture || stevePngUrl)
|
||||
}
|
||||
this.setDebugMode(this.debugMode, group)
|
||||
this.setRendering(this.rendering, group)
|
||||
this.setRendering(this.currentlyRendering, group)
|
||||
} else {
|
||||
mesh = e.children.find(c => c.name === 'mesh')
|
||||
}
|
||||
|
|
@ -716,10 +713,10 @@ export class Entities extends EventEmitter {
|
|||
if (entity.equipment) {
|
||||
this.addItemModel(e, 'left', entity.equipment[0])
|
||||
this.addItemModel(e, 'right', entity.equipment[1])
|
||||
addArmorModel(e, 'feet', entity.equipment[2])
|
||||
addArmorModel(e, 'legs', entity.equipment[3], 2)
|
||||
addArmorModel(e, 'chest', entity.equipment[4])
|
||||
addArmorModel(e, 'head', entity.equipment[5])
|
||||
addArmorModel(this.worldRenderer, e, 'feet', entity.equipment[2])
|
||||
addArmorModel(this.worldRenderer, e, 'legs', entity.equipment[3], 2)
|
||||
addArmorModel(this.worldRenderer, e, 'chest', entity.equipment[4])
|
||||
addArmorModel(this.worldRenderer, e, 'head', entity.equipment[5])
|
||||
}
|
||||
|
||||
const meta = getGeneralEntitiesMetadata(entity)
|
||||
|
|
@ -884,6 +881,30 @@ export class Entities extends EventEmitter {
|
|||
e.username = entity.username
|
||||
}
|
||||
|
||||
if (entity.type === 'player' && entity.equipment && e.playerObject) {
|
||||
const { playerObject } = e
|
||||
playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape'
|
||||
if (playerObject.cape.map === null) {
|
||||
playerObject.cape.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEntityPosition(entity, justAdded, overrides)
|
||||
}
|
||||
|
||||
updateEntityPosition (entity: import('prismarine-entity').Entity, justAdded: boolean, overrides: { rotation?: { head?: { y: number, x: number } } }) {
|
||||
const e = this.entities[entity.id]
|
||||
if (!e) return
|
||||
const ANIMATION_DURATION = justAdded ? 0 : TWEEN_DURATION
|
||||
if (entity.position) {
|
||||
new TWEEN.Tween(e.position).to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION).start()
|
||||
}
|
||||
if (entity.yaw) {
|
||||
const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
|
||||
const dy = 2 * da % (Math.PI * 2) - da
|
||||
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, ANIMATION_DURATION).start()
|
||||
}
|
||||
|
||||
if (e?.playerObject && overrides?.rotation?.head) {
|
||||
const playerObject = e.playerObject as PlayerObjectType
|
||||
const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0
|
||||
|
|
@ -891,16 +912,34 @@ export class Entities extends EventEmitter {
|
|||
playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0
|
||||
}
|
||||
|
||||
if (entity.pos) {
|
||||
new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, TWEEN_DURATION).start()
|
||||
}
|
||||
if (entity.yaw) {
|
||||
const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
|
||||
const dy = 2 * da % (Math.PI * 2) - da
|
||||
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, TWEEN_DURATION).start()
|
||||
this.maybeRenderPlayerSkin(entity)
|
||||
}
|
||||
|
||||
onAddEntity (entity: import('prismarine-entity').Entity) {
|
||||
}
|
||||
|
||||
loadedSkinEntityIds = new Set<number>()
|
||||
maybeRenderPlayerSkin (entity: import('prismarine-entity').Entity) {
|
||||
const mesh = this.entities[entity.id]
|
||||
if (!mesh) return
|
||||
if (!mesh.playerObject || !this.worldRenderer.worldRendererConfig.fetchPlayerSkins) return
|
||||
const MAX_DISTANCE_SKIN_LOAD = 128
|
||||
const cameraPos = this.worldRenderer.camera.position
|
||||
const distance = entity.position.distanceTo(new Vec3(cameraPos.x, cameraPos.y, cameraPos.z))
|
||||
if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) {
|
||||
if (this.entities[entity.id]) {
|
||||
if (this.loadedSkinEntityIds.has(entity.id)) return
|
||||
this.loadedSkinEntityIds.add(entity.id)
|
||||
this.updatePlayerSkin(entity.id, entity.username, entity.uuid, true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playerPerAnimation = {} as Record<number, string>
|
||||
onRemoveEntity (entity: import('prismarine-entity').Entity) {
|
||||
this.loadedSkinEntityIds.delete(entity.id)
|
||||
}
|
||||
|
||||
updateMap (mapNumber: string | number, data: string) {
|
||||
this.cachedMapsImages[mapNumber] = data
|
||||
let itemFrameMeshes = this.itemFrameMaps[mapNumber]
|
||||
|
|
@ -983,7 +1022,7 @@ export class Entities extends EventEmitter {
|
|||
const itemObject = this.getItemMesh(item, {
|
||||
'minecraft:display_context': 'thirdperson',
|
||||
})
|
||||
if (itemObject) {
|
||||
if (itemObject?.mesh) {
|
||||
entityMesh.traverse(c => {
|
||||
if (c.name.toLowerCase() === parentName) {
|
||||
const group = new THREE.Object3D()
|
||||
|
|
@ -1043,7 +1082,7 @@ function getSpecificEntityMetadata<T extends keyof EntityMetadataVersions> (name
|
|||
return getGeneralEntitiesMetadata(entity) as any
|
||||
}
|
||||
|
||||
function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) {
|
||||
function addArmorModel (worldRenderer: WorldRendererThree, entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) {
|
||||
if (!item) {
|
||||
removeArmorModel(entityMesh, slotType)
|
||||
return
|
||||
|
|
@ -1077,7 +1116,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
if (!texturePath) {
|
||||
// TODO: Support mirroring on certain parts of the model
|
||||
const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}`
|
||||
texturePath = viewer.world.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)
|
||||
|
|
@ -1098,7 +1137,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
material.map = texture
|
||||
})
|
||||
} else {
|
||||
mesh = getMesh(viewer.world, texturePath, armorModel[slotType])
|
||||
mesh = getMesh(worldRenderer, texturePath, armorModel[slotType])
|
||||
mesh.name = meshName
|
||||
material = mesh.material
|
||||
if (!isPlayerHead) {
|
||||
|
|
@ -1115,7 +1154,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
} else {
|
||||
material.color.setHex(0xB5_6D_51) // default brown color
|
||||
}
|
||||
addArmorModel(entityMesh, slotType, item, layer, true)
|
||||
addArmorModel(worldRenderer, entityMesh, slotType, item, layer, true)
|
||||
} else {
|
||||
material.color.setHex(0xFF_FF_FF)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { Vec3 } from 'vec3'
|
|||
import arrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
|
||||
import spectralArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
|
||||
import tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
|
||||
import { WorldRendererCommon } from '../worldrendererCommon'
|
||||
import { loadTexture } from '../utils'
|
||||
import { WorldRendererThree } from '../worldrendererThree'
|
||||
import entities from './entities.json'
|
||||
import { externalModels } from './objModels'
|
||||
import externalTexturesJson from './externalTextures.json'
|
||||
|
|
@ -223,7 +223,7 @@ function addCube (
|
|||
}
|
||||
|
||||
export function getMesh (
|
||||
worldRenderer: WorldRendererCommon | undefined,
|
||||
worldRenderer: WorldRendererThree | undefined,
|
||||
texture: string,
|
||||
jsonModel: JsonModel,
|
||||
overrides: EntityOverrides = {},
|
||||
|
|
@ -237,7 +237,7 @@ export function getMesh (
|
|||
if (useBlockTexture) {
|
||||
if (!worldRenderer) throw new Error('worldRenderer is required for block textures')
|
||||
const blockName = texture.slice(6)
|
||||
const textureInfo = worldRenderer.blocksAtlasParser!.getTextureInfo(blockName)
|
||||
const textureInfo = worldRenderer.resourcesManager.currentResources!.blocksAtlasParser.getTextureInfo(blockName)
|
||||
if (textureInfo) {
|
||||
textureWidth = blocksTexture!.image.width
|
||||
textureHeight = blocksTexture!.image.height
|
||||
|
|
@ -437,7 +437,7 @@ export class EntityMesh {
|
|||
constructor (
|
||||
version: string,
|
||||
type: string,
|
||||
worldRenderer?: WorldRendererCommon,
|
||||
worldRenderer?: WorldRendererThree,
|
||||
overrides: EntityOverrides = {},
|
||||
debugFlags: EntityDebugFlags = {}
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -2,27 +2,26 @@
|
|||
import { ItemRenderer, Identifier, ItemStack, NbtString, Structure, StructureRenderer, ItemRendererResources, BlockDefinition, BlockModel, TextureAtlas, Resources, ItemModel } from 'deepslate'
|
||||
import { mat4, vec3 } from 'gl-matrix'
|
||||
import { AssetsParser } from 'mc-assets/dist/assetsParser'
|
||||
import { getLoadedImage } from 'mc-assets/dist/utils'
|
||||
import { getLoadedImage, versionToNumber } from 'mc-assets/dist/utils'
|
||||
import { BlockModel as BlockModelMcAssets, AtlasParser } from 'mc-assets'
|
||||
import { getLoadedBlockstatesStore, getLoadedModelsStore } from 'mc-assets/dist/stores'
|
||||
import { makeTextureAtlas } from 'mc-assets/dist/atlasCreator'
|
||||
import { proxy, ref } from 'valtio'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
import { versionToNumber } from '../prepare/utils'
|
||||
|
||||
export const activeGuiAtlas = proxy({
|
||||
atlas: null as null | { json, image },
|
||||
})
|
||||
|
||||
export const getNonFullBlocksModels = () => {
|
||||
let version = viewer.world.texturesVersion ?? 'latest'
|
||||
let version = appViewer.resourcesManager.currentResources!.version ?? 'latest'
|
||||
if (versionToNumber(version) < versionToNumber('1.13')) version = '1.13'
|
||||
const itemsDefinitions = viewer.world.itemsDefinitionsStore.data.latest
|
||||
const itemsDefinitions = appViewer.resourcesManager.itemsDefinitionsStore.data.latest
|
||||
const blockModelsResolved = {} as Record<string, any>
|
||||
const itemsModelsResolved = {} as Record<string, any>
|
||||
const fullBlocksWithNonStandardDisplay = [] as string[]
|
||||
const handledItemsWithDefinitions = new Set()
|
||||
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(viewer.world.blockstatesModels), getLoadedModelsStore(viewer.world.blockstatesModels))
|
||||
const assetsParser = new AssetsParser(version, getLoadedBlockstatesStore(appViewer.resourcesManager.currentResources!.blockstatesModels), getLoadedModelsStore(appViewer.resourcesManager.currentResources!.blockstatesModels))
|
||||
|
||||
const standardGuiDisplay = {
|
||||
'rotation': [
|
||||
|
|
@ -54,7 +53,7 @@ export const getNonFullBlocksModels = () => {
|
|||
}
|
||||
|
||||
for (const [name, definition] of Object.entries(itemsDefinitions)) {
|
||||
const item = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
const item = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, {
|
||||
version,
|
||||
name,
|
||||
properties: {
|
||||
|
|
@ -97,7 +96,7 @@ export const getNonFullBlocksModels = () => {
|
|||
}
|
||||
}
|
||||
|
||||
for (const [name, blockstate] of Object.entries(viewer.world.blockstatesModels.blockstates.latest)) {
|
||||
for (const [name, blockstate] of Object.entries(appViewer.resourcesManager.currentResources!.blockstatesModels.blockstates.latest)) {
|
||||
if (handledItemsWithDefinitions.has(name)) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -120,7 +119,8 @@ export const getNonFullBlocksModels = () => {
|
|||
const RENDER_SIZE = 64
|
||||
|
||||
const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isItems = false) => {
|
||||
const img = await getLoadedImage(isItems ? viewer.world.itemsAtlasParser!.latestImage : viewer.world.blocksAtlasParser!.latestImage)
|
||||
const { currentResources } = appViewer.resourcesManager
|
||||
const img = await getLoadedImage(isItems ? currentResources!.itemsAtlasParser.latestImage : currentResources!.blocksAtlasParser.latestImage)
|
||||
const canvasTemp = document.createElement('canvas')
|
||||
canvasTemp.width = img.width
|
||||
canvasTemp.height = img.height
|
||||
|
|
@ -129,7 +129,7 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, isIt
|
|||
ctx.imageSmoothingEnabled = false
|
||||
ctx.drawImage(img, 0, 0)
|
||||
|
||||
const atlasParser = isItems ? viewer.world.itemsAtlasParser! : viewer.world.blocksAtlasParser!
|
||||
const atlasParser = isItems ? currentResources!.itemsAtlasParser : currentResources!.blocksAtlasParser
|
||||
const textureAtlas = new TextureAtlas(
|
||||
ctx.getImageData(0, 0, img.width, img.height),
|
||||
Object.fromEntries(Object.entries(atlasParser.atlas.latest.textures).map(([key, value]) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { getMyHand } from './hand'
|
||||
|
|
@ -10,6 +10,7 @@ import { SmoothSwitcher } from './smoothSwitcher'
|
|||
import { watchProperty } from './utils/proxy'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { WorldRendererConfig } from './worldrendererCommon'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
||||
export type HandItemBlock = {
|
||||
name?
|
||||
|
|
@ -114,14 +115,17 @@ export default class HoldingBlock {
|
|||
offHandModeLegacy = false
|
||||
|
||||
swingAnimator: HandSwingAnimator | undefined
|
||||
playerState: IPlayerState
|
||||
config: WorldRendererConfig
|
||||
|
||||
constructor (public playerState: IPlayerState, public config: WorldRendererConfig, public offHand = false) {
|
||||
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.config = worldRenderer.displayOptions.inWorldRenderingConfig
|
||||
|
||||
this.offHandDisplay = this.offHand
|
||||
// this.offHandDisplay = true
|
||||
|
|
@ -327,7 +331,7 @@ export default class HoldingBlock {
|
|||
|
||||
let blockInner: THREE.Object3D | undefined
|
||||
if (handItem.type === 'item' || handItem.type === 'block') {
|
||||
const result = viewer.entities.getItemMesh({
|
||||
const result = this.worldRenderer.entities.getItemMesh({
|
||||
...handItem.fullItem,
|
||||
itemId: handItem.id,
|
||||
}, {
|
||||
|
|
@ -901,8 +905,7 @@ class HandSwingAnimator {
|
|||
}
|
||||
}
|
||||
|
||||
export const getBlockMeshFromModel = (material: THREE.Material, model: BlockModel, name: string) => {
|
||||
const blockProvider = worldBlockProvider(viewer.world.blockstatesModels, viewer.world.blocksAtlasParser!.atlas, 'latest')
|
||||
export const getBlockMeshFromModel = (material: THREE.Material, model: BlockModel, name: string, blockProvider: WorldBlockProvider) => {
|
||||
const worldRenderModel = blockProvider.transformModel(model, {
|
||||
name,
|
||||
properties: {}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,9 @@ const handleMessage = data => {
|
|||
}
|
||||
case 'blockUpdate': {
|
||||
const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
|
||||
world.setBlockStateId(loc, data.stateId)
|
||||
if (data.stateId !== undefined && data.stateId !== null) {
|
||||
world.setBlockStateId(loc, data.stateId)
|
||||
}
|
||||
|
||||
const chunkKey = `${Math.floor(loc.x / 16) * 16},${Math.floor(loc.z / 16) * 16}`
|
||||
if (data.customBlockModels) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { BlockType } from '../../../playground/shared'
|
||||
|
||||
// only here for easier testing
|
||||
export const defaultMesherConfig = {
|
||||
version: '',
|
||||
enableLighting: true,
|
||||
|
|
|
|||
61
renderer/viewer/lib/threeJsSound.ts
Normal file
61
renderer/viewer/lib/threeJsSound.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
||||
export interface SoundSystem {
|
||||
playSound: (position: Vec3, path: string, volume?: number, pitch?: number) => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
export class ThreeJsSound implements SoundSystem {
|
||||
audioListener: THREE.AudioListener | undefined
|
||||
private readonly activeSounds = new Set<THREE.PositionalAudio>()
|
||||
|
||||
constructor (public worldRenderer: WorldRendererThree) {
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
|
||||
if (!this.audioListener) {
|
||||
this.audioListener = new THREE.AudioListener()
|
||||
this.worldRenderer.camera.add(this.audioListener)
|
||||
}
|
||||
|
||||
const sound = new THREE.PositionalAudio(this.audioListener)
|
||||
this.activeSounds.add(sound)
|
||||
|
||||
const audioLoader = new THREE.AudioLoader()
|
||||
const start = Date.now()
|
||||
void audioLoader.loadAsync(path).then((buffer) => {
|
||||
if (Date.now() - start > 500) return
|
||||
// play
|
||||
sound.setBuffer(buffer)
|
||||
sound.setRefDistance(20)
|
||||
sound.setVolume(volume)
|
||||
sound.setPlaybackRate(pitch) // set the pitch
|
||||
this.worldRenderer.scene.add(sound)
|
||||
// set sound position
|
||||
sound.position.set(position.x, position.y, position.z)
|
||||
sound.onEnded = () => {
|
||||
this.worldRenderer.scene.remove(sound)
|
||||
sound.disconnect()
|
||||
this.activeSounds.delete(sound)
|
||||
audioLoader.manager.itemEnd(path)
|
||||
}
|
||||
sound.play()
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
// Stop and clean up all active sounds
|
||||
for (const sound of this.activeSounds) {
|
||||
sound.stop()
|
||||
sound.disconnect()
|
||||
}
|
||||
|
||||
// Remove and cleanup audio listener
|
||||
if (this.audioListener) {
|
||||
this.audioListener.removeFromParent()
|
||||
this.audioListener = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,19 @@ export const updateStatText = (id, text) => {
|
|||
stats[id].innerText = text
|
||||
}
|
||||
|
||||
export const removeAllStats = () => {
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const id in stats) {
|
||||
removeStat(id)
|
||||
}
|
||||
}
|
||||
|
||||
export const removeStat = (id) => {
|
||||
if (!stats[id]) return
|
||||
stats[id].remove()
|
||||
delete stats[id]
|
||||
}
|
||||
|
||||
if (typeof customEvents !== 'undefined') {
|
||||
customEvents.on('gameLoaded', () => {
|
||||
const chunksLoaded = addNewStat('chunks-loaded', 80, 0, 0)
|
||||
|
|
|
|||
|
|
@ -1,325 +0,0 @@
|
|||
import EventEmitter from 'events'
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import { getMyHand } from './hand'
|
||||
import { IPlayerState, BasePlayerState } from './basePlayerState'
|
||||
import { CameraBobbing } from './cameraBobbing'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
ambientLight: THREE.AmbientLight
|
||||
directionalLight: THREE.DirectionalLight
|
||||
world: WorldRendererCommon
|
||||
entities: Entities
|
||||
// primitives: Primitives
|
||||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
threeJsWorld: WorldRendererThree
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
private readonly cameraBobbing: CameraBobbing
|
||||
|
||||
get camera () {
|
||||
return this.world.camera
|
||||
}
|
||||
|
||||
set camera (camera) {
|
||||
this.world.camera = camera
|
||||
}
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig, public playerState: IPlayerState = new BasePlayerState()) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig, this.playerState)
|
||||
this.setWorld()
|
||||
this.resetScene()
|
||||
this.entities = new Entities(this)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
this.cameraBobbing = new CameraBobbing()
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
}
|
||||
|
||||
setWorld () {
|
||||
this.world = this.threeJsWorld
|
||||
}
|
||||
|
||||
resetScene () {
|
||||
this.scene.background = new THREE.Color('lightblue')
|
||||
|
||||
if (this.ambientLight) this.scene.remove(this.ambientLight)
|
||||
this.ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
||||
this.scene.add(this.ambientLight)
|
||||
|
||||
if (this.directionalLight) this.scene.remove(this.directionalLight)
|
||||
this.directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
||||
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
||||
this.directionalLight.castShadow = true
|
||||
this.scene.add(this.directionalLight)
|
||||
|
||||
const size = this.renderer.getSize(new THREE.Vector2())
|
||||
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
||||
}
|
||||
|
||||
resetAll () {
|
||||
this.resetScene()
|
||||
this.world.resetWorld()
|
||||
this.entities.clear()
|
||||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
setVersion (userVersion: string, texturesVersion = userVersion): void | Promise<void> {
|
||||
console.log('[viewer] Using version:', userVersion, 'textures:', texturesVersion)
|
||||
this.entities.clear()
|
||||
// this.primitives.clear()
|
||||
return this.world.setVersion(userVersion, texturesVersion)
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk, isLightUpdate = false) {
|
||||
this.world.addColumn(x, z, chunk, isLightUpdate)
|
||||
}
|
||||
|
||||
removeColumn (x: string, z: string) {
|
||||
this.world.removeColumn(x, z)
|
||||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
const set = async () => {
|
||||
const sectionX = Math.floor(pos.x / 16) * 16
|
||||
const sectionZ = Math.floor(pos.z / 16) * 16
|
||||
if (this.world.queuedChunks.has(`${sectionX},${sectionZ}`)) {
|
||||
await new Promise<void>(resolve => {
|
||||
this.world.queuedFunctions.push(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
if (!this.world.loadedChunks[`${sectionX},${sectionZ}`]) {
|
||||
// console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
}
|
||||
this.world.setBlockStateId(pos, stateId)
|
||||
}
|
||||
void set()
|
||||
}
|
||||
|
||||
async demoModel () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
|
||||
const mesh = await getMyHand()
|
||||
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
||||
setBlockPosition(mesh, pos)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
demoItem () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const { mesh } = this.entities.getItemMesh({
|
||||
itemId: 541,
|
||||
}, {})!
|
||||
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
// mesh.scale.set(0.5, 0.5, 0.5)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
updateEntity (e) {
|
||||
this.entities.update(e, this.processEntityOverrides(e, {
|
||||
rotation: {
|
||||
head: {
|
||||
x: e.headPitch ?? e.pitch,
|
||||
y: e.headYaw,
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
const yOffset = this.playerState.getEyeHeight()
|
||||
// if (this.playerState.isSneaking()) yOffset -= 0.3
|
||||
|
||||
this.world.camera = cam as THREE.PerspectiveCamera
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
|
||||
// // Update camera bobbing based on movement state
|
||||
// const velocity = this.playerState.getVelocity()
|
||||
// const movementState = this.playerState.getMovementState()
|
||||
// const isMoving = movementState === 'SPRINTING' || movementState === 'WALKING'
|
||||
// const speed = Math.hypot(velocity.x, velocity.z)
|
||||
|
||||
// // Update bobbing state
|
||||
// this.cameraBobbing.updateWalkDistance(speed)
|
||||
// this.cameraBobbing.updateBobAmount(isMoving)
|
||||
|
||||
// // Get bobbing offsets
|
||||
// const bobbing = isMoving ? this.cameraBobbing.getBobbing() : { position: { x: 0, y: 0 }, rotation: { x: 0, z: 0 } }
|
||||
|
||||
// // Apply camera position with bobbing
|
||||
// const finalPos = pos ? pos.offset(bobbing.position.x, yOffset + bobbing.position.y, 0) : null
|
||||
// this.world.updateCamera(finalPos, yaw + bobbing.rotation.x, pitch)
|
||||
|
||||
// // Apply roll rotation separately since updateCamera doesn't handle it
|
||||
// this.camera.rotation.z = bobbing.rotation.z
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
|
||||
if (!this.audioListener) {
|
||||
this.audioListener = new THREE.AudioListener()
|
||||
this.camera.add(this.audioListener)
|
||||
}
|
||||
|
||||
const sound = new THREE.PositionalAudio(this.audioListener)
|
||||
|
||||
const audioLoader = new THREE.AudioLoader()
|
||||
const start = Date.now()
|
||||
void audioLoader.loadAsync(path).then((buffer) => {
|
||||
if (Date.now() - start > 500) return
|
||||
// play
|
||||
sound.setBuffer(buffer)
|
||||
sound.setRefDistance(20)
|
||||
sound.setVolume(volume)
|
||||
sound.setPlaybackRate(pitch) // set the pitch
|
||||
this.scene.add(sound)
|
||||
// set sound position
|
||||
sound.position.set(position.x, position.y, position.z)
|
||||
sound.onEnded = () => {
|
||||
this.scene.remove(sound)
|
||||
sound.disconnect()
|
||||
audioLoader.manager.itemEnd(path)
|
||||
}
|
||||
sound.play()
|
||||
})
|
||||
}
|
||||
|
||||
addChunksBatchWaitTime = 200
|
||||
|
||||
connect (worldEmitter: EventEmitter) {
|
||||
worldEmitter.on('entity', (e) => {
|
||||
this.updateEntity(e)
|
||||
})
|
||||
|
||||
worldEmitter.on('primitive', (p) => {
|
||||
// this.updatePrimitive(p)
|
||||
})
|
||||
|
||||
let currentLoadChunkBatch = null as {
|
||||
timeout
|
||||
data
|
||||
} | null
|
||||
worldEmitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
|
||||
this.world.worldConfig = worldConfig
|
||||
this.world.queuedChunks.add(`${x},${z}`)
|
||||
const args = [x, z, chunk, isLightUpdate]
|
||||
if (!currentLoadChunkBatch) {
|
||||
// add a setting to use debounce instead
|
||||
currentLoadChunkBatch = {
|
||||
data: [],
|
||||
timeout: setTimeout(() => {
|
||||
for (const args of currentLoadChunkBatch!.data) {
|
||||
this.world.queuedChunks.delete(`${args[0]},${args[1]}`)
|
||||
this.addColumn(...args as Parameters<typeof this.addColumn>)
|
||||
}
|
||||
for (const fn of this.world.queuedFunctions) {
|
||||
fn()
|
||||
}
|
||||
this.world.queuedFunctions = []
|
||||
currentLoadChunkBatch = null
|
||||
}, this.addChunksBatchWaitTime)
|
||||
}
|
||||
}
|
||||
currentLoadChunkBatch.data.push(args)
|
||||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
worldEmitter.on('blockEntities', (blockEntities) => {
|
||||
if (this.world instanceof WorldRendererThree) (this.world).blockEntities = blockEntities
|
||||
})
|
||||
|
||||
worldEmitter.on('unloadChunk', ({ x, z }) => {
|
||||
this.removeColumn(x, z)
|
||||
})
|
||||
|
||||
worldEmitter.on('blockUpdate', ({ pos, stateId }) => {
|
||||
this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
|
||||
})
|
||||
|
||||
worldEmitter.on('chunkPosUpdate', ({ pos }) => {
|
||||
this.world.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
|
||||
worldEmitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
})
|
||||
|
||||
worldEmitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
this.world.allChunksFinished = Object.keys(this.world.finishedChunks).length === this.world.chunksLength
|
||||
})
|
||||
|
||||
worldEmitter.on('markAsLoaded', ({ x, z }) => {
|
||||
this.world.markAsLoaded(x, z)
|
||||
})
|
||||
|
||||
worldEmitter.on('updateLight', ({ pos }) => {
|
||||
if (this.world instanceof WorldRendererThree) (this.world).updateLight(pos.x, pos.z)
|
||||
})
|
||||
|
||||
worldEmitter.on('time', (timeOfDay) => {
|
||||
this.world.timeUpdated?.(timeOfDay)
|
||||
|
||||
let skyLight = 15
|
||||
if (timeOfDay < 0 || timeOfDay > 24_000) {
|
||||
throw new Error('Invalid time of day. It should be between 0 and 24000.')
|
||||
} else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
|
||||
skyLight = 15
|
||||
} else if (timeOfDay > 6000 && timeOfDay < 12_000) {
|
||||
skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
|
||||
} else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
|
||||
skyLight = ((timeOfDay - 12_000) / 6000) * 15
|
||||
}
|
||||
|
||||
skyLight = Math.floor(skyLight) // todo: remove this after optimization
|
||||
|
||||
if (this.world.mesherConfig.skyLight === skyLight) return
|
||||
this.world.mesherConfig.skyLight = skyLight
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
(this.world).rerenderAllChunks?.()
|
||||
}
|
||||
})
|
||||
|
||||
worldEmitter.emit('listening')
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
(this.world).render()
|
||||
this.entities.render()
|
||||
}
|
||||
}
|
||||
|
||||
async waitForChunksToRender () {
|
||||
await this.world.waitForChunksToRender()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
import { statsEnd, statsStart } from '../../../src/topRightStats'
|
||||
import { activeModalStack } from '../../../src/globalState'
|
||||
|
||||
// wrapper for now
|
||||
export class ViewerWrapper {
|
||||
previousWindowWidth: number
|
||||
previousWindowHeight: number
|
||||
globalObject = globalThis as any
|
||||
stopRenderOnBlur = false
|
||||
addedToPage = false
|
||||
renderInterval = 0
|
||||
renderIntervalUnfocused: number | undefined
|
||||
fpsInterval
|
||||
|
||||
constructor (public canvas: HTMLCanvasElement, public renderer?: THREE.WebGLRenderer) {
|
||||
if (this.renderer) this.globalObject.renderer = this.renderer
|
||||
}
|
||||
|
||||
addToPage (startRendering = true) {
|
||||
if (this.addedToPage) throw new Error('Already added to page')
|
||||
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
|
||||
if (this.renderer) {
|
||||
if (!this.renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
|
||||
this.renderer.setPixelRatio(pixelRatio)
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
} else {
|
||||
this.canvas.width = window.innerWidth * pixelRatio
|
||||
this.canvas.height = window.innerHeight * pixelRatio
|
||||
}
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
|
||||
this.canvas.id = 'viewer-canvas'
|
||||
document.body.appendChild(this.canvas)
|
||||
|
||||
this.addedToPage = true
|
||||
|
||||
let max = 0
|
||||
this.fpsInterval = setInterval(() => {
|
||||
if (max > 0) {
|
||||
viewer.world.droppedFpsPercentage = this.renderedFps / max
|
||||
}
|
||||
max = Math.max(this.renderedFps, max)
|
||||
this.renderedFps = 0
|
||||
}, 1000)
|
||||
if (startRendering) {
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
this.trackWindowFocus()
|
||||
}
|
||||
}
|
||||
|
||||
windowFocused = true
|
||||
trackWindowFocus () {
|
||||
window.addEventListener('focus', () => {
|
||||
this.windowFocused = true
|
||||
})
|
||||
window.addEventListener('blur', () => {
|
||||
this.windowFocused = false
|
||||
})
|
||||
}
|
||||
|
||||
dispose () {
|
||||
if (!this.addedToPage) throw new Error('Not added to page')
|
||||
this.canvas.remove()
|
||||
this.renderer?.dispose()
|
||||
// this.addedToPage = false
|
||||
clearInterval(this.fpsInterval)
|
||||
}
|
||||
|
||||
|
||||
renderedFps = 0
|
||||
lastTime = performance.now()
|
||||
delta = 0
|
||||
preRender = () => { }
|
||||
postRender = () => { }
|
||||
render (time: DOMHighResTimeStamp) {
|
||||
if (this.globalObject.stopLoop) return
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
if (activeModalStack.some(m => m.reactType === 'app-status')) return
|
||||
if (!viewer || this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
const renderInterval = (this.windowFocused ? this.renderInterval : this.renderIntervalUnfocused) ?? this.renderInterval
|
||||
if (renderInterval) {
|
||||
this.delta += time - this.lastTime
|
||||
this.lastTime = time
|
||||
if (this.delta > renderInterval) {
|
||||
this.delta %= renderInterval
|
||||
// continue rendering
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
this.preRender()
|
||||
statsStart()
|
||||
// ios bug: viewport dimensions are updated after the resize event
|
||||
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) {
|
||||
this.resizeHandler()
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
}
|
||||
viewer.render()
|
||||
this.renderedFps++
|
||||
statsEnd()
|
||||
this.postRender()
|
||||
}
|
||||
|
||||
resizeHandler () {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
|
||||
viewer.camera.aspect = width / height
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.setSize(width, height)
|
||||
}
|
||||
viewer.world.handleResize()
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { EventEmitter } from 'events'
|
|||
import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
|
||||
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'
|
||||
|
|
@ -13,11 +15,26 @@ import { chunkPos } from './simpleUtils'
|
|||
export type ChunkPosKey = string
|
||||
type ChunkPos = { x: number, z: number }
|
||||
|
||||
export type WorldDataEmitterEvents = {
|
||||
chunkPosUpdate: (data: { pos: Vec3 }) => void
|
||||
blockUpdate: (data: { pos: Vec3, stateId: number }) => void
|
||||
entity: (data: any) => void
|
||||
entityMoved: (data: any) => void
|
||||
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: any, blockEntities: any, worldConfig: any, isLightUpdate: boolean }) => void
|
||||
updateLight: (data: { pos: Vec3 }) => 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 WorldDataEmitter extends EventEmitter {
|
||||
export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<WorldDataEmitterEvents>) {
|
||||
private loadedChunks: Record<ChunkPosKey, boolean>
|
||||
private readonly lastPos: Vec3
|
||||
private eventListeners: Record<string, any> = {}
|
||||
|
|
@ -26,20 +43,18 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
addWaitTime = 1
|
||||
isPlayground = false
|
||||
|
||||
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()
|
||||
this.loadedChunks = {}
|
||||
this.lastPos = new Vec3(0, 0, 0).update(position)
|
||||
// todo
|
||||
this.emitter = this
|
||||
|
||||
this.emitter.on('mouseClick', async (click) => {
|
||||
const ori = new Vec3(click.origin.x, click.origin.y, click.origin.z)
|
||||
const dir = new Vec3(click.direction.x, click.direction.y, click.direction.z)
|
||||
const block = this.world.raycast(ori, dir, 256)
|
||||
if (!block) return
|
||||
this.emit('blockClicked', block, block.face, click.button)
|
||||
})
|
||||
}
|
||||
|
||||
setBlockStateId (position: Vec3, stateId: number) {
|
||||
|
|
@ -61,9 +76,10 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
|
||||
listenToBot (bot: typeof __type_bot) {
|
||||
const emitEntity = (e) => {
|
||||
const emitEntity = (e, name = 'entity') => {
|
||||
if (!e || e === bot.entity) return
|
||||
this.emitter.emit('entity', {
|
||||
if (!e.name) return // mineflayer received update for not spawned entity
|
||||
this.emitter.emit(name as any, {
|
||||
...e,
|
||||
pos: e.position,
|
||||
username: e.username,
|
||||
|
|
@ -86,7 +102,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
emitEntity(e)
|
||||
},
|
||||
entityMoved (e: any) {
|
||||
emitEntity(e)
|
||||
emitEntity(e, 'entityMoved')
|
||||
},
|
||||
entityGone: (e: any) => {
|
||||
this.emitter.emit('entity', { id: e.id, delete: true })
|
||||
|
|
|
|||
|
|
@ -3,28 +3,21 @@ import { EventEmitter } from 'events'
|
|||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
|
||||
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
|
||||
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
|
||||
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
||||
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
|
||||
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
|
||||
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||
import { AtlasParser, getLoadedItemDefinitionsStore } from 'mc-assets'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
|
||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
||||
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
|
||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajorVersion } from '../../../src/utils'
|
||||
import { ResourcesManager } from '../../../src/resourcesManager'
|
||||
import { DisplayWorldOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo } from './mesher/shared'
|
||||
import { HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
import { updateStatText } from './ui/newStats'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { generateGuiAtlas } from './guiRenderer'
|
||||
import { removeStat, updateStatText } from './ui/newStats'
|
||||
import { WorldDataEmitter } from './worldDataEmitter'
|
||||
import { SoundSystem } from './threeJsSound'
|
||||
import { IPlayerState } from './basePlayerState'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
|
|
@ -34,38 +27,37 @@ export const worldCleanup = buildCleanupDecorator('resetWorld')
|
|||
|
||||
export const defaultWorldRendererConfig = {
|
||||
showChunkBorders: false,
|
||||
numWorkers: 4,
|
||||
mesherWorkers: 4,
|
||||
isPlayground: false,
|
||||
renderEars: true,
|
||||
// game renderer setting actually
|
||||
showHand: false,
|
||||
viewBobbing: false
|
||||
viewBobbing: false,
|
||||
extraBlockRenderers: true,
|
||||
clipWorldBelowY: undefined as number | undefined,
|
||||
smoothLighting: true,
|
||||
enableLighting: true,
|
||||
starfield: true,
|
||||
addChunksBatchWaitTime: 200,
|
||||
vrSupport: true,
|
||||
renderEntities: true,
|
||||
fov: 75,
|
||||
fetchPlayerSkins: true,
|
||||
highlightBlockColor: 'blue',
|
||||
foreground: true
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
||||
type CustomTexturesData = {
|
||||
tileSize: number | undefined
|
||||
textures: Record<string, HTMLImageElement>
|
||||
}
|
||||
|
||||
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
|
||||
// todo
|
||||
@worldCleanup()
|
||||
threejsCursorLineMaterial: LineMaterial
|
||||
@worldCleanup()
|
||||
cursorBlock = null as Vec3 | null
|
||||
displayStats = true
|
||||
@worldCleanup()
|
||||
worldConfig = { minY: 0, worldHeight: 256 }
|
||||
// todo need to cleanup
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||
worldSizeParams = { minY: 0, worldHeight: 256 }
|
||||
cameraRoll = 0
|
||||
|
||||
@worldCleanup()
|
||||
active = false
|
||||
|
||||
version = undefined as string | undefined
|
||||
// #region CHUNK & SECTIONS TRACKING
|
||||
@worldCleanup()
|
||||
loadedChunks = {} as Record<string, boolean> // data is added for these chunks and they might be still processing
|
||||
|
|
@ -89,13 +81,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{
|
||||
dirty (pos: Vec3, value: boolean): void
|
||||
update (/* pos: Vec3, value: boolean */): void
|
||||
textureDownloaded (): void
|
||||
itemsTextureDownloaded (): void
|
||||
chunkFinished (key: string): void
|
||||
}>
|
||||
customTexturesDataUrl = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
currentTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
@worldCleanup()
|
||||
viewerPosition?: Vec3
|
||||
|
|
@ -113,32 +102,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
allChunksFinished = false
|
||||
|
||||
handleResize = () => { }
|
||||
mesherConfig = defaultMesherConfig
|
||||
camera: THREE.PerspectiveCamera
|
||||
highestBlocks = new Map<string, HighestBlockInfo>()
|
||||
blockstatesModels: any
|
||||
customBlockStates: Record<string, any> | undefined
|
||||
customModels: Record<string, any> | undefined
|
||||
itemsAtlasParser: AtlasParser | undefined
|
||||
blocksAtlasParser: AtlasParser | undefined
|
||||
blockEntities = {}
|
||||
|
||||
sourceData = {
|
||||
blocksAtlases,
|
||||
itemsAtlases,
|
||||
itemDefinitionsJson
|
||||
}
|
||||
customTextures: {
|
||||
items?: CustomTexturesData
|
||||
blocks?: CustomTexturesData
|
||||
armor?: CustomTexturesData
|
||||
} = {}
|
||||
itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceData.itemDefinitionsJson)
|
||||
workersProcessAverageTime = 0
|
||||
workersProcessAverageTimeCount = 0
|
||||
maxWorkersProcessTime = 0
|
||||
geometryReceiveCount = {}
|
||||
allLoadedIn: undefined | number
|
||||
rendererDevice = '...'
|
||||
|
||||
edgeChunks = {} as Record<string, boolean>
|
||||
lastAddChunk = null as null | {
|
||||
|
|
@ -150,14 +122,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
lastChunkDistance = 0
|
||||
debugStopGeometryUpdate = false
|
||||
|
||||
@worldCleanup()
|
||||
freeFlyMode = false
|
||||
@worldCleanup()
|
||||
freeFlyState = {
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
position: new Vec3(0, 0, 0)
|
||||
}
|
||||
@worldCleanup()
|
||||
itemsRenderer: ItemsRenderer | undefined
|
||||
|
||||
|
|
@ -168,22 +132,77 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
abstract outputFormat: 'threeJs' | 'webgpu'
|
||||
worldBlockProvider: WorldBlockProvider
|
||||
soundSystem: SoundSystem | undefined
|
||||
|
||||
abstract changeBackgroundColor (color: [number, number, number]): void
|
||||
|
||||
constructor (public config: WorldRendererConfig) {
|
||||
worldRendererConfig: WorldRendererConfig
|
||||
playerState: IPlayerState
|
||||
reactiveState: RendererReactiveState
|
||||
|
||||
abortController = new AbortController()
|
||||
|
||||
constructor (public readonly resourcesManager: ResourcesManager, public displayOptions: DisplayWorldOptions, public version: string) {
|
||||
// this.initWorkers(1) // preload script on page load
|
||||
this.snapshotInitialValues()
|
||||
this.worldRendererConfig = displayOptions.inWorldRenderingConfig
|
||||
this.playerState = displayOptions.playerState
|
||||
this.reactiveState = displayOptions.rendererState
|
||||
|
||||
this.renderUpdateEmitter.on('update', () => {
|
||||
const loadedChunks = Object.keys(this.finishedChunks).length
|
||||
updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`)
|
||||
})
|
||||
|
||||
this.connect(this.displayOptions.worldView)
|
||||
}
|
||||
|
||||
init () {
|
||||
if (this.active) throw new Error('WorldRendererCommon is already initialized')
|
||||
void this.setVersion(this.version).then(() => {
|
||||
this.resourcesManager.on('assetsTexturesUpdated', () => {
|
||||
if (!this.active) return
|
||||
void this.updateAssetsData()
|
||||
})
|
||||
if (this.resourcesManager.currentResources) {
|
||||
void this.updateAssetsData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
snapshotInitialValues () { }
|
||||
|
||||
initWorkers (numWorkers = this.config.numWorkers) {
|
||||
wasChunkSentToWorker (chunkKey: string) {
|
||||
return this.loadedChunks[chunkKey]
|
||||
}
|
||||
|
||||
async getHighestBlocks () {
|
||||
return this.highestBlocks
|
||||
}
|
||||
|
||||
updateCustomBlock (chunkKey: string, blockPos: string, model: string) {
|
||||
this.protocolCustomBlocks.set(chunkKey, {
|
||||
...this.protocolCustomBlocks.get(chunkKey),
|
||||
[blockPos]: model
|
||||
})
|
||||
if (this.wasChunkSentToWorker(chunkKey)) {
|
||||
const [x, y, z] = blockPos.split(',').map(Number)
|
||||
this.setBlockStateId(new Vec3(x, y, z), undefined)
|
||||
}
|
||||
}
|
||||
|
||||
async getBlockInfo (blockPos: { x: number, y: number, z: number }, stateId: number) {
|
||||
const chunkKey = `${Math.floor(blockPos.x / 16) * 16},${Math.floor(blockPos.z / 16) * 16}`
|
||||
const customBlockName = this.protocolCustomBlocks.get(chunkKey)?.[`${blockPos.x},${blockPos.y},${blockPos.z}`]
|
||||
const cacheKey = getBlockAssetsCacheKey(stateId, customBlockName)
|
||||
const modelInfo = this.blockStateModelInfo.get(cacheKey)
|
||||
return {
|
||||
customBlockName,
|
||||
modelInfo
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -273,13 +292,16 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
checkAllFinished () {
|
||||
if (this.sectionsWaiting.size === 0) {
|
||||
const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
if (allFinished) {
|
||||
this.allChunksLoaded?.()
|
||||
this.allChunksFinished = true
|
||||
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
|
||||
}
|
||||
this.reactiveState.world.mesherWork = false
|
||||
}
|
||||
// todo check exact surrounding chunks
|
||||
const allFinished = Object.keys(this.finishedChunks).length >= this.chunksLength
|
||||
if (allFinished) {
|
||||
this.allChunksLoaded?.()
|
||||
this.allChunksFinished = true
|
||||
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
|
||||
}
|
||||
this.updateChunksStats()
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
||||
|
|
@ -328,20 +350,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
worker.terminate()
|
||||
}
|
||||
this.workers = []
|
||||
this.currentTextureImage = undefined
|
||||
this.blocksAtlasParser = undefined
|
||||
this.itemsAtlasParser = undefined
|
||||
}
|
||||
|
||||
// new game load happens here
|
||||
async setVersion (version, texturesVersion = version) {
|
||||
if (!this.blockstatesModels) throw new Error('Blockstates models is not loaded yet')
|
||||
async setVersion (version: string) {
|
||||
this.version = version
|
||||
this.texturesVersion = texturesVersion
|
||||
this.resetWorld()
|
||||
|
||||
// for workers in single file build
|
||||
if (document.readyState === 'loading') {
|
||||
if (document?.readyState === 'loading') {
|
||||
await new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', resolve)
|
||||
})
|
||||
|
|
@ -349,11 +366,26 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
this.initWorkers()
|
||||
this.active = true
|
||||
this.mesherConfig.outputFormat = this.outputFormat
|
||||
this.mesherConfig.version = this.version!
|
||||
|
||||
await this.resourcesManager.loadMcData(version)
|
||||
this.sendMesherMcData()
|
||||
await this.updateAssetsData()
|
||||
if (!this.resourcesManager.currentResources) {
|
||||
await this.resourcesManager.updateAssetsData({ })
|
||||
}
|
||||
}
|
||||
|
||||
getMesherConfig (): MesherConfig {
|
||||
return {
|
||||
version: this.version,
|
||||
enableLighting: this.worldRendererConfig.enableLighting,
|
||||
skyLight: 15,
|
||||
smoothLighting: this.worldRendererConfig.smoothLighting,
|
||||
outputFormat: this.outputFormat,
|
||||
textureSize: this.resourcesManager.currentResources!.blocksAtlasParser.atlas.latest.width,
|
||||
debugModelVariant: undefined,
|
||||
clipWorldBelowY: this.worldRendererConfig.clipWorldBelowY,
|
||||
disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers
|
||||
}
|
||||
}
|
||||
|
||||
sendMesherMcData () {
|
||||
|
|
@ -366,111 +398,40 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'mcData', mcData, config: this.mesherConfig })
|
||||
worker.postMessage({ type: 'mcData', mcData, config: this.getMesherConfig() })
|
||||
}
|
||||
}
|
||||
|
||||
async generateGuiTextures () {
|
||||
await generateGuiAtlas()
|
||||
}
|
||||
|
||||
async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
|
||||
const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
|
||||
const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
|
||||
|
||||
const blockTexturesChanges = {} as Record<string, string>
|
||||
const date = new Date()
|
||||
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) {
|
||||
Object.assign(blockTexturesChanges, christmasPack)
|
||||
}
|
||||
|
||||
const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {})
|
||||
const customItemTextures = Object.keys(this.customTextures.items?.textures ?? {})
|
||||
console.time('createBlocksAtlas')
|
||||
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
|
||||
const texture = this.customTextures?.blocks?.textures[textureName]
|
||||
return blockTexturesChanges[textureName] ?? texture
|
||||
}, /* this.customTextures?.blocks?.tileSize */undefined, prioritizeBlockTextures, customBlockTextures)
|
||||
console.timeEnd('createBlocksAtlas')
|
||||
console.time('createItemsAtlas')
|
||||
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
|
||||
const texture = this.customTextures?.items?.textures[textureName]
|
||||
if (!texture) return
|
||||
return texture
|
||||
}, this.customTextures?.items?.tileSize, undefined, customItemTextures)
|
||||
console.timeEnd('createItemsAtlas')
|
||||
|
||||
this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
|
||||
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
|
||||
|
||||
this.itemsRenderer = new ItemsRenderer(this.version!, this.blockstatesModels, this.itemsAtlasParser, this.blocksAtlasParser)
|
||||
this.worldBlockProvider = worldBlockProvider(this.blockstatesModels, this.blocksAtlasParser.atlas, 'latest')
|
||||
|
||||
const texture = await new THREE.TextureLoader().loadAsync(this.blocksAtlasParser.latestImage)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
this.material.map = texture
|
||||
this.currentTextureImage = this.material.map.image
|
||||
this.mesherConfig.textureSize = this.material.map.image.width
|
||||
async updateAssetsData () {
|
||||
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()) {
|
||||
const { blockstatesModels } = this
|
||||
if (this.customBlockStates) {
|
||||
// TODO! remove from other versions as well
|
||||
blockstatesModels.blockstates.latest = {
|
||||
...blockstatesModels.blockstates.latest,
|
||||
...this.customBlockStates
|
||||
}
|
||||
}
|
||||
if (this.customModels) {
|
||||
blockstatesModels.models.latest = {
|
||||
...blockstatesModels.models.latest,
|
||||
...this.customModels
|
||||
}
|
||||
}
|
||||
const { blockstatesModels } = resources
|
||||
|
||||
worker.postMessage({
|
||||
type: 'mesherData',
|
||||
workerIndex: i,
|
||||
blocksAtlas: {
|
||||
latest: blocksAtlas
|
||||
latest: resources.blocksAtlasParser.atlas.latest
|
||||
},
|
||||
blockstatesModels,
|
||||
config: this.mesherConfig,
|
||||
config: this.getMesherConfig(),
|
||||
})
|
||||
}
|
||||
if (!this.itemsAtlasParser) return
|
||||
const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage)
|
||||
itemsTexture.magFilter = THREE.NearestFilter
|
||||
itemsTexture.minFilter = THREE.NearestFilter
|
||||
itemsTexture.flipY = false
|
||||
viewer.entities.itemsTexture = itemsTexture
|
||||
if (!this.itemsAtlasParser) return
|
||||
|
||||
this.renderUpdateEmitter.emit('textureDownloaded')
|
||||
|
||||
console.time('generateGuiTextures')
|
||||
await this.generateGuiTextures()
|
||||
console.timeEnd('generateGuiTextures')
|
||||
if (!this.itemsAtlasParser) return
|
||||
this.renderUpdateEmitter.emit('itemsTextureDownloaded')
|
||||
console.log('textures loaded')
|
||||
}
|
||||
|
||||
async downloadDebugAtlas (isItems = false) {
|
||||
const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)!
|
||||
const dataUrl = await atlasParser.createDebugImage(true)
|
||||
const a = document.createElement('a')
|
||||
a.href = dataUrl
|
||||
a.download = `atlas-debug-${isItems ? 'items' : 'blocks'}.png`
|
||||
a.click()
|
||||
}
|
||||
|
||||
get worldMinYRender () {
|
||||
return Math.floor(Math.max(this.worldConfig.minY, this.mesherConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
|
||||
return Math.floor(Math.max(this.worldSizeParams.minY, this.worldRendererConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
|
||||
}
|
||||
|
||||
updateChunksStatsText () {
|
||||
updateChunksStats () {
|
||||
const loadedChunks = Object.keys(this.finishedChunks)
|
||||
this.reactiveState.world.chunksLoaded = loadedChunks
|
||||
this.reactiveState.world.chunksTotalNumber = this.chunksLength
|
||||
this.reactiveState.world.allChunksLoaded = this.allChunksFinished
|
||||
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`)
|
||||
}
|
||||
|
||||
|
|
@ -480,7 +441,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.initialChunksLoad = false
|
||||
this.initialChunkLoadWasStartedIn ??= Date.now()
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
this.updateChunksStatsText()
|
||||
this.updateChunksStats()
|
||||
|
||||
const chunkKey = `${x},${z}`
|
||||
const customBlockModels = this.protocolCustomBlocks.get(chunkKey)
|
||||
|
|
@ -494,10 +455,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
customBlockModels: customBlockModels || undefined
|
||||
})
|
||||
}
|
||||
for (let y = this.worldMinYRender; y < this.worldConfig.worldHeight; y += 16) {
|
||||
for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||
const loc = new Vec3(x, y, z)
|
||||
this.setSectionDirty(loc)
|
||||
if (this.neighborChunkUpdates && (!isLightUpdate || this.mesherConfig.smoothLighting)) {
|
||||
if (this.neighborChunkUpdates && (!isLightUpdate || this.worldRendererConfig.smoothLighting)) {
|
||||
this.setSectionDirty(loc.offset(-16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(0, 0, -16))
|
||||
|
|
@ -513,6 +474,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
this.updateChunksStats()
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'unloadChunk', x, z })
|
||||
|
|
@ -523,7 +485,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.allLoadedIn = undefined
|
||||
this.initialChunkLoadWasStartedIn = undefined
|
||||
}
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
delete this.finishedSections[`${x},${y},${z}`]
|
||||
}
|
||||
|
|
@ -540,7 +502,130 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
setBlockStateId (pos: Vec3, stateId: number | undefined) {
|
||||
const set = async () => {
|
||||
const sectionX = Math.floor(pos.x / 16) * 16
|
||||
const sectionZ = Math.floor(pos.z / 16) * 16
|
||||
if (this.queuedChunks.has(`${sectionX},${sectionZ}`)) {
|
||||
await new Promise<void>(resolve => {
|
||||
this.queuedFunctions.push(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
if (!this.loadedChunks[`${sectionX},${sectionZ}`]) {
|
||||
// console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
}
|
||||
this.setBlockStateIdInner(pos, stateId)
|
||||
}
|
||||
void set()
|
||||
}
|
||||
|
||||
updateEntity (e: any, isUpdate = false) { }
|
||||
|
||||
lightUpdate (chunkX: number, chunkZ: number) { }
|
||||
|
||||
connect (worldView: WorldDataEmitter) {
|
||||
const worldEmitter = worldView
|
||||
|
||||
worldEmitter.on('entity', (e) => {
|
||||
this.updateEntity(e, false)
|
||||
})
|
||||
worldEmitter.on('entityMoved', (e) => {
|
||||
this.updateEntity(e, true)
|
||||
})
|
||||
|
||||
let currentLoadChunkBatch = null as {
|
||||
timeout
|
||||
data
|
||||
} | null
|
||||
worldEmitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
|
||||
this.worldSizeParams = worldConfig
|
||||
this.queuedChunks.add(`${x},${z}`)
|
||||
const args = [x, z, chunk, isLightUpdate]
|
||||
if (!currentLoadChunkBatch) {
|
||||
// add a setting to use debounce instead
|
||||
currentLoadChunkBatch = {
|
||||
data: [],
|
||||
timeout: setTimeout(() => {
|
||||
for (const args of currentLoadChunkBatch!.data) {
|
||||
this.queuedChunks.delete(`${args[0]},${args[1]}`)
|
||||
this.addColumn(...args as Parameters<typeof this.addColumn>)
|
||||
}
|
||||
for (const fn of this.queuedFunctions) {
|
||||
fn()
|
||||
}
|
||||
this.queuedFunctions = []
|
||||
currentLoadChunkBatch = null
|
||||
}, this.worldRendererConfig.addChunksBatchWaitTime)
|
||||
}
|
||||
}
|
||||
currentLoadChunkBatch.data.push(args)
|
||||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
worldEmitter.on('blockEntities', (blockEntities) => {
|
||||
this.blockEntities = blockEntities
|
||||
})
|
||||
|
||||
worldEmitter.on('unloadChunk', ({ x, z }) => {
|
||||
this.removeColumn(x, z)
|
||||
})
|
||||
|
||||
worldEmitter.on('blockUpdate', ({ pos, stateId }) => {
|
||||
this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
|
||||
})
|
||||
|
||||
worldEmitter.on('chunkPosUpdate', ({ pos }) => {
|
||||
this.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
|
||||
worldEmitter.on('renderDistance', (d) => {
|
||||
this.viewDistance = d
|
||||
this.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
})
|
||||
|
||||
worldEmitter.on('renderDistance', (d) => {
|
||||
this.viewDistance = d
|
||||
this.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
})
|
||||
|
||||
worldEmitter.on('markAsLoaded', ({ x, z }) => {
|
||||
this.markAsLoaded(x, z)
|
||||
})
|
||||
|
||||
worldEmitter.on('updateLight', ({ pos }) => {
|
||||
this.lightUpdate(pos.x, pos.z)
|
||||
})
|
||||
|
||||
worldEmitter.on('time', (timeOfDay) => {
|
||||
this.timeUpdated?.(timeOfDay)
|
||||
|
||||
let skyLight = 15
|
||||
if (timeOfDay < 0 || timeOfDay > 24_000) {
|
||||
throw new Error('Invalid time of day. It should be between 0 and 24000.')
|
||||
} else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
|
||||
skyLight = 15
|
||||
} else if (timeOfDay > 6000 && timeOfDay < 12_000) {
|
||||
skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
|
||||
} else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
|
||||
skyLight = ((timeOfDay - 12_000) / 6000) * 15
|
||||
}
|
||||
|
||||
skyLight = Math.floor(skyLight) // todo: remove this after optimization
|
||||
|
||||
// if (this.worldRendererConfig.skyLight === skyLight) return
|
||||
// this.worldRendererConfig.skyLight = skyLight
|
||||
// if (this instanceof WorldRendererThree) {
|
||||
// (this).rerenderAllChunks?.()
|
||||
// }
|
||||
})
|
||||
|
||||
worldEmitter.emit('listening')
|
||||
}
|
||||
|
||||
setBlockStateIdInner (pos: Vec3, stateId: number | undefined) {
|
||||
const needAoRecalculation = true
|
||||
const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
|
||||
|
|
@ -604,7 +689,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
|
||||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||
this.allChunksFinished = false
|
||||
this.reactiveState.world.mesherWork = true
|
||||
const distance = this.getDistance(pos)
|
||||
if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
|
||||
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
|
|
@ -623,7 +708,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
y: pos.y,
|
||||
z: pos.z,
|
||||
value,
|
||||
config: this.mesherConfig,
|
||||
config: this.getMesherConfig(),
|
||||
})
|
||||
this.dispatchMessages()
|
||||
}
|
||||
|
|
@ -679,8 +764,24 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
|
||||
destroy () {
|
||||
console.warn('world destroy is not implemented')
|
||||
}
|
||||
// Stop all workers
|
||||
for (const worker of this.workers) {
|
||||
worker.terminate()
|
||||
}
|
||||
this.workers = []
|
||||
|
||||
abstract setHighlightCursorBlock (block: typeof this.cursorBlock, shapePositions?: Array<{ position; width; height; depth }>): void
|
||||
// Stop and destroy sound system
|
||||
if (this.soundSystem) {
|
||||
this.soundSystem.destroy()
|
||||
this.soundSystem = undefined
|
||||
}
|
||||
|
||||
this.active = false
|
||||
|
||||
this.renderUpdateEmitter.removeAllListeners()
|
||||
this.displayOptions.worldView.removeAllListeners() // todo
|
||||
this.abortController.abort()
|
||||
removeStat('chunks-loaded')
|
||||
removeStat('chunks-read')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,25 @@ import { Vec3 } from 'vec3'
|
|||
import nbt from 'prismarine-nbt'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass, LineSegmentsGeometry, Wireframe, LineMaterial } from 'three-stdlib'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { renderSign } from '../sign-renderer'
|
||||
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||
import { initVR } from '../three/world/vr'
|
||||
import { getItemUv } from '../three/appShared'
|
||||
import { CursorBlock } from '../three/world/cursorBlock'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
|
||||
import { WorldRendererCommon } from './worldrendererCommon'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import HoldingBlock, { HandItemBlock } from './holdingBlock'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import HoldingBlock from './holdingBlock'
|
||||
import { addNewStat, removeAllStats } from './ui/newStats'
|
||||
import { MesherGeometryOutput } from './mesher/shared'
|
||||
import { IPlayerState } from './basePlayerState'
|
||||
import { ItemSpecificContextProperties } from './basePlayerState'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { armorModel } from './entity/armorModels'
|
||||
import { getMyHand } from './hand'
|
||||
import { setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { Entities } from './entities'
|
||||
import { ThreeJsSound } from './threeJsSound'
|
||||
|
||||
interface MediaProperties {
|
||||
position: { x: number, y: number, z: number }
|
||||
|
|
@ -26,12 +33,12 @@ interface MediaProperties {
|
|||
opacity?: number // 0-1 value for transparency
|
||||
uvMapping?: { startU: number, endU: number, startV: number, endV: number }
|
||||
allowOrigins?: string[] | boolean
|
||||
loop?: boolean
|
||||
volume?: number
|
||||
}
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
interactionLines: null | { blockPos; mesh } = null
|
||||
outputFormat = 'threeJs' as const
|
||||
blockEntities = {}
|
||||
sectionObjects: Record<string, THREE.Object3D> = {}
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
signsCache = new Map<string, any>()
|
||||
|
|
@ -39,7 +46,16 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
||||
holdingBlock: HoldingBlock
|
||||
holdingBlockLeft: HoldingBlock
|
||||
rendererDevice = '...'
|
||||
cameraRoll = 0
|
||||
scene = new THREE.Scene()
|
||||
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
||||
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
||||
entities = new Entities(this)
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||
itemsTexture: THREE.Texture
|
||||
cursorBlock = new CursorBlock(this)
|
||||
onRender: Array<() => void> = []
|
||||
customMedia = new Map<string, {
|
||||
mesh: THREE.Object3D
|
||||
video: HTMLVideoElement | undefined
|
||||
|
|
@ -55,21 +71,72 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
|
||||
}
|
||||
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig, public playerState: IPlayerState) {
|
||||
super(config)
|
||||
this.rendererDevice = `${WorldRendererThree.getRendererInfo(this.renderer)} powered by three.js r${THREE.REVISION}`
|
||||
this.starField = new StarField(scene)
|
||||
this.holdingBlock = new HoldingBlock(playerState, this.config)
|
||||
this.holdingBlockLeft = new HoldingBlock(playerState, this.config, true)
|
||||
constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
|
||||
if (!initOptions.resourcesManager) throw new Error('resourcesManager is required')
|
||||
super(initOptions.resourcesManager, displayOptions, displayOptions.version)
|
||||
|
||||
this.renderUpdateEmitter.on('itemsTextureDownloaded', () => {
|
||||
this.holdingBlock.ready = true
|
||||
this.holdingBlock.updateItem()
|
||||
this.holdingBlockLeft.ready = true
|
||||
this.holdingBlockLeft.updateItem()
|
||||
})
|
||||
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
|
||||
this.starField = new StarField(this.scene)
|
||||
this.holdingBlock = new HoldingBlock(this)
|
||||
this.holdingBlockLeft = new HoldingBlock(this, true)
|
||||
|
||||
this.soundSystem = new ThreeJsSound(this)
|
||||
|
||||
this.addDebugOverlay()
|
||||
this.resetScene()
|
||||
this.watchReactivePlayerState()
|
||||
this.init()
|
||||
void initVR(this)
|
||||
}
|
||||
|
||||
updateEntity (e, isPosUpdate = false) {
|
||||
const overrides = {
|
||||
rotation: {
|
||||
head: {
|
||||
x: e.headPitch ?? e.pitch,
|
||||
y: e.headYaw,
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isPosUpdate) {
|
||||
this.entities.updateEntityPosition(e, false, overrides)
|
||||
} else {
|
||||
this.entities.update(e, overrides)
|
||||
}
|
||||
}
|
||||
|
||||
resetScene () {
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
||||
this.scene.add(this.ambientLight)
|
||||
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
||||
this.directionalLight.castShadow = true
|
||||
this.scene.add(this.directionalLight)
|
||||
|
||||
const size = this.renderer.getSize(new THREE.Vector2())
|
||||
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
||||
}
|
||||
|
||||
watchReactivePlayerState () {
|
||||
const updateValue = <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])
|
||||
subscribeKey(this.displayOptions.playerState.reactive, key, callback)
|
||||
}
|
||||
updateValue('backgroundColor', (value) => {
|
||||
this.changeBackgroundColor(value)
|
||||
})
|
||||
updateValue('inWater', (value) => {
|
||||
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, 100) : null
|
||||
})
|
||||
updateValue('ambientLight', (value) => {
|
||||
if (!value) return
|
||||
this.ambientLight.intensity = value
|
||||
})
|
||||
updateValue('directionalLight', (value) => {
|
||||
if (!value) return
|
||||
this.directionalLight.intensity = value
|
||||
})
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||
|
|
@ -81,6 +148,46 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
}
|
||||
|
||||
async updateAssetsData (): Promise<void> {
|
||||
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
|
||||
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
|
||||
itemsTexture.flipY = false
|
||||
this.itemsTexture = itemsTexture
|
||||
|
||||
if (oldTexture) {
|
||||
oldTexture.dispose()
|
||||
}
|
||||
if (oldItemsTexture) {
|
||||
oldItemsTexture.dispose()
|
||||
}
|
||||
|
||||
await super.updateAssetsData()
|
||||
this.onAllTexturesLoaded()
|
||||
if (Object.keys(this.loadedChunks).length > 0) {
|
||||
console.log('rerendering chunks because of texture update')
|
||||
this.rerenderAllChunks()
|
||||
}
|
||||
}
|
||||
|
||||
onAllTexturesLoaded () {
|
||||
this.holdingBlock.ready = true
|
||||
this.holdingBlock.updateItem()
|
||||
this.holdingBlockLeft.ready = true
|
||||
this.holdingBlockLeft.updateItem()
|
||||
}
|
||||
|
||||
changeBackgroundColor (color: [number, number, number]): void {
|
||||
this.scene.background = new THREE.Color(color[0], color[1], color[2])
|
||||
}
|
||||
|
|
@ -96,6 +203,35 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
}
|
||||
|
||||
getItemRenderData (item: Record<string, any>, specificProps: ItemSpecificContextProperties) {
|
||||
return getItemUv(item, specificProps, this.resourcesManager)
|
||||
}
|
||||
|
||||
async demoModel () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
|
||||
const mesh = await getMyHand()
|
||||
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
||||
setBlockPosition(mesh, pos)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
demoItem () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const { mesh } = this.entities.getItemMesh({
|
||||
itemId: 541,
|
||||
}, {})!
|
||||
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
// mesh.scale.set(0.5, 0.5, 0.5)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
debugOverlayAdded = false
|
||||
addDebugOverlay () {
|
||||
if (this.debugOverlayAdded) return
|
||||
|
|
@ -106,7 +242,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
if (this.displayStats) {
|
||||
pane.updateText(`C: ${this.renderer.info.render.calls} TR: ${this.renderer.info.render.triangles} TE: ${this.renderer.info.memory.textures} F: ${this.tilesRendered} B: ${this.blocksRendered}`)
|
||||
}
|
||||
}, 100)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,7 +320,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
object.name = 'chunk';
|
||||
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
|
||||
(object as any).blocksCount = data.geometry.blocksCount
|
||||
if (!this.config.showChunkBorders) {
|
||||
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
}
|
||||
// should not compute it once
|
||||
|
|
@ -229,7 +365,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version!)
|
||||
const PrismarineChat = PrismarineChatLoader(this.version)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
|
|
@ -240,28 +376,86 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
return tex
|
||||
}
|
||||
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
||||
if (this.freeFlyMode) {
|
||||
pos = this.freeFlyState.position
|
||||
pitch = this.freeFlyState.pitch
|
||||
yaw = this.freeFlyState.yaw
|
||||
tryIntersectMedia () {
|
||||
const { camera } = this
|
||||
const raycaster = new THREE.Raycaster()
|
||||
|
||||
// Get mouse position at center of screen
|
||||
const mouse = new THREE.Vector2(0, 0)
|
||||
|
||||
// Update the raycaster
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
|
||||
let result = null as { id: string, x: number, y: number } | null
|
||||
// Check intersection with all video meshes
|
||||
for (const [id, videoData] of this.customMedia.entries()) {
|
||||
// Get the actual mesh (first child of the group)
|
||||
const mesh = videoData.mesh.children[0] as THREE.Mesh
|
||||
if (!mesh) continue
|
||||
|
||||
const intersects = raycaster.intersectObject(mesh, false)
|
||||
if (intersects.length > 0) {
|
||||
const intersection = intersects[0]
|
||||
const { uv } = intersection
|
||||
if (uv) {
|
||||
result = {
|
||||
id,
|
||||
x: uv.x,
|
||||
y: uv.y
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.reactiveState.world.intersectMedia = result
|
||||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
const yOffset = this.displayOptions.playerState.getEyeHeight()
|
||||
|
||||
this.camera = cam as THREE.PerspectiveCamera
|
||||
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
this.tryIntersectMedia()
|
||||
}
|
||||
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
||||
// if (this.freeFlyMode) {
|
||||
// pos = this.freeFlyState.position
|
||||
// pitch = this.freeFlyState.pitch
|
||||
// yaw = this.freeFlyState.yaw
|
||||
// }
|
||||
|
||||
if (pos) {
|
||||
new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
this.freeFlyState.position = pos
|
||||
// this.freeFlyState.position = pos
|
||||
}
|
||||
this.camera.rotation.set(pitch, yaw, this.cameraRoll, 'ZYX')
|
||||
}
|
||||
|
||||
render () {
|
||||
tweenJs.update()
|
||||
render (sizeChanged = false) {
|
||||
this.cursorBlock.render()
|
||||
|
||||
const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
|
||||
if (sizeOrFovChanged) {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||
this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.entities.render()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||
this.renderer.render(this.scene, cam)
|
||||
if (this.config.showHand && !this.freeFlyMode) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
|
||||
if (this.displayOptions.inWorldRenderingConfig.showHand/* && !this.freeFlyMode */) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
||||
}
|
||||
|
||||
for (const onRender of this.onRender) {
|
||||
onRender()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -336,25 +530,13 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
return group
|
||||
}
|
||||
|
||||
updateLight (chunkX: number, chunkZ: number) {
|
||||
lightUpdate (chunkX: number, chunkZ: number) {
|
||||
// set all sections in the chunk dirty
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(chunkX, y, chunkZ))
|
||||
}
|
||||
}
|
||||
|
||||
async doHmr () {
|
||||
const oldSections = { ...this.sectionObjects }
|
||||
this.sectionObjects = {} // skip clearing
|
||||
worldView!.unloadAllChunks()
|
||||
void this.setVersion(this.version, this.texturesVersion)
|
||||
this.sectionObjects = oldSections
|
||||
// this.rerenderAllChunks()
|
||||
|
||||
// supply new data
|
||||
await worldView!.updatePosition(bot.entity.position, true)
|
||||
}
|
||||
|
||||
rerenderAllChunks () { // todo not clear what to do with loading chunks
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
|
|
@ -363,7 +545,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
this.config.showChunkBorders = value
|
||||
this.displayOptions.inWorldRenderingConfig.showChunkBorders = value
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
|
|
@ -421,7 +603,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
super.removeColumn(x, z)
|
||||
|
||||
this.cleanChunkTextures(x, z)
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
const key = `${x},${y},${z}`
|
||||
const mesh = this.sectionObjects[key]
|
||||
|
|
@ -439,34 +621,6 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
super.setSectionDirty(...args)
|
||||
}
|
||||
|
||||
setHighlightCursorBlock (blockPos: typeof this.cursorBlock, shapePositions?: Array<{ position: any; width: any; height: any; depth: any; }>): void {
|
||||
this.cursorBlock = blockPos
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
||||
return
|
||||
}
|
||||
if (this.interactionLines !== null) {
|
||||
this.scene.remove(this.interactionLines.mesh)
|
||||
this.interactionLines = null
|
||||
}
|
||||
if (blockPos === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
for (const { position, width, height, depth } of shapePositions ?? []) {
|
||||
const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const
|
||||
const geometry = new THREE.BoxGeometry(...scale)
|
||||
const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
|
||||
const wireframe = new Wireframe(lines, this.threejsCursorLineMaterial)
|
||||
const pos = blockPos.plus(position)
|
||||
wireframe.position.set(pos.x, pos.y, pos.z)
|
||||
wireframe.computeLineDistances()
|
||||
group.add(wireframe)
|
||||
}
|
||||
this.scene.add(group)
|
||||
this.interactionLines = { blockPos, mesh: group }
|
||||
}
|
||||
|
||||
static getRendererInfo (renderer: THREE.WebGLRenderer) {
|
||||
try {
|
||||
const gl = renderer.getContext()
|
||||
|
|
@ -549,8 +703,9 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
if (!isImage) {
|
||||
video = document.createElement('video')
|
||||
video.src = props.src
|
||||
video.loop = true
|
||||
video.muted = true
|
||||
video.loop = props.loop ?? true
|
||||
video.volume = props.volume ?? 1
|
||||
video.muted = !props.volume
|
||||
video.playsInline = true
|
||||
video.crossOrigin = 'anonymous'
|
||||
}
|
||||
|
|
@ -864,10 +1019,29 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
// Position the mesh exactly where we want it
|
||||
const { debugGroup } = this.positionMeshExact(plane, rotation, pos, width, height)
|
||||
|
||||
viewer.scene.add(debugGroup)
|
||||
this.scene.add(debugGroup)
|
||||
console.log('Exact test mesh added with dimensions:', width, height, 'and rotation:', rotation)
|
||||
|
||||
}
|
||||
|
||||
setCameraRoll (roll: number) {
|
||||
this.cameraRoll = roll
|
||||
const rollQuat = new THREE.Quaternion()
|
||||
rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(roll))
|
||||
|
||||
// Get camera's current rotation
|
||||
const camQuat = new THREE.Quaternion()
|
||||
this.camera.getWorldQuaternion(camQuat)
|
||||
|
||||
// Apply roll after camera rotation
|
||||
const finalQuat = camQuat.multiply(rollQuat)
|
||||
this.camera.setRotationFromQuaternion(finalQuat)
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
removeAllStats()
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
class StarField {
|
||||
|
|
|
|||
70
renderer/viewer/three/appShared.ts
Normal file
70
renderer/viewer/three/appShared.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { BlockModel } from 'mc-assets/dist/types'
|
||||
import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { renderSlot } from '../../../src/inventoryWindows'
|
||||
import { GeneralInputItem, getItemModelName } from '../../../src/mineflayer/items'
|
||||
import { ResourcesManager } from '../../../src/resourcesManager'
|
||||
|
||||
export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecificContextProperties, resourcesManager: ResourcesManager): {
|
||||
u: number
|
||||
v: number
|
||||
su: number
|
||||
sv: number
|
||||
renderInfo?: ReturnType<typeof renderSlot>
|
||||
texture: HTMLImageElement
|
||||
modelName: string
|
||||
} | {
|
||||
resolvedModel: BlockModel
|
||||
modelName: string
|
||||
} => {
|
||||
const resources = resourcesManager.currentResources
|
||||
if (!resources) throw new Error('Resources not loaded')
|
||||
const idOrName = item.itemId ?? item.blockId ?? item.name
|
||||
try {
|
||||
const name = typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
|
||||
if (!name) throw new Error(`Item not found: ${idOrName}`)
|
||||
|
||||
const model = getItemModelName({
|
||||
...item,
|
||||
name,
|
||||
} as GeneralInputItem, specificProps)
|
||||
|
||||
const renderInfo = renderSlot({
|
||||
modelName: model,
|
||||
}, false, true)
|
||||
|
||||
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
||||
|
||||
const img = renderInfo.texture === 'blocks' ? resources.blocksAtlasImage : resources.itemsAtlasImage
|
||||
|
||||
if (renderInfo.blockData) {
|
||||
return {
|
||||
resolvedModel: renderInfo.blockData.resolvedModel,
|
||||
modelName: renderInfo.modelName!
|
||||
}
|
||||
}
|
||||
if (renderInfo.slice) {
|
||||
// Get slice coordinates from either block or item texture
|
||||
const [x, y, w, h] = renderInfo.slice
|
||||
const [u, v, su, sv] = [x / img.width, y / img.height, (w / img.width), (h / img.height)]
|
||||
return {
|
||||
u, v, su, sv,
|
||||
renderInfo,
|
||||
texture: img,
|
||||
modelName: renderInfo.modelName!
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid render info for item ${name}`)
|
||||
} catch (err) {
|
||||
reportError?.(err)
|
||||
// Return default UV coordinates for missing texture
|
||||
return {
|
||||
u: 0,
|
||||
v: 0,
|
||||
su: 16 / resources.blocksAtlasImage.width,
|
||||
sv: 16 / resources.blocksAtlasImage.width,
|
||||
texture: resources.blocksAtlasImage,
|
||||
modelName: 'missing'
|
||||
}
|
||||
}
|
||||
}
|
||||
236
renderer/viewer/three/documentRenderer.ts
Normal file
236
renderer/viewer/three/documentRenderer.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import * as THREE from 'three'
|
||||
import Stats from 'stats.js'
|
||||
import StatsGl from 'stats-gl'
|
||||
import * as tween from '@tweenjs/tween.js'
|
||||
import { GraphicsBackendConfig, GraphicsInitOptions } from '../../../src/appViewer'
|
||||
|
||||
export class DocumentRenderer {
|
||||
readonly canvas = document.createElement('canvas')
|
||||
readonly renderer: THREE.WebGLRenderer
|
||||
private animationFrameId?: number
|
||||
private lastRenderTime = 0
|
||||
private previousWindowWidth = window.innerWidth
|
||||
private previousWindowHeight = window.innerHeight
|
||||
private renderedFps = 0
|
||||
private fpsInterval: any
|
||||
private readonly stats: TopRightStats
|
||||
private paused = false
|
||||
disconnected = false
|
||||
preRender = () => { }
|
||||
render = (sizeChanged: boolean) => { }
|
||||
postRender = () => { }
|
||||
sizeChanged = () => { }
|
||||
droppedFpsPercentage: number
|
||||
config: GraphicsBackendConfig
|
||||
|
||||
constructor (initOptions: GraphicsInitOptions) {
|
||||
this.config = initOptions.config
|
||||
|
||||
try {
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: this.canvas,
|
||||
preserveDrawingBuffer: true,
|
||||
logarithmicDepthBuffer: true,
|
||||
powerPreference: this.config.powerPreference
|
||||
})
|
||||
} catch (err) {
|
||||
initOptions.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()
|
||||
|
||||
this.stats = new TopRightStats(this.canvas, this.config.statsVisible)
|
||||
|
||||
this.setupFpsTracking()
|
||||
this.startRenderLoop()
|
||||
}
|
||||
|
||||
updatePixelRatio () {
|
||||
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
|
||||
if (!this.renderer.capabilities.isWebGL2) {
|
||||
pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
|
||||
}
|
||||
this.renderer.setPixelRatio(pixelRatio)
|
||||
}
|
||||
|
||||
updateSize () {
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
}
|
||||
|
||||
private addToPage () {
|
||||
this.canvas.id = 'viewer-canvas'
|
||||
this.canvas.style.width = '100%'
|
||||
this.canvas.style.height = '100%'
|
||||
document.body.appendChild(this.canvas)
|
||||
}
|
||||
|
||||
private setupFpsTracking () {
|
||||
let max = 0
|
||||
this.fpsInterval = setInterval(() => {
|
||||
if (max > 0) {
|
||||
this.droppedFpsPercentage = this.renderedFps / max
|
||||
}
|
||||
max = Math.max(this.renderedFps, max)
|
||||
this.renderedFps = 0
|
||||
}, 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.paused) return
|
||||
|
||||
// Handle FPS limiting
|
||||
if (this.config.fpsLimit) {
|
||||
const now = performance.now()
|
||||
const elapsed = now - this.lastRenderTime
|
||||
const fpsInterval = 1000 / this.config.fpsLimit
|
||||
|
||||
if (elapsed < fpsInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastRenderTime = now - (elapsed % fpsInterval)
|
||||
}
|
||||
|
||||
let sizeChanged = false
|
||||
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) {
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
this.updateSize()
|
||||
sizeChanged = true
|
||||
}
|
||||
|
||||
this.preRender()
|
||||
this.stats.markStart()
|
||||
tween.update()
|
||||
this.render(sizeChanged)
|
||||
this.renderedFps++
|
||||
this.stats.markEnd()
|
||||
this.postRender()
|
||||
|
||||
// Update stats visibility each frame
|
||||
if (this.config.statsVisible !== undefined) {
|
||||
this.stats.setVisibility(this.config.statsVisible)
|
||||
}
|
||||
}
|
||||
|
||||
animate()
|
||||
}
|
||||
|
||||
setPaused (paused: boolean) {
|
||||
this.paused = paused
|
||||
}
|
||||
|
||||
dispose () {
|
||||
this.disconnected = true
|
||||
if (this.animationFrameId) {
|
||||
cancelAnimationFrame(this.animationFrameId)
|
||||
}
|
||||
this.canvas.remove()
|
||||
this.renderer.dispose()
|
||||
clearInterval(this.fpsInterval)
|
||||
this.stats.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
class TopRightStats {
|
||||
private readonly stats: Stats
|
||||
private readonly stats2: Stats
|
||||
private readonly statsGl: StatsGl
|
||||
private total = 0
|
||||
private readonly denseMode: boolean
|
||||
|
||||
constructor (private readonly canvas: HTMLCanvasElement, initialStatsVisible = 0) {
|
||||
this.stats = new Stats()
|
||||
this.stats2 = new Stats()
|
||||
this.statsGl = new StatsGl({ minimal: true })
|
||||
this.stats2.showPanel(2)
|
||||
this.denseMode = process.env.NODE_ENV === 'production' || window.innerHeight < 500
|
||||
|
||||
this.initStats()
|
||||
this.setVisibility(initialStatsVisible)
|
||||
}
|
||||
|
||||
private addStat (dom: HTMLElement, size = 80) {
|
||||
dom.style.position = 'absolute'
|
||||
if (this.denseMode) dom.style.height = '12px'
|
||||
dom.style.overflow = 'hidden'
|
||||
dom.style.left = ''
|
||||
dom.style.top = '0'
|
||||
dom.style.right = `${this.total}px`
|
||||
dom.style.width = '80px'
|
||||
dom.style.zIndex = '1'
|
||||
dom.style.opacity = '0.8'
|
||||
document.body.appendChild(dom)
|
||||
this.total += size
|
||||
}
|
||||
|
||||
private initStats () {
|
||||
const hasRamPanel = this.stats2.dom.children.length === 3
|
||||
|
||||
this.addStat(this.stats.dom)
|
||||
if (hasRamPanel) {
|
||||
this.addStat(this.stats2.dom)
|
||||
}
|
||||
|
||||
this.statsGl.init(this.canvas)
|
||||
this.statsGl.container.style.display = 'flex'
|
||||
this.statsGl.container.style.justifyContent = 'flex-end'
|
||||
|
||||
let i = 0
|
||||
for (const _child of this.statsGl.container.children) {
|
||||
const child = _child as HTMLElement
|
||||
if (i++ === 0) {
|
||||
child.style.display = 'none'
|
||||
}
|
||||
child.style.position = ''
|
||||
}
|
||||
}
|
||||
|
||||
setVisibility (level: number) {
|
||||
const visible = level > 0
|
||||
if (visible) {
|
||||
this.stats.dom.style.display = 'block'
|
||||
this.stats2.dom.style.display = level >= 2 ? 'block' : 'none'
|
||||
this.statsGl.container.style.display = level >= 2 ? 'block' : 'none'
|
||||
} else {
|
||||
this.stats.dom.style.display = 'none'
|
||||
this.stats2.dom.style.display = 'none'
|
||||
this.statsGl.container.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
markStart () {
|
||||
this.stats.begin()
|
||||
this.stats2.begin()
|
||||
this.statsGl.begin()
|
||||
}
|
||||
|
||||
markEnd () {
|
||||
this.stats.end()
|
||||
this.stats2.end()
|
||||
this.statsGl.end()
|
||||
}
|
||||
|
||||
dispose () {
|
||||
this.stats.dom.remove()
|
||||
this.stats2.dom.remove()
|
||||
this.statsGl.container.remove()
|
||||
}
|
||||
}
|
||||
119
renderer/viewer/three/graphicsBackend.ts
Normal file
119
renderer/viewer/three/graphicsBackend.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { proxy } from 'valtio'
|
||||
import { GraphicsBackendLoader, GraphicsBackend, GraphicsInitOptions, DisplayWorldOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||
import { ProgressReporter } from '../../../src/core/progressReporter'
|
||||
import { WorldRendererThree } from '../lib/worldrendererThree'
|
||||
import { DocumentRenderer } from './documentRenderer'
|
||||
import { PanoramaRenderer } from './panorama'
|
||||
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
window.THREE = THREE
|
||||
|
||||
const getBackendMethods = (worldRenderer: WorldRendererThree) => {
|
||||
return {
|
||||
updateMap: worldRenderer.entities.updateMap.bind(worldRenderer.entities),
|
||||
updateCustomBlock: worldRenderer.updateCustomBlock.bind(worldRenderer),
|
||||
getBlockInfo: worldRenderer.getBlockInfo.bind(worldRenderer),
|
||||
playEntityAnimation: worldRenderer.entities.playAnimation.bind(worldRenderer.entities),
|
||||
damageEntity: worldRenderer.entities.handleDamageEvent.bind(worldRenderer.entities),
|
||||
updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities),
|
||||
setHighlightCursorBlock: worldRenderer.cursorBlock.setHighlightCursorBlock.bind(worldRenderer.cursorBlock),
|
||||
updateBreakAnimation: worldRenderer.cursorBlock.updateBreakAnimation.bind(worldRenderer.cursorBlock),
|
||||
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
|
||||
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
|
||||
rerenderAllChunks: worldRenderer.rerenderAllChunks.bind(worldRenderer),
|
||||
addMedia: worldRenderer.addMedia.bind(worldRenderer),
|
||||
destroyMedia: worldRenderer.destroyMedia.bind(worldRenderer),
|
||||
setVideoPlaying: worldRenderer.setVideoPlaying.bind(worldRenderer),
|
||||
setVideoSeeking: worldRenderer.setVideoSeeking.bind(worldRenderer),
|
||||
setVideoVolume: worldRenderer.setVideoVolume.bind(worldRenderer),
|
||||
setVideoSpeed: worldRenderer.setVideoSpeed.bind(worldRenderer),
|
||||
}
|
||||
}
|
||||
|
||||
export type ThreeJsBackendMethods = ReturnType<typeof getBackendMethods>
|
||||
|
||||
const createGraphicsBackend: GraphicsBackendLoader = (initOptions: GraphicsInitOptions) => {
|
||||
// Private state
|
||||
const documentRenderer = new DocumentRenderer(initOptions)
|
||||
globalThis.renderer = documentRenderer.renderer
|
||||
|
||||
let panoramaRenderer: PanoramaRenderer | null = null
|
||||
let worldRenderer: WorldRendererThree | null = null
|
||||
|
||||
const startPanorama = () => {
|
||||
if (worldRenderer) return
|
||||
if (!panoramaRenderer) {
|
||||
panoramaRenderer = new PanoramaRenderer(documentRenderer, initOptions, !!process.env.SINGLE_FILE_BUILD_MODE)
|
||||
void panoramaRenderer.start()
|
||||
window.panoramaRenderer = panoramaRenderer
|
||||
}
|
||||
}
|
||||
|
||||
let version = ''
|
||||
const prepareResources = async (ver: string, progressReporter: ProgressReporter): Promise<void> => {
|
||||
version = ver
|
||||
await initOptions.resourcesManager.updateAssetsData({ })
|
||||
}
|
||||
|
||||
const startWorld = (displayOptions: DisplayWorldOptions) => {
|
||||
if (panoramaRenderer) {
|
||||
panoramaRenderer.dispose()
|
||||
panoramaRenderer = null
|
||||
}
|
||||
worldRenderer = new WorldRendererThree(documentRenderer.renderer, initOptions, displayOptions)
|
||||
documentRenderer.render = (sizeChanged: boolean) => {
|
||||
worldRenderer?.render(sizeChanged)
|
||||
}
|
||||
window.world = worldRenderer
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
if (panoramaRenderer) {
|
||||
panoramaRenderer.dispose()
|
||||
panoramaRenderer = null
|
||||
}
|
||||
if (documentRenderer) {
|
||||
documentRenderer.dispose()
|
||||
}
|
||||
if (worldRenderer) {
|
||||
worldRenderer.destroy()
|
||||
worldRenderer = null
|
||||
}
|
||||
}
|
||||
|
||||
// Public interface
|
||||
const backend: GraphicsBackend = {
|
||||
//@ts-expect-error mark as three.js renderer
|
||||
__isThreeJsRenderer: true,
|
||||
NAME: `three.js ${THREE.REVISION}`,
|
||||
startPanorama,
|
||||
prepareResources,
|
||||
startWorld,
|
||||
disconnect,
|
||||
setRendering (rendering) {
|
||||
documentRenderer.setPaused(!rendering)
|
||||
},
|
||||
getDebugOverlay: () => ({
|
||||
}),
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
worldRenderer?.setFirstPersonCamera(pos, yaw, pitch)
|
||||
},
|
||||
setRoll (roll: number) {
|
||||
worldRenderer?.setCameraRoll(roll)
|
||||
},
|
||||
get soundSystem () {
|
||||
return worldRenderer?.soundSystem
|
||||
},
|
||||
get backendMethods () {
|
||||
if (!worldRenderer) return undefined
|
||||
return getBackendMethods(worldRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
return backend
|
||||
}
|
||||
|
||||
export default createGraphicsBackend
|
||||
207
renderer/viewer/three/panorama.ts
Normal file
207
renderer/viewer/three/panorama.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import { join } from 'path'
|
||||
import * as THREE from 'three'
|
||||
import { getSyncWorld } from 'renderer/playground/shared'
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { EntityMesh } from '../lib/entity/EntityMesh'
|
||||
import type { GraphicsInitOptions } from '../../../src/appViewer'
|
||||
import { WorldDataEmitter } from '../lib/worldDataEmitter'
|
||||
import { WorldRendererThree } from '../lib/worldrendererThree'
|
||||
import { defaultWorldRendererConfig } from '../lib/worldrendererCommon'
|
||||
import { BasePlayerState } from '../lib/basePlayerState'
|
||||
import { getDefaultRendererState } from '../baseGraphicsBackend'
|
||||
import { DocumentRenderer } from './documentRenderer'
|
||||
|
||||
const panoramaFiles = [
|
||||
'panorama_3.png', // right (+x)
|
||||
'panorama_1.png', // left (-x)
|
||||
'panorama_4.png', // top (+y)
|
||||
'panorama_5.png', // bottom (-y)
|
||||
'panorama_0.png', // front (+z)
|
||||
'panorama_2.png', // back (-z)
|
||||
]
|
||||
|
||||
export class PanoramaRenderer {
|
||||
private readonly camera: THREE.PerspectiveCamera
|
||||
private scene: THREE.Scene
|
||||
private readonly ambientLight: THREE.AmbientLight
|
||||
private readonly directionalLight: THREE.DirectionalLight
|
||||
private panoramaGroup: THREE.Object3D | null = null
|
||||
private time = 0
|
||||
private readonly abortController = new AbortController()
|
||||
private worldRenderer: WorldRendererThree | undefined
|
||||
|
||||
constructor (private readonly documentRenderer: DocumentRenderer, private readonly options: GraphicsInitOptions, private readonly doWorldBlocksPanorama = false) {
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.background = new THREE.Color(this.options.config.sceneBackground)
|
||||
|
||||
// Add ambient light
|
||||
this.ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
||||
this.scene.add(this.ambientLight)
|
||||
|
||||
// Add directional light
|
||||
this.directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
||||
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
||||
this.directionalLight.castShadow = true
|
||||
this.scene.add(this.directionalLight)
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000)
|
||||
this.camera.position.set(0, 0, 0)
|
||||
this.camera.rotation.set(0, 0, 0)
|
||||
}
|
||||
|
||||
async start () {
|
||||
if (this.doWorldBlocksPanorama) {
|
||||
await this.worldBlocksPanorama()
|
||||
} else {
|
||||
this.addClassicPanorama()
|
||||
}
|
||||
|
||||
|
||||
this.documentRenderer.render = (sizeChanged = false) => {
|
||||
if (sizeChanged) {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
this.documentRenderer.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
}
|
||||
|
||||
addClassicPanorama () {
|
||||
const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
|
||||
const loader = new THREE.TextureLoader()
|
||||
const panorMaterials = [] as THREE.MeshBasicMaterial[]
|
||||
|
||||
for (const file of panoramaFiles) {
|
||||
const texture = loader.load(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 // Changed from RepeatWrapping
|
||||
texture.wrapT = THREE.ClampToEdgeWrapping // Changed from RepeatWrapping
|
||||
texture.minFilter = THREE.LinearFilter
|
||||
texture.magFilter = THREE.LinearFilter
|
||||
|
||||
panorMaterials.push(new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
}))
|
||||
}
|
||||
|
||||
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
|
||||
panoramaBox.onBeforeRender = () => {
|
||||
this.time += 0.01
|
||||
panoramaBox.rotation.y = Math.PI + this.time * 0.01
|
||||
panoramaBox.rotation.z = Math.sin(-this.time * 0.001) * 0.001
|
||||
}
|
||||
|
||||
const group = new THREE.Object3D()
|
||||
group.add(panoramaBox)
|
||||
|
||||
// Add squids
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const m = new EntityMesh('1.16.4', 'squid').mesh
|
||||
m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17)
|
||||
m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX')
|
||||
const v = Math.random() * 0.01
|
||||
m.children[0].onBeforeRender = () => {
|
||||
m.rotation.y += v
|
||||
m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2
|
||||
}
|
||||
group.add(m)
|
||||
}
|
||||
|
||||
this.scene.add(group)
|
||||
this.panoramaGroup = group
|
||||
}
|
||||
|
||||
async worldBlocksPanorama () {
|
||||
const version = '1.21.4'
|
||||
this.options.resourcesManager.currentConfig = { version }
|
||||
await this.options.resourcesManager.updateAssetsData({ })
|
||||
if (this.abortController.signal.aborted) return
|
||||
console.time('load panorama scene')
|
||||
const world = getSyncWorld(version)
|
||||
const PrismarineBlock = require('prismarine-block')
|
||||
const Block = PrismarineBlock(version)
|
||||
const fullBlocks = loadedData.blocksArray.filter(block => {
|
||||
// if (block.name.includes('leaves')) return false
|
||||
if (/* !block.name.includes('wool') && */!block.name.includes('stained_glass')/* && !block.name.includes('terracotta') */) return false
|
||||
const b = Block.fromStateId(block.defaultState, 0)
|
||||
if (b.shapes?.length !== 1) return false
|
||||
const shape = b.shapes[0]
|
||||
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
||||
})
|
||||
const Z = -15
|
||||
const sizeX = 100
|
||||
const sizeY = 100
|
||||
for (let x = -sizeX; x < sizeX; x++) {
|
||||
for (let y = -sizeY; y < sizeY; y++) {
|
||||
const block = fullBlocks[Math.floor(Math.random() * fullBlocks.length)]
|
||||
world.setBlockStateId(new Vec3(x, y, Z), block.defaultState)
|
||||
}
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.camera.position.set(0.5, sizeY / 2 + 0.5, 0.5)
|
||||
this.camera.rotation.set(0, 0, 0)
|
||||
const initPos = new Vec3(...this.camera.position.toArray())
|
||||
const worldView = new WorldDataEmitter(world, 2, initPos)
|
||||
// worldView.addWaitTime = 0
|
||||
if (this.abortController.signal.aborted) return
|
||||
|
||||
this.worldRenderer = new WorldRendererThree(
|
||||
this.documentRenderer.renderer,
|
||||
this.options,
|
||||
{
|
||||
version,
|
||||
worldView,
|
||||
inWorldRenderingConfig: defaultWorldRendererConfig,
|
||||
playerState: new BasePlayerState(),
|
||||
rendererState: getDefaultRendererState()
|
||||
}
|
||||
)
|
||||
this.scene = this.worldRenderer.scene
|
||||
void worldView.init(initPos)
|
||||
|
||||
await this.worldRenderer.waitForChunksToRender()
|
||||
if (this.abortController.signal.aborted) return
|
||||
// add small camera rotation to side on mouse move depending on absolute position of the cursor
|
||||
const { camera } = this
|
||||
const initX = camera.position.x
|
||||
const initY = camera.position.y
|
||||
let prevTwin: tweenJs.Tween<THREE.Vector3> | undefined
|
||||
document.body.addEventListener('pointermove', (e) => {
|
||||
if (e.pointerType !== 'mouse') return
|
||||
const pos = new THREE.Vector2(e.clientX, e.clientY)
|
||||
const SCALE = 0.2
|
||||
/* -0.5 - 0.5 */
|
||||
const xRel = pos.x / window.innerWidth - 0.5
|
||||
const yRel = -(pos.y / window.innerHeight - 0.5)
|
||||
prevTwin?.stop()
|
||||
const to = {
|
||||
x: initX + (xRel * SCALE),
|
||||
y: initY + (yRel * SCALE)
|
||||
}
|
||||
prevTwin = new tweenJs.Tween(camera.position).to(to, 0) // todo use the number depending on diff // todo use the number depending on diff
|
||||
// prevTwin.easing(tweenJs.Easing.Exponential.InOut)
|
||||
prevTwin.start()
|
||||
camera.updateProjectionMatrix()
|
||||
}, {
|
||||
signal: this.abortController.signal
|
||||
})
|
||||
|
||||
console.timeEnd('load panorama scene')
|
||||
}
|
||||
|
||||
dispose () {
|
||||
this.scene.clear()
|
||||
this.worldRenderer?.destroy()
|
||||
this.abortController.abort()
|
||||
}
|
||||
}
|
||||
15
renderer/viewer/three/threeJsMethods.ts
Normal file
15
renderer/viewer/three/threeJsMethods.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { GraphicsBackend } from '../../../src/appViewer'
|
||||
import type { ThreeJsBackendMethods } from './graphicsBackend'
|
||||
|
||||
export function getThreeJsRendererMethods (): ThreeJsBackendMethods | undefined {
|
||||
const renderer = appViewer.backend
|
||||
if (!renderer?.['__isThreeJsRenderer'] || !renderer.backendMethods) return
|
||||
return new Proxy(renderer.backendMethods, {
|
||||
get (target, prop) {
|
||||
return async (...args) => {
|
||||
const result = await (target[prop as any] as any)(...args)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}) as ThreeJsBackendMethods
|
||||
}
|
||||
144
renderer/viewer/three/world/cursorBlock.ts
Normal file
144
renderer/viewer/three/world/cursorBlock.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import * as THREE from 'three'
|
||||
import { LineMaterial, LineSegmentsGeometry, Wireframe } from 'three-stdlib'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { Block } from 'prismarine-block'
|
||||
import { WorldRendererThree } from '../../lib/worldrendererThree'
|
||||
import destroyStage0 from '../../../../assets/destroy_stage_0.png'
|
||||
import destroyStage1 from '../../../../assets/destroy_stage_1.png'
|
||||
import destroyStage2 from '../../../../assets/destroy_stage_2.png'
|
||||
import destroyStage3 from '../../../../assets/destroy_stage_3.png'
|
||||
import destroyStage4 from '../../../../assets/destroy_stage_4.png'
|
||||
import destroyStage5 from '../../../../assets/destroy_stage_5.png'
|
||||
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'
|
||||
|
||||
export class CursorBlock {
|
||||
cursorLineMaterial: LineMaterial
|
||||
interactionLines: null | { blockPos; mesh } = null
|
||||
prevColor
|
||||
blockBreakMesh: THREE.Mesh
|
||||
breakTextures: THREE.Texture[] = []
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const breakMaterial = new THREE.MeshBasicMaterial({
|
||||
transparent: true,
|
||||
blending: THREE.MultiplyBlending,
|
||||
alphaTest: 0.5,
|
||||
})
|
||||
this.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial)
|
||||
this.blockBreakMesh.visible = false
|
||||
this.blockBreakMesh.renderOrder = 999
|
||||
this.blockBreakMesh.name = 'blockBreakMesh'
|
||||
this.worldRenderer.scene.add(this.blockBreakMesh)
|
||||
|
||||
subscribeKey(this.worldRenderer.playerState.reactive, 'gameMode', () => {
|
||||
this.updateLineMaterial()
|
||||
})
|
||||
|
||||
this.updateLineMaterial()
|
||||
}
|
||||
|
||||
// Update functions
|
||||
updateLineMaterial () {
|
||||
const inCreative = this.worldRenderer.displayOptions.playerState.reactive.gameMode === 'creative'
|
||||
const pixelRatio = this.worldRenderer.renderer.getPixelRatio()
|
||||
|
||||
this.cursorLineMaterial = new LineMaterial({
|
||||
color: (() => {
|
||||
switch (this.worldRenderer.worldRendererConfig.highlightBlockColor) {
|
||||
case 'blue':
|
||||
return 0x40_80_ff
|
||||
case 'classic':
|
||||
return 0x00_00_00
|
||||
default:
|
||||
return inCreative ? 0x40_80_ff : 0x00_00_00
|
||||
}
|
||||
})(),
|
||||
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
|
||||
// dashed: true,
|
||||
// dashSize: 5,
|
||||
})
|
||||
this.prevColor = this.worldRenderer.worldRendererConfig.highlightBlockColor
|
||||
}
|
||||
|
||||
updateBreakAnimation (block: Block | undefined, stage: number | null) {
|
||||
this.hideBreakAnimation()
|
||||
if (stage === null || !block) return
|
||||
|
||||
const mergedShape = bot.mouse.getMergedCursorShape(block)
|
||||
if (!mergedShape) return
|
||||
const { position, width, height, depth } = bot.mouse.getDataFromShape(mergedShape)
|
||||
this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001)
|
||||
position.add(block.position)
|
||||
this.blockBreakMesh.position.set(position.x, position.y, position.z)
|
||||
this.blockBreakMesh.visible = true;
|
||||
|
||||
(this.blockBreakMesh.material as THREE.MeshBasicMaterial).map = this.breakTextures[stage] ?? this.breakTextures.at(-1);
|
||||
(this.blockBreakMesh.material as THREE.MeshBasicMaterial).needsUpdate = true
|
||||
}
|
||||
|
||||
hideBreakAnimation () {
|
||||
if (this.blockBreakMesh) {
|
||||
this.blockBreakMesh.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplay () {
|
||||
if (this.cursorLineMaterial) {
|
||||
const { renderer } = this.worldRenderer
|
||||
this.cursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
|
||||
this.cursorLineMaterial.dashOffset = performance.now() / 750
|
||||
}
|
||||
}
|
||||
|
||||
setHighlightCursorBlock (blockPos: Vec3 | null, shapePositions?: Array<{ position: any; width: any; height: any; depth: any; }>): void {
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
||||
return
|
||||
}
|
||||
if (this.interactionLines !== null) {
|
||||
this.worldRenderer.scene.remove(this.interactionLines.mesh)
|
||||
this.interactionLines = null
|
||||
}
|
||||
if (blockPos === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
for (const { position, width, height, depth } of shapePositions ?? []) {
|
||||
const scale = [1.0001 * width, 1.0001 * height, 1.0001 * depth] as const
|
||||
const geometry = new THREE.BoxGeometry(...scale)
|
||||
const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
|
||||
const wireframe = new Wireframe(lines, this.cursorLineMaterial)
|
||||
const pos = blockPos.plus(position)
|
||||
wireframe.position.set(pos.x, pos.y, pos.z)
|
||||
wireframe.computeLineDistances()
|
||||
group.add(wireframe)
|
||||
}
|
||||
this.worldRenderer.scene.add(group)
|
||||
this.interactionLines = { blockPos, mesh: group }
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.prevColor !== this.worldRenderer.worldRendererConfig.highlightBlockColor) {
|
||||
this.updateLineMaterial()
|
||||
}
|
||||
this.updateDisplay()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,41 +3,38 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|||
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js'
|
||||
import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad'
|
||||
import * as THREE from 'three'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { subscribe } from 'valtio'
|
||||
import { activeModalStack, hideModal } from './globalState'
|
||||
import { watchUnloadForCleanup } from './gameUnload'
|
||||
import { options } from './optionsStorage'
|
||||
import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree'
|
||||
|
||||
export async function initVR () {
|
||||
options.vrSupport = true
|
||||
const { renderer } = viewer
|
||||
if (!('xr' in navigator)) return
|
||||
export async function initVR (worldRenderer: WorldRendererThree) {
|
||||
if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return
|
||||
const { renderer } = worldRenderer
|
||||
|
||||
const isSupported = await checkVRSupport()
|
||||
if (!isSupported) return
|
||||
|
||||
enableVr(renderer)
|
||||
enableVr()
|
||||
|
||||
const vrButtonContainer = createVrButtonContainer(renderer)
|
||||
const updateVrButtons = () => {
|
||||
vrButtonContainer.hidden = !options.vrSupport || activeModalStack.length !== 0
|
||||
const newHidden = !worldRenderer.worldRendererConfig.vrSupport || !worldRenderer.worldRendererConfig.foreground
|
||||
if (vrButtonContainer.hidden !== newHidden) {
|
||||
vrButtonContainer.hidden = newHidden
|
||||
}
|
||||
}
|
||||
|
||||
const unsubWatchSetting = subscribeKey(options, 'vrSupport', updateVrButtons)
|
||||
const unsubWatchModals = subscribe(activeModalStack, updateVrButtons)
|
||||
worldRenderer.onRender.push(updateVrButtons)
|
||||
|
||||
function enableVr (renderer) {
|
||||
function enableVr () {
|
||||
renderer.xr.enabled = true
|
||||
worldRenderer.reactiveState.preventEscapeMenu = true
|
||||
}
|
||||
|
||||
function disableVr () {
|
||||
renderer.xr.enabled = false
|
||||
viewer.cameraObjectOverride = undefined
|
||||
viewer.scene.remove(user)
|
||||
worldRenderer.cameraObjectOverride = undefined
|
||||
worldRenderer.reactiveState.preventEscapeMenu = false
|
||||
worldRenderer.scene.remove(user)
|
||||
vrButtonContainer.hidden = true
|
||||
unsubWatchSetting()
|
||||
unsubWatchModals()
|
||||
}
|
||||
|
||||
function createVrButtonContainer (renderer) {
|
||||
|
|
@ -84,7 +81,7 @@ export async function initVR () {
|
|||
|
||||
closeButton.addEventListener('click', () => {
|
||||
container.hidden = true
|
||||
options.vrSupport = false
|
||||
worldRenderer.worldRendererConfig.vrSupport = false
|
||||
})
|
||||
|
||||
return closeButton
|
||||
|
|
@ -103,8 +100,8 @@ export async function initVR () {
|
|||
|
||||
// hack for vr camera
|
||||
const user = new THREE.Group()
|
||||
user.add(viewer.camera)
|
||||
viewer.scene.add(user)
|
||||
user.add(worldRenderer.camera)
|
||||
worldRenderer.scene.add(user)
|
||||
const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader())
|
||||
const controller1 = renderer.xr.getControllerGrip(0)
|
||||
const controller2 = renderer.xr.getControllerGrip(1)
|
||||
|
|
@ -191,8 +188,8 @@ export async function initVR () {
|
|||
rotSnapReset = true
|
||||
}
|
||||
|
||||
// viewer.setFirstPersonCamera(null, yawOffset, 0)
|
||||
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
|
||||
// appViewer.backend?.updateCamera(null, yawOffset, 0)
|
||||
worldRenderer.updateCamera(null, bot.entity.yaw, bot.entity.pitch)
|
||||
|
||||
// todo restore this logic (need to preserve ability to move camera)
|
||||
// const xrCamera = renderer.xr.getCamera()
|
||||
|
|
@ -203,20 +200,16 @@ export async function initVR () {
|
|||
// todo ?
|
||||
// bot.physics.stepHeight = 1
|
||||
|
||||
viewer.render()
|
||||
worldRenderer.render()
|
||||
})
|
||||
renderer.xr.addEventListener('sessionstart', () => {
|
||||
viewer.cameraObjectOverride = user
|
||||
// close all modals to be in game
|
||||
for (const _modal of activeModalStack) {
|
||||
hideModal(undefined, {}, { force: true })
|
||||
}
|
||||
worldRenderer.cameraObjectOverride = user
|
||||
})
|
||||
renderer.xr.addEventListener('sessionend', () => {
|
||||
viewer.cameraObjectOverride = undefined
|
||||
worldRenderer.cameraObjectOverride = undefined
|
||||
})
|
||||
|
||||
watchUnloadForCleanup(disableVr)
|
||||
worldRenderer.abortController.signal.addEventListener('abort', disableVr)
|
||||
}
|
||||
|
||||
const xrStandardRightButtonsMap = [
|
||||
250
src/appViewer.ts
Normal file
250
src/appViewer.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
||||
import { IPlayerState } 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/lib/threeJsSound'
|
||||
import { proxy } from 'valtio'
|
||||
import { getDefaultRendererState } from 'renderer/viewer/baseGraphicsBackend'
|
||||
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 { watchOptionsAfterWorldViewInit } from './watchOptions'
|
||||
|
||||
export interface RendererReactiveState {
|
||||
world: {
|
||||
chunksLoaded: string[]
|
||||
chunksTotalNumber: number
|
||||
allChunksLoaded: boolean
|
||||
mesherWork: boolean
|
||||
intersectMedia: { id: string, x: number, y: number } | null
|
||||
}
|
||||
renderer: string
|
||||
preventEscapeMenu: boolean
|
||||
}
|
||||
|
||||
export interface GraphicsBackendConfig {
|
||||
fpsLimit?: number
|
||||
powerPreference?: 'high-performance' | 'low-power'
|
||||
statsVisible?: number
|
||||
sceneBackground: string
|
||||
}
|
||||
|
||||
const defaultGraphicsBackendConfig: GraphicsBackendConfig = {
|
||||
fpsLimit: undefined,
|
||||
powerPreference: undefined,
|
||||
sceneBackground: 'lightblue'
|
||||
}
|
||||
|
||||
export interface GraphicsInitOptions {
|
||||
resourcesManager: ResourcesManager
|
||||
config: GraphicsBackendConfig
|
||||
|
||||
displayCriticalError: (error: Error) => void
|
||||
}
|
||||
|
||||
export interface DisplayWorldOptions {
|
||||
version: string
|
||||
worldView: WorldDataEmitter
|
||||
inWorldRenderingConfig: WorldRendererConfig
|
||||
playerState: IPlayerState
|
||||
rendererState: RendererReactiveState
|
||||
}
|
||||
|
||||
export type GraphicsBackendLoader = (options: GraphicsInitOptions) => GraphicsBackend
|
||||
|
||||
// no sync methods
|
||||
export interface GraphicsBackend {
|
||||
NAME: string
|
||||
startPanorama: () => void
|
||||
// prepareResources: (version: string, progressReporter: ProgressReporter) => Promise<void>
|
||||
startWorld: (options: DisplayWorldOptions) => void
|
||||
disconnect: () => void
|
||||
setRendering: (rendering: boolean) => void
|
||||
getDebugOverlay: () => Record<string, any>
|
||||
updateCamera: (pos: Vec3 | null, yaw: number, pitch: number) => void
|
||||
setRoll: (roll: number) => void
|
||||
soundSystem: SoundSystem | undefined
|
||||
|
||||
backendMethods: Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
export class AppViewer {
|
||||
resourcesManager = new ResourcesManager()
|
||||
worldView: WorldDataEmitter | undefined
|
||||
readonly config: GraphicsBackendConfig = {
|
||||
...defaultGraphicsBackendConfig,
|
||||
powerPreference: options.gpuPreference === 'default' ? undefined : options.gpuPreference
|
||||
}
|
||||
backend?: GraphicsBackend
|
||||
backendLoader?: GraphicsBackendLoader
|
||||
private currentState?: {
|
||||
method: string
|
||||
args: any[]
|
||||
}
|
||||
currentDisplay = null as 'menu' | 'world' | null
|
||||
inWorldRenderingConfig: WorldRendererConfig = proxy(defaultWorldRendererConfig)
|
||||
lastCamUpdate = 0
|
||||
playerState = playerState
|
||||
rendererState = proxy(getDefaultRendererState())
|
||||
worldReady: Promise<void>
|
||||
private resolveWorldReady: () => void
|
||||
|
||||
constructor () {
|
||||
this.disconnectBackend()
|
||||
}
|
||||
|
||||
loadBackend (loader: GraphicsBackendLoader) {
|
||||
if (this.backend) {
|
||||
this.disconnectBackend()
|
||||
}
|
||||
|
||||
this.backendLoader = loader
|
||||
const loaderOptions: GraphicsInitOptions = {
|
||||
resourcesManager: this.resourcesManager,
|
||||
config: this.config,
|
||||
displayCriticalError (error) {
|
||||
console.error(error)
|
||||
setLoadingScreenStatus(error.message, true)
|
||||
},
|
||||
}
|
||||
this.backend = loader(loaderOptions)
|
||||
|
||||
// if (this.resourcesManager.currentResources) {
|
||||
// void this.prepareResources(this.resourcesManager.currentResources.version, createNotificationProgressReporter())
|
||||
// }
|
||||
|
||||
// Execute queued action if exists
|
||||
if (this.currentState) {
|
||||
const { method, args } = this.currentState
|
||||
this.backend[method](...args)
|
||||
if (method === 'startWorld') {
|
||||
void this.worldView!.init(args[0].playerState.getPosition())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startWorld (world, renderDistance: number, playerStateSend: IPlayerState = playerState) {
|
||||
if (this.currentDisplay === 'world') throw new Error('World already started')
|
||||
this.currentDisplay = 'world'
|
||||
const startPosition = playerStateSend.getPosition()
|
||||
this.worldView = new WorldDataEmitter(world, renderDistance, startPosition)
|
||||
window.worldView = this.worldView
|
||||
watchOptionsAfterWorldViewInit(this.worldView)
|
||||
|
||||
const displayWorldOptions: DisplayWorldOptions = {
|
||||
version: this.resourcesManager.currentConfig!.version,
|
||||
worldView: this.worldView,
|
||||
inWorldRenderingConfig: this.inWorldRenderingConfig,
|
||||
playerState: playerStateSend,
|
||||
rendererState: this.rendererState
|
||||
}
|
||||
if (this.backend) {
|
||||
this.backend.startWorld(displayWorldOptions)
|
||||
void this.worldView.init(startPosition)
|
||||
}
|
||||
this.currentState = { method: 'startWorld', args: [displayWorldOptions] }
|
||||
|
||||
// Resolve the promise after world is started
|
||||
this.resolveWorldReady()
|
||||
}
|
||||
|
||||
resetBackend (cleanState = false) {
|
||||
if (cleanState) {
|
||||
this.currentState = undefined
|
||||
this.currentDisplay = null
|
||||
this.worldView = undefined
|
||||
}
|
||||
if (this.backendLoader) {
|
||||
this.loadBackend(this.backendLoader)
|
||||
}
|
||||
}
|
||||
|
||||
startPanorama () {
|
||||
if (this.currentDisplay === 'menu') return
|
||||
this.currentDisplay = 'menu'
|
||||
if (options.disableAssets) return
|
||||
if (this.backend) {
|
||||
this.backend.startPanorama()
|
||||
}
|
||||
this.currentState = { method: 'startPanorama', args: [] }
|
||||
}
|
||||
|
||||
// async prepareResources (version: string, progressReporter: ProgressReporter) {
|
||||
// if (this.backend) {
|
||||
// await this.backend.prepareResources(version, progressReporter)
|
||||
// }
|
||||
// }
|
||||
|
||||
destroyAll () {
|
||||
this.disconnectBackend()
|
||||
this.resourcesManager.destroy()
|
||||
}
|
||||
|
||||
disconnectBackend () {
|
||||
if (this.backend) {
|
||||
this.backend.disconnect()
|
||||
this.backend = undefined
|
||||
}
|
||||
this.currentDisplay = null
|
||||
const { promise, resolve } = Promise.withResolvers<void>()
|
||||
this.worldReady = promise
|
||||
this.resolveWorldReady = resolve
|
||||
Object.assign(this.rendererState, getDefaultRendererState())
|
||||
// this.queuedDisplay = undefined
|
||||
}
|
||||
|
||||
get utils () {
|
||||
return {
|
||||
async waitingForChunks () {
|
||||
if (this.backend?.worldState.allChunksLoaded) return
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (this.backend?.worldState.allChunksLoaded) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const appViewer = new AppViewer()
|
||||
window.appViewer = appViewer
|
||||
|
||||
const initialMenuStart = async () => {
|
||||
if (appViewer.currentDisplay === 'world') {
|
||||
appViewer.resetBackend(true)
|
||||
}
|
||||
appViewer.startPanorama()
|
||||
|
||||
// await appViewer.resourcesManager.loadMcData('1.21.4')
|
||||
// const world = getSyncWorld('1.21.4')
|
||||
// await appViewer.prepareResources('1.21.4', createNullProgressReporter())
|
||||
// world.setBlockStateId(new Vec3(0, 64, 0), 1)
|
||||
// appViewer.startWorld(world, 3, new Vec3(0, 64, 0), new BasePlayerState())
|
||||
// appViewer.backend?.updateCamera(new Vec3(0, 64, 2), 0, 0)
|
||||
// void appViewer.worldView.init(new Vec3(0, 64, 0))
|
||||
}
|
||||
window.initialMenuStart = initialMenuStart
|
||||
|
||||
const modalStackUpdateChecks = () => {
|
||||
// maybe start panorama
|
||||
if (activeModalStack.length === 0 && !miscUiState.gameLoaded) {
|
||||
void initialMenuStart()
|
||||
}
|
||||
|
||||
if (appViewer.backend) {
|
||||
const hasAppStatus = activeModalStack.some(m => m.reactType === 'app-status')
|
||||
appViewer.backend.setRendering(!hasAppStatus)
|
||||
}
|
||||
|
||||
appViewer.inWorldRenderingConfig.foreground = activeModalStack.length === 0
|
||||
}
|
||||
subscribeKey(activeModalStack, 'length', modalStackUpdateChecks)
|
||||
modalStackUpdateChecks()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import * as nbt from 'prismarine-nbt'
|
||||
|
||||
export const displayClientChat = (text: string) => {
|
||||
|
|
|
|||
|
|
@ -37,18 +37,19 @@ export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => {
|
|||
const maxPitch = 0.5 * Math.PI
|
||||
const minPitch = -0.5 * Math.PI
|
||||
|
||||
viewer.world.lastCamUpdate = Date.now()
|
||||
appViewer.lastCamUpdate = Date.now()
|
||||
|
||||
if (viewer.world.freeFlyMode) {
|
||||
// Update freeFlyState directly
|
||||
viewer.world.freeFlyState.yaw = (viewer.world.freeFlyState.yaw - x) % (2 * Math.PI)
|
||||
viewer.world.freeFlyState.pitch = Math.max(minPitch, Math.min(maxPitch, viewer.world.freeFlyState.pitch - y))
|
||||
return
|
||||
}
|
||||
// if (viewer.world.freeFlyMode) {
|
||||
// // Update freeFlyState directly
|
||||
// viewer.world.freeFlyState.yaw = (viewer.world.freeFlyState.yaw - x) % (2 * Math.PI)
|
||||
// viewer.world.freeFlyState.pitch = Math.max(minPitch, Math.min(maxPitch, viewer.world.freeFlyState.pitch - y))
|
||||
// return
|
||||
// }
|
||||
|
||||
if (!bot?.entity) return
|
||||
const pitch = bot.entity.pitch - y
|
||||
void bot.look(bot.entity.yaw - x, Math.max(minPitch, Math.min(maxPitch, pitch)), true)
|
||||
appViewer.backend?.updateCamera(null, bot.entity.yaw, pitch)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
|
|
@ -76,7 +77,7 @@ function pointerLockChangeCallback () {
|
|||
if (notificationProxy.id === 'pointerlockchange') {
|
||||
hideNotification()
|
||||
}
|
||||
if (viewer.renderer.xr.isPresenting) return // todo
|
||||
if (appViewer.rendererState.preventEscapeMenu) return
|
||||
if (!pointerLock.hasPointerLock && activeModalStack.length === 0 && miscUiState.gameLoaded) {
|
||||
showModal({ reactType: 'pause-screen' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { fromFormattedString, TextComponent } from '@xmcl/text-component'
|
||||
import type { IndexedData } from 'minecraft-data'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
|
||||
export type MessageFormatPart = Pick<TextComponent, 'hoverEvent' | 'clickEvent'> & {
|
||||
text: string
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import MinecraftData from 'minecraft-data'
|
|||
import PrismarineBlock from 'prismarine-block'
|
||||
import PrismarineItem from 'prismarine-item'
|
||||
import pathfinder from 'mineflayer-pathfinder'
|
||||
import { importLargeData } from '../generated/large-data-aliases'
|
||||
import { miscUiState } from './globalState'
|
||||
import supportedVersions from './supportedVersions.mjs'
|
||||
import { options } from './optionsStorage'
|
||||
|
|
@ -44,7 +43,7 @@ export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVers
|
|||
return autoVersionSelect
|
||||
}
|
||||
|
||||
export const loadMinecraftData = async (version: string, importBlockstatesModels = false) => {
|
||||
export const loadMinecraftData = async (version: string) => {
|
||||
await window._LOAD_MC_DATA()
|
||||
// setLoadingScreenStatus(`Loading data for ${version}`)
|
||||
// // todo expose cache
|
||||
|
|
@ -58,12 +57,9 @@ export const loadMinecraftData = async (version: string, importBlockstatesModels
|
|||
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
|
||||
window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!)
|
||||
window.loadedData = mcData
|
||||
window.mcData = mcData
|
||||
window.pathfinder = pathfinder
|
||||
miscUiState.loadedDataVersion = version
|
||||
|
||||
if (importBlockstatesModels) {
|
||||
viewer.world.blockstatesModels = await importLargeData('blockStatesModels')
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadAllMinecraftData = async () => {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
import { Vec3 } from 'vec3'
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { ControMax } from 'contro-max/build/controMax'
|
||||
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
|
||||
import { stringStartsWith } from 'contro-max/build/stringUtils'
|
||||
import { UserOverrideCommand, UserOverridesConfig } from 'contro-max/build/types/store'
|
||||
import { GameMode } from 'mineflayer'
|
||||
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, hideModal, hideAllModals } from './globalState'
|
||||
import { goFullscreen, isInRealGameSession, pointerLock, reloadChunks } from './utils'
|
||||
|
|
@ -133,21 +131,21 @@ contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => {
|
|||
miscUiState.usingGamepadInput = gamepadIndex !== undefined
|
||||
if (!bot || !isGameActive(false)) return
|
||||
|
||||
if (viewer.world.freeFlyMode) {
|
||||
// Create movement vector from input
|
||||
const direction = new THREE.Vector3(0, 0, 0)
|
||||
if (vector.z !== undefined) direction.z = vector.z
|
||||
if (vector.x !== undefined) direction.x = vector.x
|
||||
// if (viewer.world.freeFlyMode) {
|
||||
// // Create movement vector from input
|
||||
// const direction = new THREE.Vector3(0, 0, 0)
|
||||
// if (vector.z !== undefined) direction.z = vector.z
|
||||
// if (vector.x !== undefined) direction.x = vector.x
|
||||
|
||||
// Apply camera rotation to movement direction
|
||||
direction.applyQuaternion(viewer.camera.quaternion)
|
||||
// // Apply camera rotation to movement direction
|
||||
// direction.applyQuaternion(viewer.camera.quaternion)
|
||||
|
||||
// Update freeFlyState position with normalized direction
|
||||
const moveSpeed = 1
|
||||
direction.multiplyScalar(moveSpeed)
|
||||
viewer.world.freeFlyState.position.add(new Vec3(direction.x, direction.y, direction.z))
|
||||
return
|
||||
}
|
||||
// // Update freeFlyState position with normalized direction
|
||||
// const moveSpeed = 1
|
||||
// direction.multiplyScalar(moveSpeed)
|
||||
// viewer.world.freeFlyState.position.add(new Vec3(direction.x, direction.y, direction.z))
|
||||
// return
|
||||
// }
|
||||
|
||||
// gamepadIndex will be used for splitscreen in future
|
||||
const coordToAction = [
|
||||
|
|
@ -355,20 +353,20 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
|||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
||||
switch (command) {
|
||||
case 'general.jump':
|
||||
if (viewer.world.freeFlyMode) {
|
||||
const moveSpeed = 0.5
|
||||
viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0))
|
||||
} else {
|
||||
bot.setControlState('jump', pressed)
|
||||
}
|
||||
// if (viewer.world.freeFlyMode) {
|
||||
// const moveSpeed = 0.5
|
||||
// viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0))
|
||||
// } else {
|
||||
bot.setControlState('jump', pressed)
|
||||
// }
|
||||
break
|
||||
case 'general.sneak':
|
||||
if (viewer.world.freeFlyMode) {
|
||||
const moveSpeed = 0.5
|
||||
viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? -moveSpeed : 0, 0))
|
||||
} else {
|
||||
setSneaking(pressed)
|
||||
}
|
||||
// if (viewer.world.freeFlyMode) {
|
||||
// const moveSpeed = 0.5
|
||||
// viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? -moveSpeed : 0, 0))
|
||||
// } else {
|
||||
setSneaking(pressed)
|
||||
// }
|
||||
break
|
||||
case 'general.sprint':
|
||||
// todo add setting to change behavior
|
||||
|
|
@ -592,12 +590,12 @@ export const f3Keybinds: Array<{
|
|||
for (const [x, z] of loadedChunks) {
|
||||
worldView!.unloadChunk({ x, z })
|
||||
}
|
||||
for (const child of viewer.scene.children) {
|
||||
if (child.name === 'chunk') { // should not happen
|
||||
viewer.scene.remove(child)
|
||||
console.warn('forcefully removed chunk from scene')
|
||||
}
|
||||
}
|
||||
// for (const child of viewer.scene.children) {
|
||||
// if (child.name === 'chunk') { // should not happen
|
||||
// viewer.scene.remove(child)
|
||||
// console.warn('forcefully removed chunk from scene')
|
||||
// }
|
||||
// }
|
||||
if (localServer) {
|
||||
//@ts-expect-error not sure why it is private... maybe revisit api?
|
||||
localServer.players[0].world.columns = {}
|
||||
|
|
@ -610,7 +608,6 @@ export const f3Keybinds: Array<{
|
|||
key: 'KeyG',
|
||||
action () {
|
||||
options.showChunkBorders = !options.showChunkBorders
|
||||
viewer.world.updateShowChunksBorder(options.showChunkBorders)
|
||||
},
|
||||
mobileTitle: 'Toggle chunk borders',
|
||||
},
|
||||
|
|
@ -926,9 +923,16 @@ window.addEventListener('keydown', (e) => {
|
|||
|
||||
// #region experimental debug things
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'KeyL' && e.altKey) {
|
||||
if (e.code === 'KeyL' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||
console.clear()
|
||||
}
|
||||
if (e.code === 'KeyK' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||
if (sessionStorage.delayLoadUntilFocus) {
|
||||
sessionStorage.removeItem('delayLoadUntilFocus')
|
||||
} else {
|
||||
sessionStorage.setItem('delayLoadUntilFocus', 'true')
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -181,13 +181,13 @@ export const createNotificationProgressReporter = (endMessage?: string): Progres
|
|||
})
|
||||
}
|
||||
|
||||
export const createConsoleLogProgressReporter = (): ProgressReporter => {
|
||||
export const createConsoleLogProgressReporter = (group?: string): ProgressReporter => {
|
||||
return createProgressReporter({
|
||||
setMessage (message: string) {
|
||||
console.log(message)
|
||||
console.log(group ? `[${group}] ${message}` : message)
|
||||
},
|
||||
end () {
|
||||
console.log('done')
|
||||
console.log(group ? `[${group}] done` : 'done')
|
||||
},
|
||||
|
||||
error (message: string): void {
|
||||
|
|
@ -217,3 +217,14 @@ export const createWrappedProgressReporter = (reporter: ProgressReporter, messag
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const createNullProgressReporter = (): ProgressReporter => {
|
||||
return createProgressReporter({
|
||||
setMessage (message: string) {
|
||||
},
|
||||
end () {
|
||||
},
|
||||
error (message: string) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,35 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import PItem from 'prismarine-item'
|
||||
import * as THREE from 'three'
|
||||
import { WorldRendererThree } from '../renderer/viewer/lib/worldrendererThree'
|
||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||
import { options } from './optionsStorage'
|
||||
import { jeiCustomCategories } from './inventoryWindows'
|
||||
|
||||
customEvents.on('mineflayerBotCreated', async () => {
|
||||
if (!options.customChannels) return
|
||||
await new Promise(resolve => {
|
||||
bot.once('login', () => {
|
||||
resolve(true)
|
||||
export default () => {
|
||||
customEvents.on('mineflayerBotCreated', async () => {
|
||||
if (!options.customChannels) return
|
||||
await new Promise(resolve => {
|
||||
bot.once('login', () => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
registerBlockModelsChannel()
|
||||
registerMediaChannels()
|
||||
registeredJeiChannel()
|
||||
})
|
||||
registerBlockModelsChannel()
|
||||
registerMediaChannels()
|
||||
registeredJeiChannel()
|
||||
})
|
||||
}
|
||||
|
||||
const registerChannel = (channelName: string, packetStructure: any[], handler: (data: any) => void, waitForWorld = true) => {
|
||||
bot._client.registerChannel(channelName, packetStructure, true)
|
||||
bot._client.on(channelName as any, async (data) => {
|
||||
if (waitForWorld) {
|
||||
await appViewer.worldReady
|
||||
handler(data)
|
||||
} else {
|
||||
handler(data)
|
||||
}
|
||||
})
|
||||
|
||||
console.debug(`registered custom channel ${channelName} channel`)
|
||||
}
|
||||
|
||||
const registerBlockModelsChannel = () => {
|
||||
const CHANNEL_NAME = 'minecraft-web-client:blockmodels'
|
||||
|
|
@ -46,9 +60,7 @@ const registerBlockModelsChannel = () => {
|
|||
]
|
||||
]
|
||||
|
||||
bot._client.registerChannel(CHANNEL_NAME, packetStructure, true)
|
||||
|
||||
bot._client.on(CHANNEL_NAME as any, (data) => {
|
||||
registerChannel(CHANNEL_NAME, packetStructure, (data) => {
|
||||
const { worldName, x, y, z, model } = data
|
||||
|
||||
const chunkX = Math.floor(x / 16) * 16
|
||||
|
|
@ -56,31 +68,8 @@ const registerBlockModelsChannel = () => {
|
|||
const chunkKey = `${chunkX},${chunkZ}`
|
||||
const blockPosKey = `${x},${y},${z}`
|
||||
|
||||
const chunkModels = viewer.world.protocolCustomBlocks.get(chunkKey) || {}
|
||||
|
||||
if (model) {
|
||||
chunkModels[blockPosKey] = model
|
||||
} else {
|
||||
delete chunkModels[blockPosKey]
|
||||
}
|
||||
|
||||
if (Object.keys(chunkModels).length > 0) {
|
||||
viewer.world.protocolCustomBlocks.set(chunkKey, chunkModels)
|
||||
} else {
|
||||
viewer.world.protocolCustomBlocks.delete(chunkKey)
|
||||
}
|
||||
|
||||
// Trigger update
|
||||
if (worldView) {
|
||||
const block = worldView.world.getBlock(new Vec3(x, y, z))
|
||||
if (block) {
|
||||
worldView.world.setBlockStateId(new Vec3(x, y, z), block.stateId)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
console.debug(`registered custom channel ${CHANNEL_NAME} channel`)
|
||||
getThreeJsRendererMethods()?.updateCustomBlock(chunkKey, blockPosKey, model)
|
||||
}, true)
|
||||
}
|
||||
|
||||
const registeredJeiChannel = () => {
|
||||
|
|
@ -155,7 +144,7 @@ const registerMediaChannels = () => {
|
|||
{ name: 'rotation', type: 'i16' }, // 0: 0° - towards positive z, 1: 90° - positive x, 2: 180° - negative z, 3: 270° - negative x (3-6 is same but double side)
|
||||
{ name: 'source', type: ['pstring', { countType: 'i16' }] },
|
||||
{ name: 'loop', type: 'bool' },
|
||||
{ name: '_volume', type: 'f32' }, // 0
|
||||
{ name: 'volume', type: 'f32' }, // 0
|
||||
{ name: '_aspectRatioMode', type: 'i16' }, // 0
|
||||
{ name: '_background', type: 'i16' }, // 0
|
||||
{ name: '_opacity', type: 'i16' }, // 1
|
||||
|
|
@ -199,16 +188,11 @@ const registerMediaChannels = () => {
|
|||
bot._client.registerChannel(DESTROY_CHANNEL, noDataPacketStructure, true)
|
||||
|
||||
// Handle media add
|
||||
bot._client.on(ADD_CHANNEL as any, (data) => {
|
||||
const { id, x, y, z, width, height, rotation, source, loop, background, opacity } = data
|
||||
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
|
||||
// Destroy existing video if it exists
|
||||
worldRenderer.destroyMedia(id)
|
||||
registerChannel(ADD_CHANNEL, addPacketStructure, (data) => {
|
||||
const { id, x, y, z, width, height, rotation, source, loop, volume, background, opacity } = data
|
||||
|
||||
// Add new video
|
||||
worldRenderer.addMedia(id, {
|
||||
getThreeJsRendererMethods()?.addMedia(id, {
|
||||
position: { x, y, z },
|
||||
size: { width, height },
|
||||
// side: 'towards',
|
||||
|
|
@ -217,59 +201,47 @@ const registerMediaChannels = () => {
|
|||
doubleSide: false,
|
||||
background,
|
||||
opacity: opacity / 100,
|
||||
allowOrigins: options.remoteContentNotSameOrigin === false ? [getCurrentTopDomain()] : options.remoteContentNotSameOrigin
|
||||
allowOrigins: options.remoteContentNotSameOrigin === false ? [getCurrentTopDomain()] : options.remoteContentNotSameOrigin,
|
||||
loop,
|
||||
volume
|
||||
})
|
||||
|
||||
// Set loop state
|
||||
if (!loop) {
|
||||
const videoData = worldRenderer.customMedia.get(id)
|
||||
if (videoData?.video) {
|
||||
videoData.video.loop = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle media play
|
||||
bot._client.on(PLAY_CHANNEL as any, (data) => {
|
||||
registerChannel(PLAY_CHANNEL, noDataPacketStructure, (data) => {
|
||||
const { id } = data
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
worldRenderer.setVideoPlaying(id, true)
|
||||
})
|
||||
getThreeJsRendererMethods()?.setVideoPlaying(id, true)
|
||||
}, true)
|
||||
|
||||
// Handle media pause
|
||||
bot._client.on(PAUSE_CHANNEL as any, (data) => {
|
||||
registerChannel(PAUSE_CHANNEL, noDataPacketStructure, (data) => {
|
||||
const { id } = data
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
worldRenderer.setVideoPlaying(id, false)
|
||||
})
|
||||
getThreeJsRendererMethods()?.setVideoPlaying(id, false)
|
||||
}, true)
|
||||
|
||||
// Handle media seek
|
||||
bot._client.on(SEEK_CHANNEL as any, (data) => {
|
||||
registerChannel(SEEK_CHANNEL, setNumberPacketStructure, (data) => {
|
||||
const { id, seconds } = data
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
worldRenderer.setVideoSeeking(id, seconds)
|
||||
})
|
||||
getThreeJsRendererMethods()?.setVideoSeeking(id, seconds)
|
||||
}, true)
|
||||
|
||||
// Handle media destroy
|
||||
bot._client.on(DESTROY_CHANNEL as any, (data) => {
|
||||
registerChannel(DESTROY_CHANNEL, noDataPacketStructure, (data) => {
|
||||
const { id } = data
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
worldRenderer.destroyMedia(id)
|
||||
})
|
||||
getThreeJsRendererMethods()?.destroyMedia(id)
|
||||
}, true)
|
||||
|
||||
// Handle media volume
|
||||
bot._client.on(VOLUME_CHANNEL as any, (data) => {
|
||||
registerChannel(VOLUME_CHANNEL, setNumberPacketStructure, (data) => {
|
||||
const { id, volume } = data
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
worldRenderer.setVideoVolume(id, volume)
|
||||
})
|
||||
getThreeJsRendererMethods()?.setVideoVolume(id, volume)
|
||||
}, true)
|
||||
|
||||
// Handle media speed
|
||||
bot._client.on(SPEED_CHANNEL as any, (data) => {
|
||||
registerChannel(SPEED_CHANNEL, setNumberPacketStructure, (data) => {
|
||||
const { id, speed } = data
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
worldRenderer.setVideoSpeed(id, speed)
|
||||
})
|
||||
getThreeJsRendererMethods()?.setVideoSpeed(id, speed)
|
||||
}, true)
|
||||
|
||||
// ---
|
||||
|
||||
|
|
@ -296,37 +268,9 @@ export const sendVideoInteraction = (id: string, x: number, y: number, isRightCl
|
|||
}
|
||||
|
||||
export const videoCursorInteraction = () => {
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
const { camera } = worldRenderer
|
||||
const raycaster = new THREE.Raycaster()
|
||||
|
||||
// Get mouse position at center of screen
|
||||
const mouse = new THREE.Vector2(0, 0)
|
||||
|
||||
// Update the raycaster
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
|
||||
// Check intersection with all video meshes
|
||||
for (const [id, videoData] of worldRenderer.customMedia.entries()) {
|
||||
// Get the actual mesh (first child of the group)
|
||||
const mesh = videoData.mesh.children[0] as THREE.Mesh
|
||||
if (!mesh) continue
|
||||
|
||||
const intersects = raycaster.intersectObject(mesh, false)
|
||||
if (intersects.length > 0) {
|
||||
const intersection = intersects[0]
|
||||
const { uv } = intersection
|
||||
if (!uv) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
x: uv.x,
|
||||
y: uv.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
const { intersectMedia } = appViewer.rendererState.world
|
||||
if (!intersectMedia) return null
|
||||
return intersectMedia
|
||||
}
|
||||
window.videoCursorInteraction = videoCursorInteraction
|
||||
|
||||
|
|
@ -335,10 +279,8 @@ const addTestVideo = (rotation = 0 as 0 | 1 | 2 | 3, scale = 1, isImage = false)
|
|||
if (!block) return
|
||||
const { position: startPosition } = block
|
||||
|
||||
const worldRenderer = viewer.world as WorldRendererThree
|
||||
|
||||
// Add video with proper positioning
|
||||
worldRenderer.addMedia('test-video', {
|
||||
getThreeJsRendererMethods()?.addMedia('test-video', {
|
||||
position: {
|
||||
x: startPosition.x,
|
||||
y: startPosition.y + 1,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { updateBackground } from './water'
|
|||
|
||||
export default () => {
|
||||
const timeUpdated = () => {
|
||||
assertDefined(viewer)
|
||||
// 0 morning
|
||||
const dayTotal = 24_000
|
||||
const evening = 11_500
|
||||
|
|
@ -37,8 +36,8 @@ export default () => {
|
|||
const colorInt = Math.max(int, 0.1)
|
||||
updateBackground({ r: dayColor.r * colorInt, g: dayColor.g * colorInt, b: dayColor.b * colorInt })
|
||||
if (!options.newVersionsLighting && bot.supportFeature('blockStateId')) {
|
||||
viewer.ambientLight.intensity = Math.max(int, 0.25)
|
||||
viewer.directionalLight.intensity = Math.min(int, 0.5)
|
||||
appViewer.playerState.reactive.ambientLight = Math.max(int, 0.25)
|
||||
appViewer.playerState.reactive.directionalLight = Math.min(int, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/
|
|||
|
||||
Object.defineProperty(window, 'debugSceneChunks', {
|
||||
get () {
|
||||
return (viewer.world as WorldRendererThree).getLoadedChunksRelative?.(bot.entity.position, true)
|
||||
return (window.world as WorldRendererThree)?.getLoadedChunksRelative?.(bot.entity.position, true)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ Object.defineProperty(window, 'debugToggle', {
|
|||
})
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
window.holdingBlock = (viewer.world as WorldRendererThree).holdingBlock
|
||||
window.holdingBlock = (window.world as WorldRendererThree).holdingBlock
|
||||
})
|
||||
|
||||
window.clearStorage = (...keysToKeep: string[]) => {
|
||||
|
|
@ -161,3 +161,50 @@ window.clearStorage = (...keysToKeep: string[]) => {
|
|||
}
|
||||
return `Cleared ${localStorage.length - keysToKeep.length} items from localStorage. Kept: ${keysToKeep.join(', ')}`
|
||||
}
|
||||
|
||||
|
||||
// PERF DEBUG
|
||||
|
||||
// for advanced debugging, use with watch expression
|
||||
|
||||
window.statsPerSecAvg = {}
|
||||
let currentStatsPerSec = {} as Record<string, number[]>
|
||||
const waitingStatsPerSec = {}
|
||||
window.markStart = (label) => {
|
||||
waitingStatsPerSec[label] ??= []
|
||||
waitingStatsPerSec[label][0] = performance.now()
|
||||
}
|
||||
window.markEnd = (label) => {
|
||||
if (!waitingStatsPerSec[label]?.[0]) return
|
||||
currentStatsPerSec[label] ??= []
|
||||
currentStatsPerSec[label].push(performance.now() - waitingStatsPerSec[label][0])
|
||||
delete waitingStatsPerSec[label]
|
||||
}
|
||||
const updateStatsPerSecAvg = () => {
|
||||
window.statsPerSecAvg = Object.fromEntries(Object.entries(currentStatsPerSec).map(([key, value]) => {
|
||||
return [key, {
|
||||
avg: value.reduce((a, b) => a + b, 0) / value.length,
|
||||
count: value.length
|
||||
}]
|
||||
}))
|
||||
currentStatsPerSec = {}
|
||||
}
|
||||
|
||||
|
||||
window.statsPerSec = {}
|
||||
let statsPerSecCurrent = {}
|
||||
let lastReset = performance.now()
|
||||
window.addStatPerSec = (name) => {
|
||||
statsPerSecCurrent[name] ??= 0
|
||||
statsPerSecCurrent[name]++
|
||||
}
|
||||
window.statsPerSecCurrent = statsPerSecCurrent
|
||||
setInterval(() => {
|
||||
window.statsPerSec = { duration: Math.floor(performance.now() - lastReset), ...statsPerSecCurrent, }
|
||||
statsPerSecCurrent = {}
|
||||
window.statsPerSecCurrent = statsPerSecCurrent
|
||||
updateStatsPerSecAvg()
|
||||
lastReset = performance.now()
|
||||
}, 1000)
|
||||
|
||||
// ---
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Entity } from 'prismarine-entity'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import tracker from '@nxg-org/mineflayer-tracker'
|
||||
import { loader as autoJumpPlugin } from '@nxg-org/mineflayer-auto-jump'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { miscUiState } from './globalState'
|
||||
|
||||
|
|
@ -40,23 +41,13 @@ customEvents.on('gameLoaded', () => {
|
|||
bot.loadPlugin(autoJumpPlugin)
|
||||
updateAutoJump()
|
||||
|
||||
// todo cleanup (move to viewer, also shouldnt be used at all)
|
||||
const playerPerAnimation = {} as Record<string, string>
|
||||
const entityData = (e: Entity) => {
|
||||
if (!e.username) return
|
||||
window.debugEntityMetadata ??= {}
|
||||
window.debugEntityMetadata[e.username] = e
|
||||
// todo entity spawn timing issue, check perf
|
||||
const playerObject = viewer.entities.entities[e.id]?.playerObject
|
||||
if (playerObject) {
|
||||
// todo throttle!
|
||||
if (e.type === 'player') {
|
||||
bot.tracker.trackEntity(e)
|
||||
playerObject.backEquipment = e.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape'
|
||||
if (playerObject.cape.map === null) {
|
||||
playerObject.cape.visible = false
|
||||
}
|
||||
// todo (easy, important) elytra flying animation
|
||||
// todo cleanup states
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,57 +67,34 @@ customEvents.on('gameLoaded', () => {
|
|||
const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED
|
||||
const newAnimation = isWalking ? (isSprinting ? 'running' : 'walking') : 'idle'
|
||||
if (newAnimation !== playerPerAnimation[id]) {
|
||||
viewer.entities.playAnimation(e.id, newAnimation)
|
||||
getThreeJsRendererMethods()?.playEntityAnimation(e.id, newAnimation)
|
||||
playerPerAnimation[id] = newAnimation
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot.on('entitySwingArm', (e) => {
|
||||
if (viewer.entities.entities[e.id]?.playerObject) {
|
||||
viewer.entities.playAnimation(e.id, 'oneSwing')
|
||||
}
|
||||
getThreeJsRendererMethods()?.playEntityAnimation(e.id, 'oneSwing')
|
||||
})
|
||||
|
||||
bot._client.on('damage_event', (data) => {
|
||||
const { entityId, sourceTypeId: damage } = data
|
||||
if (viewer.entities.entities[entityId]) {
|
||||
viewer.entities.handleDamageEvent(entityId, damage)
|
||||
}
|
||||
getThreeJsRendererMethods()?.damageEntity(entityId, damage)
|
||||
})
|
||||
|
||||
bot._client.on('entity_status', (data) => {
|
||||
if (versionToNumber(bot.version) >= versionToNumber('1.19.4')) return
|
||||
const { entityId, entityStatus } = data
|
||||
if (entityStatus === 2 && viewer.entities.entities[entityId]) {
|
||||
viewer.entities.handleDamageEvent(entityId, entityStatus)
|
||||
if (entityStatus === 2) {
|
||||
getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus)
|
||||
}
|
||||
})
|
||||
|
||||
const loadedSkinEntityIds = new Set<number>()
|
||||
|
||||
const playerRenderSkin = (e: Entity) => {
|
||||
const mesh = viewer.entities.entities[e.id]
|
||||
if (!mesh) return
|
||||
if (!mesh.playerObject || !options.loadPlayerSkins) return
|
||||
const MAX_DISTANCE_SKIN_LOAD = 128
|
||||
const distance = e.position.distanceTo(bot.entity.position)
|
||||
if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (bot.settings.viewDistance as number) * 16) {
|
||||
if (viewer.entities.entities[e.id]) {
|
||||
if (loadedSkinEntityIds.has(e.id)) return
|
||||
loadedSkinEntityIds.add(e.id)
|
||||
viewer.entities.updatePlayerSkin(e.id, e.username, e.uuid, true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer.entities.addListener('remove', (e) => {
|
||||
loadedSkinEntityIds.delete(e.id)
|
||||
playerPerAnimation[e.id] = ''
|
||||
bot.tracker.stopTrackingEntity(e, true)
|
||||
bot.on('entityGone', (entity) => {
|
||||
bot.tracker.stopTrackingEntity(entity, true)
|
||||
})
|
||||
|
||||
bot.on('entityMoved', (e) => {
|
||||
playerRenderSkin(e)
|
||||
entityData(e)
|
||||
})
|
||||
bot._client.on('entity_velocity', (packet) => {
|
||||
|
|
@ -135,11 +103,6 @@ customEvents.on('gameLoaded', () => {
|
|||
entityData(e)
|
||||
})
|
||||
|
||||
viewer.entities.addListener('add', (e) => {
|
||||
if (!viewer.entities.entities[e.id]) throw new Error('mesh still not loaded')
|
||||
playerRenderSkin(e)
|
||||
})
|
||||
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
if (entity !== bot.entity) {
|
||||
entityData(entity)
|
||||
|
|
@ -150,10 +113,6 @@ customEvents.on('gameLoaded', () => {
|
|||
bot.on('entityUpdate', entityData)
|
||||
bot.on('entityEquip', entityData)
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.entities.setDebugMode(o.showChunkBorders ? 'basic' : 'none')
|
||||
})
|
||||
|
||||
// Texture override from packet properties
|
||||
bot._client.on('player_info', (packet) => {
|
||||
for (const playerEntry of packet.data) {
|
||||
|
|
@ -177,7 +136,7 @@ customEvents.on('gameLoaded', () => {
|
|||
}
|
||||
}
|
||||
// even if not found, still record to cache
|
||||
viewer.entities.updatePlayerSkin(entityId, playerEntry.player?.name, playerEntry.uuid, skinUrl, capeUrl)
|
||||
getThreeJsRendererMethods()?.updatePlayerSkin(entityId, playerEntry.player?.name, playerEntry.uuid, skinUrl, capeUrl)
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
}
|
||||
|
|
|
|||
2
src/globals.d.ts
vendored
2
src/globals.d.ts
vendored
|
|
@ -11,7 +11,7 @@ declare const bot: Omit<import('mineflayer').Bot, 'world' | '_client'> & {
|
|||
}
|
||||
}
|
||||
declare const __type_bot: typeof bot
|
||||
declare const viewer: import('renderer/viewer/lib/viewer').Viewer
|
||||
declare const appViewer: import('./appViewer').AppViewer
|
||||
declare const worldView: import('renderer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined
|
||||
declare const addStatPerSec: (name: string) => void
|
||||
declare const localServer: import('flying-squid/dist/index').FullServer & { options } | undefined
|
||||
|
|
|
|||
267
src/index.ts
267
src/index.ts
|
|
@ -14,17 +14,15 @@ import './mineflayer/java-tester/index'
|
|||
import './external'
|
||||
import './appConfig'
|
||||
import { getServerInfo } from './mineflayer/mc-protocol'
|
||||
import { onGameLoad, renderSlot } from './inventoryWindows'
|
||||
import { GeneralInputItem, RenderItem } from './mineflayer/items'
|
||||
import { onGameLoad } from './inventoryWindows'
|
||||
import initCollisionShapes from './getCollisionInteractionShapes'
|
||||
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
|
||||
import microsoftAuthflow from './microsoftAuthflow'
|
||||
import { Duplex } from 'stream'
|
||||
|
||||
import './scaleInterface'
|
||||
import { initWithRenderer } from './topRightStats'
|
||||
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { options } from './optionsStorage'
|
||||
import './reactUi'
|
||||
import { lockUrl, onBotCreate } from './controls'
|
||||
import './dragndrop'
|
||||
|
|
@ -35,16 +33,11 @@ import downloadAndOpenFile from './downloadAndOpenFile'
|
|||
import fs from 'fs'
|
||||
import net from 'net'
|
||||
import mineflayer from 'mineflayer'
|
||||
import { WorldDataEmitter, Viewer } from 'renderer/viewer'
|
||||
import pathfinder from 'mineflayer-pathfinder'
|
||||
|
||||
import * as THREE from 'three'
|
||||
import MinecraftData from 'minecraft-data'
|
||||
import debug from 'debug'
|
||||
import { defaultsDeep } from 'lodash-es'
|
||||
import initializePacketsReplay from './packetsReplay/packetsReplayLegacy'
|
||||
|
||||
import { initVR } from './vr'
|
||||
import {
|
||||
activeModalStack,
|
||||
activeModalStacks,
|
||||
|
|
@ -60,11 +53,6 @@ import { parseServerAddress } from './parseServerAddress'
|
|||
import { setLoadingScreenStatus } from './appStatus'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
|
||||
import {
|
||||
removePanorama
|
||||
} from './panorama'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
|
||||
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
|
||||
import defaultServerOptions from './defaultLocalServerOptions'
|
||||
import dayCycle from './dayCycle'
|
||||
|
|
@ -79,42 +67,37 @@ import { fsState } from './loadSave'
|
|||
import { watchFov } from './rendererUtils'
|
||||
import { loadInMemorySave } from './react/SingleplayerProvider'
|
||||
|
||||
import { ua } from './react/utils'
|
||||
import { possiblyHandleStateVariable } from './googledrive'
|
||||
import flyingSquidEvents from './flyingSquidEvents'
|
||||
import { hideNotification, notificationProxy, showNotification } from './react/NotificationProvider'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { saveToBrowserMemory } from './react/PauseScreen'
|
||||
import { ViewerWrapper } from 'renderer/viewer/lib/viewerWrapper'
|
||||
import './devReload'
|
||||
import './water'
|
||||
import { ConnectOptions, loadMinecraftData, getVersionAutoSelect, downloadOtherGameData, downloadAllMinecraftData } from './connect'
|
||||
import { ref, subscribe } from 'valtio'
|
||||
import { signInMessageState } from './react/SignInMessageProvider'
|
||||
import { updateAuthenticatedAccountData, updateLoadedServerData, updateServerConnectionHistory } from './react/serversStorage'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import packetsPatcher from './mineflayer/plugins/packetsPatcher'
|
||||
import { mainMenuState } from './react/MainMenuRenderApp'
|
||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
||||
import './mobileShim'
|
||||
import { parseFormattedMessagePacket } from './botUtils'
|
||||
import { getViewerVersionData, getWsProtocolStream, handleCustomChannel } from './viewerConnector'
|
||||
import { getWebsocketStream } from './mineflayer/websocket-core'
|
||||
import { appQueryParams, appQueryParamsArray } from './appParams'
|
||||
import { playerState, PlayerStateManager } from './mineflayer/playerState'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
import { states } from 'minecraft-protocol'
|
||||
import { initMotionTracking } from './react/uiMotion'
|
||||
import { UserError } from './mineflayer/userError'
|
||||
import ping from './mineflayer/plugins/ping'
|
||||
import mouse from './mineflayer/plugins/mouse'
|
||||
import { LocalServer } from './customServer'
|
||||
import { startLocalReplayServer } from './packetsReplay/replayPackets'
|
||||
import { localRelayServerPlugin } from './mineflayer/plugins/packetsRecording'
|
||||
import { createFullScreenProgressReporter } from './core/progressReporter'
|
||||
import { getItemModelName } from './resourcesManager'
|
||||
import { importLargeData } from '../generated/large-data-aliases'
|
||||
import { createConsoleLogProgressReporter, createFullScreenProgressReporter, ProgressReporter } from './core/progressReporter'
|
||||
import { appViewer } from './appViewer'
|
||||
import createGraphicsBackend from 'renderer/viewer/three/graphicsBackend'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
window.beforeRenderFrame = []
|
||||
|
||||
// ACTUAL CODE
|
||||
|
|
@ -128,105 +111,30 @@ initializePacketsReplay()
|
|||
packetsPatcher()
|
||||
onAppLoad()
|
||||
|
||||
// Create three.js context, add to page
|
||||
let renderer: THREE.WebGLRenderer
|
||||
try {
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
powerPreference: options.gpuPreference,
|
||||
preserveDrawingBuffer: true,
|
||||
logarithmicDepthBuffer: true,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new Error(`Failed to create WebGL context, not possible to render (restart browser): ${err.message}`)
|
||||
}
|
||||
|
||||
// renderer.localClippingEnabled = true
|
||||
initWithRenderer(renderer.domElement)
|
||||
const renderWrapper = new ViewerWrapper(renderer.domElement, renderer)
|
||||
renderWrapper.addToPage()
|
||||
watchValue(options, (o) => {
|
||||
renderWrapper.renderInterval = o.frameLimit ? 1000 / o.frameLimit : 0
|
||||
renderWrapper.renderIntervalUnfocused = o.backgroundRendering === '5fps' ? 1000 / 5 : o.backgroundRendering === '20fps' ? 1000 / 20 : undefined
|
||||
})
|
||||
|
||||
const isFirefox = ua.getBrowser().name === 'Firefox'
|
||||
if (isFirefox) {
|
||||
// set custom property
|
||||
document.body.style.setProperty('--thin-if-firefox', 'thin')
|
||||
}
|
||||
|
||||
const isIphone = ua.getDevice().model === 'iPhone' // todo ipad?
|
||||
|
||||
if (isIphone) {
|
||||
document.documentElement.style.setProperty('--hud-bottom-max', '21px') // env-safe-aria-inset-bottom
|
||||
}
|
||||
|
||||
if (appQueryParams.testCrashApp === '2') throw new Error('test')
|
||||
|
||||
// Create viewer
|
||||
const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer, undefined, playerState)
|
||||
window.viewer = viewer
|
||||
Object.defineProperty(window, 'world', {
|
||||
get () {
|
||||
return viewer.world
|
||||
},
|
||||
})
|
||||
// todo unify
|
||||
viewer.entities.getItemUv = (item, specificProps) => {
|
||||
const idOrName = item.itemId ?? item.blockId ?? item.name
|
||||
try {
|
||||
const name = typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
|
||||
if (!name) throw new Error(`Item not found: ${idOrName}`)
|
||||
|
||||
const model = getItemModelName({
|
||||
...item,
|
||||
name,
|
||||
} as GeneralInputItem, specificProps)
|
||||
|
||||
const renderInfo = renderSlot({
|
||||
modelName: model,
|
||||
}, false, true)
|
||||
|
||||
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
||||
|
||||
const textureThree = renderInfo.texture === 'blocks' ? viewer.world.material.map! : viewer.entities.itemsTexture!
|
||||
const img = textureThree.image
|
||||
|
||||
if (renderInfo.blockData) {
|
||||
return {
|
||||
resolvedModel: renderInfo.blockData.resolvedModel,
|
||||
modelName: renderInfo.modelName!
|
||||
}
|
||||
const loadBackend = () => {
|
||||
appViewer.loadBackend(createGraphicsBackend)
|
||||
}
|
||||
window.loadBackend = loadBackend
|
||||
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
||||
const unsub = subscribeKey(miscUiState, 'fsReady', () => {
|
||||
if (miscUiState.fsReady) {
|
||||
// don't do it earlier to load fs and display menu faster
|
||||
loadBackend()
|
||||
unsub()
|
||||
}
|
||||
if (renderInfo.slice) {
|
||||
// Get slice coordinates from either block or item texture
|
||||
const [x, y, w, h] = renderInfo.slice
|
||||
const [u, v, su, sv] = [x / img.width, y / img.height, (w / img.width), (h / img.height)]
|
||||
return {
|
||||
u, v, su, sv,
|
||||
texture: textureThree,
|
||||
modelName: renderInfo.modelName
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid render info for item ${name}`)
|
||||
} catch (err) {
|
||||
reportError?.(err)
|
||||
// Return default UV coordinates for missing texture
|
||||
return {
|
||||
u: 0,
|
||||
v: 0,
|
||||
su: 16 / viewer.world.material.map!.image.width,
|
||||
sv: 16 / viewer.world.material.map!.image.width,
|
||||
texture: viewer.world.material.map!
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
loadBackend()
|
||||
}
|
||||
|
||||
viewer.entities.entitiesOptions = {
|
||||
fontFamily: 'mojangles'
|
||||
const animLoop = () => {
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
requestAnimationFrame(animLoop)
|
||||
}
|
||||
requestAnimationFrame(animLoop)
|
||||
|
||||
watchOptionsAfterViewerInit()
|
||||
|
||||
function hideCurrentScreens () {
|
||||
|
|
@ -252,30 +160,21 @@ function listenGlobalEvents () {
|
|||
})
|
||||
}
|
||||
|
||||
let listeners = [] as Array<{ target, event, callback }>
|
||||
let cleanupFunctions = [] as Array<() => void>
|
||||
// only for dom listeners (no removeAllListeners)
|
||||
// todo refactor them out of connect fn instead
|
||||
const registerListener: import('./utilsTs').RegisterListener = (target, event, callback) => {
|
||||
target.addEventListener(event, callback)
|
||||
listeners.push({ target, event, callback })
|
||||
}
|
||||
const removeAllListeners = () => {
|
||||
for (const { target, event, callback } of listeners) {
|
||||
target.removeEventListener(event, callback)
|
||||
}
|
||||
for (const cleanupFunction of cleanupFunctions) {
|
||||
cleanupFunction()
|
||||
}
|
||||
cleanupFunctions = []
|
||||
listeners = []
|
||||
}
|
||||
|
||||
export async function connect (connectOptions: ConnectOptions) {
|
||||
if (miscUiState.gameLoaded) return
|
||||
|
||||
if (sessionStorage.delayLoadUntilFocus) {
|
||||
await new Promise(resolve => {
|
||||
if (document.hasFocus()) {
|
||||
resolve(undefined)
|
||||
} else {
|
||||
window.addEventListener('focus', resolve)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
miscUiState.hasErrors = false
|
||||
lastConnectOptions.value = connectOptions
|
||||
removePanorama()
|
||||
|
||||
const { singleplayer } = connectOptions
|
||||
const p2pMultiplayer = !!connectOptions.peerId
|
||||
|
|
@ -318,12 +217,11 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
const destroyAll = () => {
|
||||
if (ended) return
|
||||
ended = true
|
||||
viewer.resetAll()
|
||||
progress.end()
|
||||
// dont reset viewer so we can still do debugging
|
||||
localServer = window.localServer = window.server = undefined
|
||||
gameAdditionalState.viewerConnection = false
|
||||
|
||||
renderWrapper.postRender = () => { }
|
||||
if (bot) {
|
||||
bot.end()
|
||||
// ensure mineflayer plugins receive this event for cleanup
|
||||
|
|
@ -337,7 +235,6 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
}
|
||||
resetStateAfterDisconnect()
|
||||
cleanFs()
|
||||
removeAllListeners()
|
||||
}
|
||||
const cleanFs = () => {
|
||||
if (singleplayer && !fsState.inMemorySave) {
|
||||
|
|
@ -411,13 +308,20 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
const downloadMcData = async (version: string) => {
|
||||
if (dataDownloaded) return
|
||||
dataDownloaded = true
|
||||
appViewer.resourcesManager.currentConfig = { version, texturesVersion: options.useVersionsTextures || undefined }
|
||||
|
||||
await progress.executeWithMessage(
|
||||
'Loading minecraft data',
|
||||
async () => {
|
||||
await appViewer.resourcesManager.loadSourceData(version)
|
||||
}
|
||||
)
|
||||
|
||||
await progress.executeWithMessage(
|
||||
'Applying user-installed resource pack',
|
||||
async () => {
|
||||
await loadMinecraftData(version)
|
||||
try {
|
||||
await resourcepackReload(version)
|
||||
await resourcepackReload(true)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const doContinue = confirm('Failed to apply texture pack. See errors in the console. Continue?')
|
||||
|
|
@ -429,11 +333,9 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
)
|
||||
|
||||
await progress.executeWithMessage(
|
||||
'Loading minecraft models',
|
||||
'Preparing textures',
|
||||
async () => {
|
||||
viewer.world.blockstatesModels = await importLargeData('blockStatesModels')
|
||||
void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
miscUiState.loadedDataVersion = version
|
||||
await appViewer.resourcesManager.updateAssetsData({})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -749,14 +651,26 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
setLoadingScreenStatus('Loading world')
|
||||
})
|
||||
|
||||
const start = Date.now()
|
||||
const loadStart = Date.now()
|
||||
let worldWasReady = false
|
||||
void viewer.world.renderUpdateEmitter.on('update', () => {
|
||||
// todo might not emit as servers simply don't send chunk if it's empty
|
||||
if (!viewer.world.allChunksFinished || worldWasReady) return
|
||||
worldWasReady = true
|
||||
console.log('All chunks done and ready! Time from renderer open to ready', (Date.now() - start) / 1000, 's')
|
||||
viewer.render() // ensure the last state is rendered
|
||||
const waitForChunksToLoad = async (progress?: ProgressReporter) => {
|
||||
await new Promise<void>(resolve => {
|
||||
const unsub = subscribe(appViewer.rendererState, () => {
|
||||
if (worldWasReady) return
|
||||
if (appViewer.rendererState.world.allChunksLoaded) {
|
||||
worldWasReady = true
|
||||
resolve()
|
||||
unsub()
|
||||
} else {
|
||||
const perc = Math.round(appViewer.rendererState.world.chunksLoaded.length / appViewer.rendererState.world.chunksTotalNumber * 100)
|
||||
progress?.reportProgress('chunks', perc / 100)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
void waitForChunksToLoad().then(() => {
|
||||
console.log('All chunks done and ready! Time from renderer connect to ready', (Date.now() - loadStart) / 1000, 's')
|
||||
document.dispatchEvent(new Event('cypress-world-ready'))
|
||||
})
|
||||
|
||||
|
|
@ -778,7 +692,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
|
||||
playerState.onlineMode = !!connectOptions.authenticatedAccount
|
||||
|
||||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
progress.setMessage('Placing blocks (starting viewer)')
|
||||
if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) {
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !Object.keys(window.debugQueryParams).length) {
|
||||
|
|
@ -793,38 +707,25 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
bot.chat(`/login ${connectOptions.autoLoginPassword}`)
|
||||
}
|
||||
|
||||
|
||||
console.log('bot spawned - starting viewer')
|
||||
appViewer.startWorld(bot.world, renderDistance)
|
||||
appViewer.worldView!.listenToBot(bot)
|
||||
|
||||
const center = bot.entity.position
|
||||
|
||||
const worldView = window.worldView = new WorldDataEmitter(bot.world, renderDistance, center)
|
||||
watchOptionsAfterWorldViewInit()
|
||||
|
||||
void initVR()
|
||||
initMotionTracking()
|
||||
|
||||
renderWrapper.postRender = () => {
|
||||
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
|
||||
}
|
||||
|
||||
// Link WorldDataEmitter and Viewer
|
||||
viewer.connect(worldView)
|
||||
worldView.listenToBot(bot)
|
||||
void worldView.init(bot.entity.position)
|
||||
|
||||
dayCycle()
|
||||
|
||||
// Bot position callback
|
||||
function botPosition () {
|
||||
viewer.world.lastCamUpdate = Date.now()
|
||||
appViewer.lastCamUpdate = Date.now()
|
||||
// this might cause lag, but not sure
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
void worldView.updatePosition(bot.entity.position)
|
||||
appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
void appViewer.worldView?.updatePosition(bot.entity.position)
|
||||
}
|
||||
bot.on('move', botPosition)
|
||||
botPosition()
|
||||
|
||||
setLoadingScreenStatus('Setting callbacks')
|
||||
progress.setMessage('Setting callbacks')
|
||||
|
||||
onGameLoad(() => {})
|
||||
|
||||
|
|
@ -833,7 +734,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
const waitForChunks = async () => {
|
||||
if (appQueryParams.sp === '1') return //todo
|
||||
const waitForChunks = options.waitForChunksRender === 'sp-only' ? !!singleplayer : options.waitForChunksRender
|
||||
if (viewer.world.allChunksFinished || !waitForChunks) {
|
||||
if (!appViewer.backend || appViewer.rendererState.world.allChunksLoaded || !waitForChunks) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -841,19 +742,7 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
'Loading chunks',
|
||||
'chunks',
|
||||
async () => {
|
||||
await new Promise<void>(resolve => {
|
||||
let wasFinished = false
|
||||
void viewer.world.renderUpdateEmitter.on('update', () => {
|
||||
if (wasFinished) return
|
||||
if (viewer.world.allChunksFinished) {
|
||||
wasFinished = true
|
||||
resolve()
|
||||
} else {
|
||||
const perc = Math.round(Object.keys(viewer.world.finishedChunks).length / viewer.world.chunksLength * 100)
|
||||
progress.reportProgress('chunks', perc / 100)
|
||||
}
|
||||
})
|
||||
})
|
||||
await waitForChunksToLoad(progress)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { RecipeItem } from 'minecraft-data'
|
|||
import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||
import { splitEvery, equals } from 'rambda'
|
||||
import PItem, { Item } from 'prismarine-item'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
|
|
@ -20,8 +20,7 @@ import { displayClientChat } from './botUtils'
|
|||
import { currentScaling } from './scaleInterface'
|
||||
import { getItemDescription } from './itemsDescriptions'
|
||||
import { MessageFormatPart } from './chatUtils'
|
||||
import { GeneralInputItem, getItemMetadata, getItemNameRaw, RenderItem } from './mineflayer/items'
|
||||
import { getItemModelName } from './resourcesManager'
|
||||
import { GeneralInputItem, getItemMetadata, getItemModelName, getItemNameRaw, RenderItem } from './mineflayer/items'
|
||||
|
||||
const loadedImagesCache = new Map<string, HTMLImageElement>()
|
||||
const cleanLoadedImagesCache = () => {
|
||||
|
|
@ -46,7 +45,7 @@ export const onGameLoad = (onLoad) => {
|
|||
version = bot.version
|
||||
|
||||
const checkIfLoaded = () => {
|
||||
if (!viewer.world.itemsAtlasParser) return
|
||||
if (!appViewer.resourcesManager.currentResources?.itemsAtlasParser) return
|
||||
if (!allImagesLoadedState.value) {
|
||||
onLoad?.()
|
||||
}
|
||||
|
|
@ -55,7 +54,8 @@ export const onGameLoad = (onLoad) => {
|
|||
allImagesLoadedState.value = true
|
||||
}, 0)
|
||||
}
|
||||
viewer.world.renderUpdateEmitter.on('textureDownloaded', checkIfLoaded)
|
||||
appViewer.resourcesManager.on('assetsTexturesUpdated', checkIfLoaded)
|
||||
appViewer.resourcesManager.on('assetsInventoryReady', checkIfLoaded)
|
||||
checkIfLoaded()
|
||||
|
||||
PrismarineItem = PItem(version)
|
||||
|
|
@ -137,11 +137,10 @@ export const onGameLoad = (onLoad) => {
|
|||
}
|
||||
|
||||
const getImageSrc = (path): string | HTMLImageElement => {
|
||||
assertDefined(viewer)
|
||||
switch (path) {
|
||||
case 'gui/container/inventory': return appReplacableResources.latest_gui_container_inventory.content
|
||||
case 'blocks': return viewer.world.blocksAtlasParser!.latestImage
|
||||
case 'items': return viewer.world.itemsAtlasParser!.latestImage
|
||||
case 'blocks': return appViewer.resourcesManager.currentResources!.blocksAtlasParser.latestImage
|
||||
case 'items': return appViewer.resourcesManager.currentResources!.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
|
||||
|
|
@ -223,13 +222,13 @@ export const renderSlot = (model: ResolvedItemModelRender, debugIsQuickbar = fal
|
|||
}
|
||||
|
||||
try {
|
||||
assertDefined(viewer.world.itemsRenderer)
|
||||
assertDefined(appViewer.resourcesManager.currentResources?.itemsRenderer)
|
||||
itemTexture =
|
||||
viewer.world.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport)
|
||||
?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport)
|
||||
?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
} catch (err) {
|
||||
inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
|
||||
itemTexture = viewer.world.itemsRenderer!.getItemTexture('block/errored')!
|
||||
itemTexture = appViewer.resourcesManager.currentResources!.itemsRenderer.getItemTexture('block/errored')!
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import path from 'path'
|
|||
import * as nbt from 'prismarine-nbt'
|
||||
import { proxy } from 'valtio'
|
||||
import { gzip } from 'node-gzip'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { nameToMcOfflineUUID, disconnect } from './flyingSquidUtils'
|
||||
import { existsViaStats, forceCachedDataPaths, forceRedirectPaths, mkdirRecursive } from './browserfs'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
class CameraShake {
|
||||
private rollAngle = 0
|
||||
private get damageRollAmount () { return 5 }
|
||||
|
|
@ -10,10 +8,10 @@ class CameraShake {
|
|||
this.rollAngle = 0
|
||||
}
|
||||
|
||||
shakeFromDamage (yaw?: number) {
|
||||
shakeFromDamage () {
|
||||
// Add roll animation
|
||||
const startRoll = this.rollAngle
|
||||
const targetRoll = startRoll + (yaw ?? (Math.random() < 0.5 ? -1 : 1)) * this.damageRollAmount
|
||||
const targetRoll = startRoll + (Math.random() < 0.5 ? -1 : 1) * this.damageRollAmount
|
||||
|
||||
this.rollAnimation = {
|
||||
startTime: performance.now(),
|
||||
|
|
@ -52,18 +50,8 @@ class CameraShake {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply roll in camera's local space to maintain consistent left/right roll
|
||||
const { camera } = viewer
|
||||
const rollQuat = new THREE.Quaternion()
|
||||
rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
|
||||
|
||||
// Get camera's current rotation
|
||||
const camQuat = new THREE.Quaternion()
|
||||
camera.getWorldQuaternion(camQuat)
|
||||
|
||||
// Apply roll after camera rotation
|
||||
const finalQuat = camQuat.multiply(rollQuat)
|
||||
camera.setRotationFromQuaternion(finalQuat)
|
||||
// Apply roll to camera
|
||||
appViewer.backend?.setRoll(this.rollAngle)
|
||||
}
|
||||
|
||||
private easeOut (t: number): number {
|
||||
|
|
@ -77,6 +65,10 @@ class CameraShake {
|
|||
|
||||
let cameraShake: CameraShake
|
||||
|
||||
customEvents.on('hurtAnimation', () => {
|
||||
cameraShake.shakeFromDamage()
|
||||
})
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
if (!cameraShake) {
|
||||
cameraShake = new CameraShake()
|
||||
|
|
@ -85,8 +77,8 @@ customEvents.on('mineflayerBotCreated', () => {
|
|||
})
|
||||
}
|
||||
|
||||
customEvents.on('hurtAnimation', (yaw) => {
|
||||
cameraShake.shakeFromDamage(yaw)
|
||||
customEvents.on('hurtAnimation', () => {
|
||||
cameraShake.shakeFromDamage()
|
||||
})
|
||||
|
||||
bot._client.on('hurt_animation', ({ entityId, yaw }) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +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 { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
import { MessageFormatPart } from '../chatUtils'
|
||||
import { playerState } from './playerState'
|
||||
|
||||
type RenderSlotComponent = {
|
||||
type: string,
|
||||
|
|
@ -86,3 +89,22 @@ export const getItemNameRaw = (item: Pick<import('prismarine-item').Item, 'nbt'>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties) => {
|
||||
let itemModelName = item.name
|
||||
const { customModel } = getItemMetadata(item)
|
||||
if (customModel) {
|
||||
itemModelName = customModel
|
||||
}
|
||||
|
||||
const itemSelector = playerState.getItemSelector({
|
||||
...specificProps
|
||||
})
|
||||
const modelFromDef = getItemDefinition(appViewer.resourcesManager.itemsDefinitionsStore, {
|
||||
name: itemModelName,
|
||||
version: appViewer.resourcesManager.currentResources!.version,
|
||||
properties: itemSelector
|
||||
})?.model
|
||||
const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName
|
||||
return model
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { mapDownloader } from 'mineflayer-item-map-downloader'
|
||||
import { setImageConverter } from 'mineflayer-item-map-downloader/lib/util'
|
||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||
|
||||
setImageConverter((buf: Uint8Array) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
|
|
@ -17,7 +18,7 @@ customEvents.on('mineflayerBotCreated', () => {
|
|||
bot.on('login', () => {
|
||||
bot.loadPlugin(mapDownloader)
|
||||
bot.mapDownloader.on('new_map', ({ png, id }) => {
|
||||
viewer.entities.updateMap(id, png)
|
||||
getThreeJsRendererMethods()?.updateMap(id, png)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { IPlayerState, ItemSpecificContextProperties, MovementState, PlayerStateEvents } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { BasePlayerState, IPlayerState, ItemSpecificContextProperties, MovementState, PlayerStateEvents } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { HandItemBlock } from 'renderer/viewer/lib/holdingBlock'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
|
|
@ -29,9 +29,7 @@ export class PlayerStateManager implements IPlayerState {
|
|||
return bot.player?.username ?? ''
|
||||
}
|
||||
|
||||
reactive = proxy({
|
||||
playerSkin: undefined as string | undefined,
|
||||
})
|
||||
reactive: IPlayerState['reactive'] = new BasePlayerState().reactive
|
||||
|
||||
static getInstance (): PlayerStateManager {
|
||||
if (!this.instance) {
|
||||
|
|
@ -40,7 +38,7 @@ export class PlayerStateManager implements IPlayerState {
|
|||
return this.instance
|
||||
}
|
||||
|
||||
private constructor () {
|
||||
constructor () {
|
||||
this.updateState = this.updateState.bind(this)
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
this.ready = false
|
||||
|
|
@ -70,6 +68,11 @@ export class PlayerStateManager implements IPlayerState {
|
|||
// Initial held items setup
|
||||
this.updateHeldItem(false)
|
||||
this.updateHeldItem(true)
|
||||
|
||||
bot.on('game', () => {
|
||||
this.reactive.gameMode = bot.game.gameMode
|
||||
})
|
||||
this.reactive.gameMode = bot.game?.gameMode
|
||||
}
|
||||
|
||||
// #region Movement and Physics State
|
||||
|
|
@ -133,6 +136,10 @@ export class PlayerStateManager implements IPlayerState {
|
|||
isSprinting (): boolean {
|
||||
return gameAdditionalState.isSprinting
|
||||
}
|
||||
|
||||
getPosition (): Vec3 {
|
||||
return bot.player?.entity.position ?? new Vec3(0, 0, 0)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Held Item State
|
||||
|
|
|
|||
|
|
@ -1,142 +1,30 @@
|
|||
import { createMouse } from 'mineflayer-mouse'
|
||||
import * as THREE from 'three'
|
||||
import { Bot } from 'mineflayer'
|
||||
import { Block } from 'prismarine-block'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { disposeObject } from 'renderer/viewer/lib/threeJsUtils'
|
||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||
import { isGameActive, showModal } from '../../globalState'
|
||||
|
||||
// wouldn't better to create atlas instead?
|
||||
import destroyStage0 from '../../../assets/destroy_stage_0.png'
|
||||
import destroyStage1 from '../../../assets/destroy_stage_1.png'
|
||||
import destroyStage2 from '../../../assets/destroy_stage_2.png'
|
||||
import destroyStage3 from '../../../assets/destroy_stage_3.png'
|
||||
import destroyStage4 from '../../../assets/destroy_stage_4.png'
|
||||
import destroyStage5 from '../../../assets/destroy_stage_5.png'
|
||||
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 { options } from '../../optionsStorage'
|
||||
import { isCypress } from '../../standaloneUtils'
|
||||
import { playerState } from '../playerState'
|
||||
import { sendVideoInteraction, videoCursorInteraction } from '../../customChannels'
|
||||
|
||||
function createDisplayManager (bot: Bot, scene: THREE.Scene, renderer: THREE.WebGLRenderer) {
|
||||
// State
|
||||
const state = {
|
||||
blockBreakMesh: null as THREE.Mesh | null,
|
||||
breakTextures: [] as THREE.Texture[],
|
||||
}
|
||||
|
||||
// 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
|
||||
state.breakTextures.push(texture)
|
||||
}
|
||||
|
||||
const breakMaterial = new THREE.MeshBasicMaterial({
|
||||
transparent: true,
|
||||
blending: THREE.MultiplyBlending,
|
||||
alphaTest: 0.5,
|
||||
})
|
||||
state.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial)
|
||||
state.blockBreakMesh.visible = false
|
||||
state.blockBreakMesh.renderOrder = 999
|
||||
state.blockBreakMesh.name = 'blockBreakMesh'
|
||||
scene.add(state.blockBreakMesh)
|
||||
|
||||
// Update functions
|
||||
function updateLineMaterial () {
|
||||
const inCreative = bot.game.gameMode === 'creative'
|
||||
const pixelRatio = viewer.renderer.getPixelRatio()
|
||||
|
||||
viewer.world.threejsCursorLineMaterial = new LineMaterial({
|
||||
color: (() => {
|
||||
switch (options.highlightBlockColor) {
|
||||
case 'blue':
|
||||
return 0x40_80_ff
|
||||
case 'classic':
|
||||
return 0x00_00_00
|
||||
default:
|
||||
return inCreative ? 0x40_80_ff : 0x00_00_00
|
||||
}
|
||||
})(),
|
||||
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
|
||||
// dashed: true,
|
||||
// dashSize: 5,
|
||||
})
|
||||
}
|
||||
|
||||
function updateDisplay () {
|
||||
if (viewer.world.threejsCursorLineMaterial) {
|
||||
const { renderer } = viewer
|
||||
viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
|
||||
viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750
|
||||
}
|
||||
}
|
||||
beforeRenderFrame.push(updateDisplay)
|
||||
|
||||
// Update cursor line material on game mode change
|
||||
bot.on('game', updateLineMaterial)
|
||||
// Update material when highlight color setting changes
|
||||
subscribeKey(options, 'highlightBlockColor', updateLineMaterial)
|
||||
|
||||
function updateBreakAnimation (block: Block | undefined, stage: number | null) {
|
||||
hideBreakAnimation()
|
||||
if (!state.blockBreakMesh) return // todo
|
||||
if (stage === null || !block) return
|
||||
|
||||
const mergedShape = bot.mouse.getMergedCursorShape(block)
|
||||
if (!mergedShape) return
|
||||
const { position, width, height, depth } = bot.mouse.getDataFromShape(mergedShape)
|
||||
state.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001)
|
||||
position.add(block.position)
|
||||
state.blockBreakMesh.position.set(position.x, position.y, position.z)
|
||||
state.blockBreakMesh.visible = true
|
||||
|
||||
//@ts-expect-error
|
||||
state.blockBreakMesh.material.map = state.breakTextures[stage] ?? state.breakTextures.at(-1)
|
||||
//@ts-expect-error
|
||||
state.blockBreakMesh.material.needsUpdate = true
|
||||
}
|
||||
|
||||
function hideBreakAnimation () {
|
||||
if (state.blockBreakMesh) {
|
||||
state.blockBreakMesh.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
function updateCursorBlock (data?: { block: Block }) {
|
||||
function cursorBlockDisplay (bot: Bot) {
|
||||
const updateCursorBlock = (data?: { block: Block }) => {
|
||||
if (!data?.block) {
|
||||
viewer.world.setHighlightCursorBlock(null)
|
||||
getThreeJsRendererMethods()?.setHighlightCursorBlock(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { block } = data
|
||||
viewer.world.setHighlightCursorBlock(block.position, bot.mouse.getBlockCursorShapes(block).map(shape => {
|
||||
getThreeJsRendererMethods()?.setHighlightCursorBlock(block.position, bot.mouse.getBlockCursorShapes(block).map(shape => {
|
||||
return bot.mouse.getDataFromShape(shape)
|
||||
}))
|
||||
}
|
||||
|
||||
bot.on('highlightCursorBlock', updateCursorBlock)
|
||||
|
||||
bot.on('blockBreakProgressStage', updateBreakAnimation)
|
||||
|
||||
bot.on('end', () => {
|
||||
disposeObject(state.blockBreakMesh!, true)
|
||||
scene.remove(state.blockBreakMesh!)
|
||||
viewer.world.setHighlightCursorBlock(null)
|
||||
bot.on('blockBreakProgressStage', (block, stage) => {
|
||||
getThreeJsRendererMethods()?.updateBreakAnimation(block, stage)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +32,7 @@ export default (bot: Bot) => {
|
|||
bot.loadPlugin(createMouse({}))
|
||||
|
||||
domListeners(bot)
|
||||
createDisplayManager(bot, viewer.scene, viewer.renderer)
|
||||
cursorBlockDisplay(bot)
|
||||
|
||||
otherListeners()
|
||||
}
|
||||
|
|
@ -159,11 +47,11 @@ const otherListeners = () => {
|
|||
})
|
||||
|
||||
bot.on('botArmSwingStart', (hand) => {
|
||||
viewer.world.changeHandSwingingState(true, hand === 'left')
|
||||
getThreeJsRendererMethods()?.changeHandSwingingState(true, hand === 'left')
|
||||
})
|
||||
|
||||
bot.on('botArmSwingEnd', (hand) => {
|
||||
viewer.world.changeHandSwingingState(false, hand === 'left')
|
||||
getThreeJsRendererMethods()?.changeHandSwingingState(false, hand === 'left')
|
||||
})
|
||||
|
||||
bot.on('startUsingItem', (item, slot, isOffhand, duration) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
|
||||
export default () => {
|
||||
let i = 0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
|
||||
type IdMap = Record<string, number>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import Slider from './react/Slider'
|
|||
import { getScreenRefreshRate } from './utils'
|
||||
import { setLoadingScreenStatus } from './appStatus'
|
||||
import { openFilePicker, resetLocalStorage } from './browserfs'
|
||||
import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack'
|
||||
import { completeResourcepackPackInstall, getResourcePackNames, resourcepackReload, resourcePackState, uninstallResourcePack } from './resourcePack'
|
||||
import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy'
|
||||
import { showInputsModal, showOptionsModal } from './react/SelectOption'
|
||||
import supportedVersions from './supportedVersions.mjs'
|
||||
|
|
@ -181,6 +181,7 @@ export const guiOptionsScheme: {
|
|||
if (!choice) return
|
||||
if (choice === 'Disable') {
|
||||
options.enabledResourcepack = null
|
||||
await resourcepackReload()
|
||||
return
|
||||
}
|
||||
if (choice === 'Enable') {
|
||||
|
|
|
|||
198
src/panorama.ts
198
src/panorama.ts
|
|
@ -1,198 +0,0 @@
|
|||
//@ts-check
|
||||
|
||||
import { join } from 'path'
|
||||
import * as THREE from 'three'
|
||||
import { EntityMesh } from 'renderer/viewer/lib/entity/EntityMesh'
|
||||
import { WorldDataEmitter } from 'renderer/viewer'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { getSyncWorld } from 'renderer/playground/shared'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { miscUiState } from './globalState'
|
||||
import { loadMinecraftData } from './connect'
|
||||
|
||||
let panoramaCubeMap
|
||||
let shouldDisplayPanorama = true
|
||||
|
||||
const panoramaFiles = [
|
||||
'panorama_3.png', // right (+x)
|
||||
'panorama_1.png', // left (-x)
|
||||
'panorama_4.png', // top (+y)
|
||||
'panorama_5.png', // bottom (-y)
|
||||
'panorama_0.png', // front (+z)
|
||||
'panorama_2.png', // back (-z)
|
||||
]
|
||||
|
||||
let unloadPanoramaCallbacks = [] as Array<() => void>
|
||||
|
||||
// Menu panorama background
|
||||
// TODO-low use abort controller
|
||||
export async function addPanoramaCubeMap () {
|
||||
if (panoramaCubeMap || miscUiState.loadedDataVersion || options.disableAssets) return
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0) // wait for viewer to be initialized
|
||||
})
|
||||
viewer.camera.fov = 85
|
||||
if (!shouldDisplayPanorama) return
|
||||
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
||||
void initDemoWorld()
|
||||
return
|
||||
}
|
||||
|
||||
let time = 0
|
||||
viewer.camera.near = 0.05
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.camera.position.set(0, 0, 0)
|
||||
viewer.camera.rotation.set(0, 0, 0)
|
||||
const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
|
||||
|
||||
const loader = new THREE.TextureLoader()
|
||||
const panorMaterials = [] as THREE.MeshBasicMaterial[]
|
||||
for (const file of panoramaFiles) {
|
||||
const texture = loader.load(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 // Changed from RepeatWrapping
|
||||
texture.wrapT = THREE.ClampToEdgeWrapping // Changed from RepeatWrapping
|
||||
texture.minFilter = THREE.LinearFilter
|
||||
texture.magFilter = THREE.LinearFilter
|
||||
|
||||
panorMaterials.push(new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false
|
||||
}))
|
||||
}
|
||||
|
||||
if (!shouldDisplayPanorama) return
|
||||
|
||||
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
|
||||
|
||||
panoramaBox.onBeforeRender = () => {
|
||||
time += 0.01
|
||||
panoramaBox.rotation.y = Math.PI + time * 0.01
|
||||
panoramaBox.rotation.z = Math.sin(-time * 0.001) * 0.001
|
||||
}
|
||||
|
||||
const group = new THREE.Object3D()
|
||||
group.add(panoramaBox)
|
||||
|
||||
// should be rewritten entirely
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const m = new EntityMesh('1.16.4', 'squid', viewer.world).mesh
|
||||
m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17)
|
||||
m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX')
|
||||
const v = Math.random() * 0.01
|
||||
m.children[0].onBeforeRender = () => {
|
||||
m.rotation.y += v
|
||||
m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2
|
||||
}
|
||||
group.add(m)
|
||||
}
|
||||
|
||||
viewer.scene.add(group)
|
||||
panoramaCubeMap = group
|
||||
}
|
||||
|
||||
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
||||
subscribeKey(miscUiState, 'fsReady', () => {
|
||||
if (miscUiState.fsReady) {
|
||||
// don't do it earlier to load fs and display menu faster
|
||||
void addPanoramaCubeMap()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
void addPanoramaCubeMap()
|
||||
}
|
||||
|
||||
export function removePanorama () {
|
||||
for (const unloadPanoramaCallback of unloadPanoramaCallbacks) {
|
||||
unloadPanoramaCallback()
|
||||
}
|
||||
unloadPanoramaCallbacks = []
|
||||
viewer.camera.fov = options.fov
|
||||
shouldDisplayPanorama = false
|
||||
if (!panoramaCubeMap) return
|
||||
viewer.camera.near = 0.1
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.scene.remove(panoramaCubeMap)
|
||||
panoramaCubeMap = null
|
||||
}
|
||||
|
||||
const initDemoWorld = async () => {
|
||||
const abortController = new AbortController()
|
||||
unloadPanoramaCallbacks.push(() => {
|
||||
abortController.abort()
|
||||
})
|
||||
const version = '1.21.4'
|
||||
console.time(`load ${version} mc-data`)
|
||||
await loadMinecraftData(version, true)
|
||||
console.timeEnd(`load ${version} mc-data`)
|
||||
if (abortController.signal.aborted) return
|
||||
console.time('load scene')
|
||||
const world = getSyncWorld(version)
|
||||
const PrismarineBlock = require('prismarine-block')
|
||||
const Block = PrismarineBlock(version)
|
||||
const fullBlocks = loadedData.blocksArray.filter(block => {
|
||||
// if (block.name.includes('leaves')) return false
|
||||
if (/* !block.name.includes('wool') && */!block.name.includes('stained_glass')/* && !block.name.includes('terracotta') */) return false
|
||||
const b = Block.fromStateId(block.defaultState, 0)
|
||||
if (b.shapes?.length !== 1) return false
|
||||
const shape = b.shapes[0]
|
||||
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
||||
})
|
||||
const Z = -15
|
||||
const sizeX = 100
|
||||
const sizeY = 100
|
||||
for (let x = -sizeX; x < sizeX; x++) {
|
||||
for (let y = -sizeY; y < sizeY; y++) {
|
||||
const block = fullBlocks[Math.floor(Math.random() * fullBlocks.length)]
|
||||
world.setBlockStateId(new Vec3(x, y, Z), block.defaultState)
|
||||
}
|
||||
}
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.camera.position.set(0.5, sizeY / 2 + 0.5, 0.5)
|
||||
viewer.camera.rotation.set(0, 0, 0)
|
||||
const initPos = new Vec3(...viewer.camera.position.toArray())
|
||||
const worldView = new WorldDataEmitter(world, 2, initPos)
|
||||
// worldView.addWaitTime = 0
|
||||
await viewer.world.setVersion(version)
|
||||
if (abortController.signal.aborted) return
|
||||
viewer.connect(worldView)
|
||||
void worldView.init(initPos)
|
||||
await viewer.world.waitForChunksToRender()
|
||||
if (abortController.signal.aborted) return
|
||||
// add small camera rotation to side on mouse move depending on absolute position of the cursor
|
||||
const { camera } = viewer
|
||||
const initX = camera.position.x
|
||||
const initY = camera.position.y
|
||||
let prevTwin: tweenJs.Tween<THREE.Vector3> | undefined
|
||||
document.body.addEventListener('pointermove', (e) => {
|
||||
if (e.pointerType !== 'mouse') return
|
||||
const pos = new THREE.Vector2(e.clientX, e.clientY)
|
||||
const SCALE = 0.2
|
||||
/* -0.5 - 0.5 */
|
||||
const xRel = pos.x / window.innerWidth - 0.5
|
||||
const yRel = -(pos.y / window.innerHeight - 0.5)
|
||||
prevTwin?.stop()
|
||||
const to = {
|
||||
x: initX + (xRel * SCALE),
|
||||
y: initY + (yRel * SCALE)
|
||||
}
|
||||
prevTwin = new tweenJs.Tween(camera.position).to(to, 0) // todo use the number depending on diff // todo use the number depending on diff
|
||||
// prevTwin.easing(tweenJs.Easing.Exponential.InOut)
|
||||
prevTwin.start()
|
||||
camera.updateProjectionMatrix()
|
||||
}, {
|
||||
signal: abortController.signal
|
||||
})
|
||||
|
||||
console.timeEnd('load scene')
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ export default ({
|
|||
children
|
||||
}) => {
|
||||
const [loadingDotIndex, setLoadingDotIndex] = useState(0)
|
||||
const lockConnect = appQueryParams.lockConnect === 'true'
|
||||
|
||||
useEffect(() => {
|
||||
const statusRunner = async () => {
|
||||
|
|
@ -84,7 +83,7 @@ export default ({
|
|||
>
|
||||
<b>Reset App (recommended)</b>
|
||||
</Button>
|
||||
{!lockConnect && backAction && <Button label="Back" onClick={backAction} />}
|
||||
{backAction && <Button label="Back" onClick={backAction} />}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ConnectOptions } from '../connect'
|
|||
import { downloadPacketsReplay, packetsRecordingState, replayLogger } from '../packetsReplay/packetsReplayLegacy'
|
||||
import { getProxyDetails } from '../microsoftAuthflow'
|
||||
import { downloadAutoCapturedPackets, getLastAutoCapturedPackets } from '../mineflayer/plugins/packetsRecording'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import AppStatus from './AppStatus'
|
||||
import DiveTransition from './DiveTransition'
|
||||
import { useDidUpdateEffect } from './utils'
|
||||
|
|
@ -64,7 +65,7 @@ export default () => {
|
|||
lastState.current = JSON.parse(JSON.stringify(currentState))
|
||||
}
|
||||
|
||||
const usingState = isOpen ? currentState : lastState.current
|
||||
const usingState = (isOpen ? currentState : lastState.current) as typeof currentState
|
||||
const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint, loadingChunksData, loadingChunksDataPlayerChunk, minecraftJsonMessage, showReconnect } = usingState
|
||||
|
||||
useDidUpdateEffect(() => {
|
||||
|
|
@ -92,6 +93,7 @@ export default () => {
|
|||
window.addEventListener('keyup', (e) => {
|
||||
if ('input textarea select'.split(' ').includes((e.target as HTMLElement).tagName?.toLowerCase() ?? '')) return
|
||||
if (activeModalStack.at(-1)?.reactType !== 'app-status') return
|
||||
// todo do only if reconnect is possible
|
||||
if (e.code !== 'KeyR' || !lastConnectOptions.value) return
|
||||
reconnect()
|
||||
}, {
|
||||
|
|
@ -116,6 +118,29 @@ export default () => {
|
|||
}
|
||||
|
||||
const lastAutoCapturedPackets = getLastAutoCapturedPackets()
|
||||
const lockConnect = appQueryParams.lockConnect === 'true'
|
||||
const wasDisconnected = showReconnect
|
||||
let backAction = undefined as (() => void) | undefined
|
||||
if (maybeRecoverable && (!lockConnect || !wasDisconnected)) {
|
||||
backAction = () => {
|
||||
if (!wasDisconnected) {
|
||||
hideModal(undefined, undefined, { force: true })
|
||||
return
|
||||
}
|
||||
resetAppStatusState()
|
||||
miscUiState.gameLoaded = false
|
||||
miscUiState.loadedDataVersion = null
|
||||
window.loadedData = undefined
|
||||
if (activeModalStacks['main-menu']) {
|
||||
insertActiveModalStack('main-menu')
|
||||
if (activeModalStack.at(-1)?.reactType === 'app-status') {
|
||||
hideModal(undefined, undefined, { force: true }) // workaround: hide loader that was shown on world loading
|
||||
}
|
||||
} else {
|
||||
hideModal(undefined, undefined, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
return <DiveTransition open={isOpen}>
|
||||
<AppStatus
|
||||
status={status}
|
||||
|
|
@ -129,26 +154,13 @@ export default () => {
|
|||
}{
|
||||
minecraftJsonMessage && <MessageFormattedString message={minecraftJsonMessage} />
|
||||
}</>}
|
||||
backAction={maybeRecoverable ? () => {
|
||||
resetAppStatusState()
|
||||
miscUiState.gameLoaded = false
|
||||
miscUiState.loadedDataVersion = null
|
||||
window.loadedData = undefined
|
||||
if (activeModalStacks['main-menu']) {
|
||||
insertActiveModalStack('main-menu')
|
||||
if (activeModalStack.at(-1)?.reactType === 'app-status') {
|
||||
hideModal(undefined, undefined, { force: true }) // workaround: hide loader that was shown on world loading
|
||||
}
|
||||
} else {
|
||||
hideModal(undefined, undefined, { force: true })
|
||||
}
|
||||
} : undefined}
|
||||
backAction={backAction}
|
||||
actionsSlot={
|
||||
<>
|
||||
{displayAuthButton && <Button label='Authenticate' onClick={authReconnectAction} />}
|
||||
{displayVpnButton && <PossiblyVpnBypassProxyButton reconnect={reconnect} />}
|
||||
{replayActive && <Button label={`Download Packets Replay ${replayLogger?.contents.split('\n').length}L`} onClick={downloadPacketsReplay} />}
|
||||
{lastAutoCapturedPackets && <Button label={`Inspect Last ${lastAutoCapturedPackets} Packets`} onClick={() => downloadAutoCapturedPackets()} />}
|
||||
{wasDisconnected && lastAutoCapturedPackets && <Button label={`Inspect Last ${lastAutoCapturedPackets} Packets`} onClick={() => downloadAutoCapturedPackets()} />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect, useRef, useMemo, useState } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { Block } from 'prismarine-block'
|
||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||
import { getFixedFilesize } from '../downloadAndOpenFile'
|
||||
import { options } from '../optionsStorage'
|
||||
import { getBlockAssetsCacheKey } from '../../renderer/viewer/lib/mesher/shared'
|
||||
import { BlockStateModelInfo } from '../../renderer/viewer/lib/mesher/shared'
|
||||
import styles from './DebugOverlay.module.css'
|
||||
|
||||
export default () => {
|
||||
|
|
@ -34,13 +34,13 @@ export default () => {
|
|||
const [blockL, setBlockL] = useState(0)
|
||||
const [biomeId, setBiomeId] = useState(0)
|
||||
const [day, setDay] = useState(0)
|
||||
const [entitiesCount, setEntitiesCount] = useState('0')
|
||||
const [dimension, setDimension] = useState('')
|
||||
const [cursorBlock, setCursorBlock] = useState<Block | null>(null)
|
||||
const [blockInfo, setBlockInfo] = useState<{ customBlockName?: string, modelInfo?: BlockStateModelInfo } | null>(null)
|
||||
const [clientTps, setClientTps] = useState(0)
|
||||
const minecraftYaw = useRef(0)
|
||||
const minecraftQuad = useRef(0)
|
||||
const { rendererDevice } = viewer.world
|
||||
const rendererDevice = appViewer.rendererState.renderer ?? 'No render backend'
|
||||
|
||||
const quadsDescription = [
|
||||
'north (towards negative Z)',
|
||||
|
|
@ -132,10 +132,19 @@ export default () => {
|
|||
setBiomeId(bot.world.getBiome(bot.entity.position))
|
||||
setDimension(bot.game.dimension)
|
||||
setDay(bot.time.day)
|
||||
setCursorBlock(bot.blockAtCursor(5))
|
||||
setEntitiesCount(`${viewer.entities.entitiesRenderingCount} (${Object.values(bot.entities).length})`)
|
||||
setCursorBlock(bot.mouse.getCursorState().cursorBlock)
|
||||
}, 100)
|
||||
|
||||
const notFrequentUpdateInterval = setInterval(async () => {
|
||||
const block = bot.mouse.cursorBlock
|
||||
if (!block) {
|
||||
setBlockInfo(null)
|
||||
return
|
||||
}
|
||||
const { customBlockName, modelInfo } = await getThreeJsRendererMethods()?.getBlockInfo(pos, block.stateId) ?? {}
|
||||
setBlockInfo({ customBlockName, modelInfo })
|
||||
}, 300)
|
||||
|
||||
// @ts-expect-error
|
||||
bot._client.on('packet', readPacket)
|
||||
// @ts-expect-error
|
||||
|
|
@ -149,6 +158,7 @@ export default () => {
|
|||
document.removeEventListener('keydown', handleF3)
|
||||
clearInterval(packetsUpdateInterval)
|
||||
clearInterval(freqUpdateInterval)
|
||||
clearInterval(notFrequentUpdateInterval)
|
||||
console.log('Last physics tick before disconnect was', Date.now() - lastTickDate, 'ms ago')
|
||||
}
|
||||
}, [])
|
||||
|
|
@ -163,7 +173,7 @@ export default () => {
|
|||
return <>
|
||||
<div className={`debug-left-side ${styles['debug-left-side']}`}>
|
||||
<p>Prismarine Web Client ({bot.version})</p>
|
||||
<p>E: {entitiesCount}</p>
|
||||
{appViewer.backend?.getDebugOverlay().entitiesString && <p>E: {appViewer.backend.getDebugOverlay().entitiesString}</p>}
|
||||
<p>{dimension}</p>
|
||||
<div className={styles.empty} />
|
||||
<p>XYZ: {pos.x.toFixed(3)} / {pos.y.toFixed(3)} / {pos.z.toFixed(3)}</p>
|
||||
|
|
@ -177,10 +187,11 @@ export default () => {
|
|||
<p>Biome: minecraft:{loadedData.biomesArray[biomeId]?.name ?? 'unknown biome'}</p>
|
||||
<p>Day: {day}</p>
|
||||
<div className={styles.empty} />
|
||||
{Object.entries(customEntries.current).map(([name, value]) => <p key={name}>{name}: {value}</p>)}
|
||||
{Object.entries(appViewer.backend?.getDebugOverlay().left ?? {}).map(([name, value]) => <p key={name}>{name}: {value}</p>)}
|
||||
</div>
|
||||
|
||||
<div className={`debug-right-side ${styles['debug-right-side']}`}>
|
||||
<p>Backend: {appViewer.backend?.NAME}</p>
|
||||
<p>Renderer: {rendererDevice}</p>
|
||||
<div className={styles.empty} />
|
||||
{cursorBlock ? (<>
|
||||
|
|
@ -202,11 +213,8 @@ export default () => {
|
|||
<p>Looking at: {cursorBlock.position.x} {cursorBlock.position.y} {cursorBlock.position.z}</p>
|
||||
) : ''}
|
||||
<div className={styles.empty} />
|
||||
{cursorBlock && (() => {
|
||||
const chunkKey = `${Math.floor(cursorBlock.position.x / 16) * 16},${Math.floor(cursorBlock.position.z / 16) * 16}`
|
||||
const customBlockName = viewer.world.protocolCustomBlocks.get(chunkKey)?.[`${cursorBlock.position.x},${cursorBlock.position.y},${cursorBlock.position.z}`]
|
||||
const cacheKey = getBlockAssetsCacheKey(cursorBlock.stateId, customBlockName)
|
||||
const modelInfo = viewer.world.blockStateModelInfo.get(cacheKey)
|
||||
{blockInfo && (() => {
|
||||
const { customBlockName, modelInfo } = blockInfo
|
||||
return modelInfo && (
|
||||
<>
|
||||
{customBlockName && <p style={{ fontSize: 7, }}>Custom block: {customBlockName}</p>}
|
||||
|
|
@ -221,6 +229,7 @@ export default () => {
|
|||
</>
|
||||
)
|
||||
})()}
|
||||
{Object.entries(appViewer.backend?.getDebugOverlay().right ?? {}).map(([name, value]) => <p key={name}>{name}: {value}</p>)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ const HotbarInner = () => {
|
|||
inv.canvas.style.pointerEvents = 'auto'
|
||||
container.current.appendChild(inv.canvas)
|
||||
const upHotbarItems = () => {
|
||||
if (!viewer.world.currentTextureImage || !allImagesLoadedState.value) return
|
||||
if (!appViewer.resourcesManager.currentResources?.itemsAtlasParser || !allImagesLoadedState.value) return
|
||||
upInventoryItems(true, inv)
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ const HotbarInner = () => {
|
|||
|
||||
upHotbarItems()
|
||||
bot.inventory.on('updateSlot', upHotbarItems)
|
||||
viewer.world.renderUpdateEmitter.on('textureDownloaded', upHotbarItems)
|
||||
appViewer.resourcesManager.on('assetsTexturesUpdated', upHotbarItems)
|
||||
const unsub2 = subscribe(allImagesLoadedState, () => {
|
||||
upHotbarItems()
|
||||
})
|
||||
|
|
@ -197,7 +197,7 @@ const HotbarInner = () => {
|
|||
inv.destroy()
|
||||
controller.abort()
|
||||
unsub2()
|
||||
viewer.world.renderUpdateEmitter.off('textureDownloaded', upHotbarItems)
|
||||
appViewer.resourcesManager.off('assetsTexturesUpdated', upHotbarItems)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { proxy, subscribe, useSnapshot } from 'valtio'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { inGameError } from '../utils'
|
||||
import { fsState } from '../loadSave'
|
||||
import { gameAdditionalState, miscUiState } from '../globalState'
|
||||
|
|
@ -9,7 +10,6 @@ import { images } from './effectsImages'
|
|||
|
||||
export const state = proxy({
|
||||
indicators: {
|
||||
chunksLoading: false
|
||||
},
|
||||
effects: [] as EffectType[]
|
||||
})
|
||||
|
|
@ -52,6 +52,9 @@ const getEffectIndex = (newEffect: EffectType) => {
|
|||
|
||||
export default () => {
|
||||
const stateIndicators = useSnapshot(state.indicators)
|
||||
const chunksLoading = !useSnapshot(appViewer.rendererState).world.allChunksLoaded
|
||||
const { mesherWork } = useSnapshot(appViewer.rendererState).world
|
||||
|
||||
const { hasErrors } = useSnapshot(miscUiState)
|
||||
const { disabledUiParts } = useSnapshot(options)
|
||||
const { isReadonly, openReadOperations, openWriteOperations } = useSnapshot(fsState)
|
||||
|
|
@ -62,27 +65,11 @@ export default () => {
|
|||
readingFiles: openReadOperations > 0,
|
||||
appHasErrors: hasErrors,
|
||||
connectionIssues: poorConnection ? 1 : noConnection ? 2 : 0,
|
||||
chunksLoading,
|
||||
// mesherWork,
|
||||
...stateIndicators,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let alreadyWaiting = false
|
||||
const listener = () => {
|
||||
if (alreadyWaiting) return
|
||||
state.indicators.chunksLoading = true
|
||||
alreadyWaiting = true
|
||||
void viewer.waitForChunksToRender().then(() => {
|
||||
state.indicators.chunksLoading = false
|
||||
alreadyWaiting = false
|
||||
})
|
||||
}
|
||||
viewer.world.renderUpdateEmitter.on('dirty', listener)
|
||||
|
||||
return () => {
|
||||
viewer.world.renderUpdateEmitter.off('dirty', listener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const effects = useSnapshot(state.effects)
|
||||
|
||||
useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect } from 'react'
|
|||
import { simplify } from 'prismarine-nbt'
|
||||
import RegionFile from 'prismarine-provider-anvil/src/region'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import { TypedEventEmitter } from 'contro-max/build/typedEventEmitter'
|
||||
import { PCChunk } from 'prismarine-chunk'
|
||||
|
|
@ -11,6 +11,8 @@ import { Block } from 'prismarine-block'
|
|||
import { INVISIBLE_BLOCKS } from 'renderer/viewer/lib/mesher/worldConstants'
|
||||
import { getRenamedData } from 'flying-squid/dist/blockRenames'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
||||
import BlockData from '../../renderer/viewer/lib/moreBlockDataGenerated.json'
|
||||
import preflatMap from '../preflatMap.json'
|
||||
import { contro } from '../controls'
|
||||
|
|
@ -54,6 +56,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
loadChunkFullmap: (key: string) => Promise<ChunkInfo | null | undefined>
|
||||
_full = false
|
||||
isBuiltinHeightmapAvailable = false
|
||||
unsubscribers: Array<() => void> = []
|
||||
|
||||
constructor (pos?: Vec3) {
|
||||
super()
|
||||
|
|
@ -111,13 +114,22 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
this.blockData.set(renamedKey, BlockData.colors[blockKey])
|
||||
}
|
||||
|
||||
viewer.world?.renderUpdateEmitter.on('chunkFinished', (key) => {
|
||||
if (!this.loadingChunksQueue.has(key)) return
|
||||
this.loadingChunksQueue.delete(key)
|
||||
void this.loadChunk(key)
|
||||
subscribeKey(appViewer.rendererState, 'world', () => {
|
||||
for (const key of this.loadingChunksQueue) {
|
||||
if (appViewer.rendererState.world.chunksLoaded.includes(key)) {
|
||||
this.loadingChunksQueue.delete(key)
|
||||
void this.loadChunk(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
for (const unsubscriber of this.unsubscribers) {
|
||||
unsubscriber()
|
||||
}
|
||||
}
|
||||
|
||||
get full () {
|
||||
return this._full
|
||||
}
|
||||
|
|
@ -188,7 +200,9 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) {
|
||||
if (appViewer.rendererState.world.chunksLoaded.includes(`${chunkWorldX},${chunkWorldZ}`)) {
|
||||
const highestBlocks = await getThreeJsRendererMethods()?.getHighestBlocks()
|
||||
if (!highestBlocks) return undefined
|
||||
const heightmap = new Uint8Array(256)
|
||||
const colors = Array.from({ length: 256 }).fill('') as string[]
|
||||
// avoid creating new object every time
|
||||
|
|
@ -198,7 +212,7 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
for (let x = 0; x < 16; x += 1) {
|
||||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const hBlock = viewer.world.highestBlocks.get(`${blockX},${blockZ}`)
|
||||
const hBlock = highestBlocks.get(`${blockX},${blockZ}`)
|
||||
blockPos.x = blockX; blockPos.z = blockZ; blockPos.y = hBlock?.y ?? 0
|
||||
let block = bot.world.getBlock(blockPos)
|
||||
while (block?.name.includes('air')) {
|
||||
|
|
@ -319,14 +333,16 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
const [chunkX, chunkZ] = key.split(',').map(Number)
|
||||
const chunkWorldX = chunkX * 16
|
||||
const chunkWorldZ = chunkZ * 16
|
||||
if (viewer.world.finishedChunks[`${chunkWorldX},${chunkWorldZ}`]) {
|
||||
const highestBlocks = await getThreeJsRendererMethods()?.getHighestBlocks()
|
||||
if (appViewer.rendererState.world.chunksLoaded.includes(`${chunkWorldX},${chunkWorldZ}`)) {
|
||||
const heightmap = new Uint8Array(256)
|
||||
const colors = Array.from({ length: 256 }).fill('') as string[]
|
||||
if (!highestBlocks) return null
|
||||
for (let z = 0; z < 16; z += 1) {
|
||||
for (let x = 0; x < 16; x += 1) {
|
||||
const blockX = chunkWorldX + x
|
||||
const blockZ = chunkWorldZ + z
|
||||
const hBlock = viewer.world.highestBlocks.get(`${blockX},${blockZ}`)
|
||||
const hBlock = highestBlocks.get(`${blockX},${blockZ}`)
|
||||
const block = bot.world.getBlock(new Vec3(blockX, hBlock?.y ?? 0, blockZ))
|
||||
// const block = Block.fromStateId(hBlock?.stateId ?? -1, hBlock?.biomeId ?? -1)
|
||||
const index = z * 16 + x
|
||||
|
|
|
|||
|
|
@ -7,21 +7,22 @@ export const name = 'loaded world signs'
|
|||
export default () => {
|
||||
const [selected, setSelected] = useState([] as string[])
|
||||
const allSignsPos = [] as string[]
|
||||
const signs = viewer.world instanceof WorldRendererThree ? [...viewer.world.chunkTextures.values()].flatMap(textures => {
|
||||
return Object.entries(textures).map(([signPosKey, texture]) => {
|
||||
allSignsPos.push(signPosKey)
|
||||
const pos = signPosKey.split(',').map(Number)
|
||||
const isSelected = selected.includes(signPosKey)
|
||||
return <div key={signPosKey}>
|
||||
<div style={{ color: 'white', userSelect: 'text', fontSize: 8, }}>{pos.join(', ')}</div>
|
||||
<div
|
||||
style={{ background: isSelected ? 'rgba(255, 255, 255, 0.8)' : 'rgba(255, 255, 255, 0.5)', padding: 5, borderRadius: 5, cursor: 'pointer' }}
|
||||
onClick={() => setSelected(selected.includes(signPosKey) ? selected.filter(x => x !== signPosKey) : [...selected, signPosKey])}>
|
||||
<AddElem elem={texture.image} />
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}) : []
|
||||
const signs = []
|
||||
// const signs = viewer.world instanceof WorldRendererThree ? [...viewer.world.chunkTextures.values()].flatMap(textures => {
|
||||
// return Object.entries(textures).map(([signPosKey, texture]) => {
|
||||
// allSignsPos.push(signPosKey)
|
||||
// const pos = signPosKey.split(',').map(Number)
|
||||
// const isSelected = selected.includes(signPosKey)
|
||||
// return <div key={signPosKey}>
|
||||
// <div style={{ color: 'white', userSelect: 'text', fontSize: 8, }}>{pos.join(', ')}</div>
|
||||
// <div
|
||||
// style={{ background: isSelected ? 'rgba(255, 255, 255, 0.8)' : 'rgba(255, 255, 255, 0.5)', padding: 5, borderRadius: 5, cursor: 'pointer' }}
|
||||
// onClick={() => setSelected(selected.includes(signPosKey) ? selected.filter(x => x !== signPosKey) : [...selected, signPosKey])}>
|
||||
// <AddElem elem={texture.image} />
|
||||
// </div>
|
||||
// </div>
|
||||
// })
|
||||
// }) : []
|
||||
|
||||
return <FullScreenWidget name={name} title='Loaded Signs'>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,19 @@ import { UIProvider } from './react/UIProvider'
|
|||
import { useAppScale } from './scaleInterface'
|
||||
import PacketsReplayProvider from './react/PacketsReplayProvider'
|
||||
import TouchInteractionHint from './react/TouchInteractionHint'
|
||||
import { ua } from './react/utils'
|
||||
|
||||
const isFirefox = ua.getBrowser().name === 'Firefox'
|
||||
if (isFirefox) {
|
||||
// set custom property
|
||||
document.body.style.setProperty('--thin-if-firefox', 'thin')
|
||||
}
|
||||
|
||||
const isIphone = ua.getDevice().model === 'iPhone' // todo ipad?
|
||||
|
||||
if (isIphone) {
|
||||
document.documentElement.style.setProperty('--hud-bottom-max', '21px') // env-safe-aria-inset-bottom
|
||||
}
|
||||
|
||||
const RobustPortal = ({ children, to }) => {
|
||||
return createPortal(<PerComponentErrorBoundary>{children}</PerComponentErrorBoundary>, to)
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ const updateFovAnimation = () => {
|
|||
currentFov = targetFov
|
||||
}
|
||||
|
||||
viewer.camera.fov = currentFov
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
appViewer.inWorldRenderingConfig.fov = currentFov
|
||||
}
|
||||
lastUpdateTime = now
|
||||
}
|
||||
|
|
@ -91,6 +90,6 @@ export const watchFov = () => {
|
|||
})
|
||||
|
||||
subscribeKey(gameAdditionalState, 'isSneaking', () => {
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
appViewer.backend?.updateCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ import { join, dirname, basename } from 'path'
|
|||
import fs from 'fs'
|
||||
import JSZip from 'jszip'
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree'
|
||||
import { armorTextures } from 'renderer/viewer/lib/entity/armorModels'
|
||||
import { collectFilesToCopy, copyFilesAsyncWithProgress, mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
|
||||
import { copyFilesAsyncWithProgress, mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { options } from './optionsStorage'
|
||||
import { showOptionsModal } from './react/SelectOption'
|
||||
import { appStatusState } from './react/AppStatusProvider'
|
||||
import { appReplacableResources, resourcesContentOriginal } from './generated/resources'
|
||||
import { gameAdditionalState, miscUiState } from './globalState'
|
||||
import { watchUnloadForCleanup } from './gameUnload'
|
||||
|
|
@ -312,8 +310,9 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items' | 'armor', e
|
|||
}
|
||||
|
||||
const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) => {
|
||||
viewer.world.customBlockStates = {}
|
||||
viewer.world.customModels = {}
|
||||
const resources = appViewer.resourcesManager.currentResources!
|
||||
resources.customBlockStates = {}
|
||||
resources.customModels = {}
|
||||
const usedBlockTextures = new Set<string>()
|
||||
const usedItemTextures = new Set<string>()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
|
|
@ -360,9 +359,9 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) =
|
|||
const blockModelsPath = `${basePath}/assets/${namespaceDir}/models/block`
|
||||
const itemModelsPath = `${basePath}/assets/${namespaceDir}/models/item`
|
||||
|
||||
Object.assign(viewer.world.customBlockStates!, await readModelData(blockstatesPath, 'blockstates', namespaceDir))
|
||||
Object.assign(viewer.world.customModels!, await readModelData(blockModelsPath, 'models', namespaceDir))
|
||||
Object.assign(viewer.world.customModels!, await readModelData(itemModelsPath, 'models', namespaceDir))
|
||||
Object.assign(resources.customBlockStates!, await readModelData(blockstatesPath, 'blockstates', namespaceDir))
|
||||
Object.assign(resources.customModels!, await readModelData(blockModelsPath, 'models', namespaceDir))
|
||||
Object.assign(resources.customModels!, await readModelData(itemModelsPath, 'models', namespaceDir))
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -372,8 +371,8 @@ const prepareBlockstatesAndModels = async (progressReporter: ProgressReporter) =
|
|||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to read some of resource pack blockstates and models', err)
|
||||
viewer.world.customBlockStates = undefined
|
||||
viewer.world.customModels = undefined
|
||||
resources.customBlockStates = undefined
|
||||
resources.customModels = undefined
|
||||
}
|
||||
return {
|
||||
usedBlockTextures,
|
||||
|
|
@ -531,46 +530,48 @@ const updateAllReplacableTextures = async () => {
|
|||
|
||||
const repeatArr = (arr, i) => Array.from({ length: i }, () => arr)
|
||||
|
||||
const updateTextures = async (progressReporter = createConsoleLogProgressReporter()) => {
|
||||
const updateTextures = async (progressReporter = createConsoleLogProgressReporter(), skipResourcesLoad = false) => {
|
||||
if (!appViewer.resourcesManager.currentResources) {
|
||||
appViewer.resourcesManager.resetResources()
|
||||
}
|
||||
const resources = appViewer.resourcesManager.currentResources!
|
||||
currentErrors = []
|
||||
const origBlocksFiles = Object.keys(viewer.world.sourceData.blocksAtlases.latest.textures)
|
||||
const origItemsFiles = Object.keys(viewer.world.sourceData.itemsAtlases.latest.textures)
|
||||
const origBlocksFiles = Object.keys(appViewer.resourcesManager.sourceBlocksAtlases.latest.textures)
|
||||
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()
|
||||
viewer.world.customTextures = {}
|
||||
resources.customTextures = {}
|
||||
|
||||
if (blocksData) {
|
||||
viewer.world.customTextures.blocks = {
|
||||
resources.customTextures.blocks = {
|
||||
tileSize: blocksData.firstTextureSize,
|
||||
textures: blocksData.textures
|
||||
}
|
||||
}
|
||||
if (itemsData) {
|
||||
viewer.world.customTextures.items = {
|
||||
resources.customTextures.items = {
|
||||
tileSize: itemsData.firstTextureSize,
|
||||
textures: itemsData.textures
|
||||
}
|
||||
}
|
||||
if (armorData) {
|
||||
viewer.world.customTextures.armor = {
|
||||
resources.customTextures.armor = {
|
||||
tileSize: armorData.firstTextureSize,
|
||||
textures: armorData.textures
|
||||
}
|
||||
}
|
||||
|
||||
if (viewer.world.active) {
|
||||
await viewer.world.updateAssetsData()
|
||||
if (viewer.world instanceof WorldRendererThree) {
|
||||
viewer.world.rerenderAllChunks?.()
|
||||
}
|
||||
if (!skipResourcesLoad) {
|
||||
await appViewer.resourcesManager.updateAssetsData({ })
|
||||
}
|
||||
}
|
||||
|
||||
export const resourcepackReload = async (version) => {
|
||||
await updateTextures()
|
||||
export const resourcepackReload = async (skipResourcesLoad = false) => {
|
||||
await updateTextures(undefined, skipResourcesLoad)
|
||||
}
|
||||
|
||||
export const copyServerResourcePackToRegular = async (name = 'default') => {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,201 @@
|
|||
import { Item } from 'prismarine-item'
|
||||
import { ItemSpecificContextProperties } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
import { GeneralInputItem, getItemMetadata } from './mineflayer/items'
|
||||
import { EventEmitter } from 'events'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
|
||||
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
|
||||
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
|
||||
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
|
||||
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 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'
|
||||
|
||||
export const getItemModelName = (item: GeneralInputItem, specificProps: ItemSpecificContextProperties) => {
|
||||
let itemModelName = item.name
|
||||
const { customModel } = getItemMetadata(item)
|
||||
if (customModel) {
|
||||
itemModelName = customModel
|
||||
type ResourceManagerEvents = {
|
||||
assetsTexturesUpdated: () => void
|
||||
assetsInventoryReady: () => void
|
||||
}
|
||||
|
||||
export class LoadedResources {
|
||||
// Atlas parsers
|
||||
itemsAtlasParser: AtlasParser
|
||||
blocksAtlasParser: AtlasParser
|
||||
itemsAtlasImage: HTMLImageElement
|
||||
blocksAtlasImage: HTMLImageElement
|
||||
// User data (specific to current resourcepack/version)
|
||||
customBlockStates?: Record<string, any>
|
||||
customModels?: Record<string, any>
|
||||
customTextures: {
|
||||
items?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
|
||||
blocks?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
|
||||
armor?: { tileSize: number | undefined, textures: Record<string, HTMLImageElement> }
|
||||
} = {}
|
||||
|
||||
itemsRenderer: ItemsRenderer
|
||||
worldBlockProvider: WorldBlockProvider
|
||||
blockstatesModels: any = null
|
||||
|
||||
version: string
|
||||
texturesVersion: string
|
||||
}
|
||||
|
||||
export interface ResourcesCurrentConfig {
|
||||
version: string
|
||||
texturesVersion?: string
|
||||
noBlockstatesModels?: boolean
|
||||
includeOnlyBlocks?: string[]
|
||||
}
|
||||
|
||||
export interface UpdateAssetsRequest {
|
||||
_?: false
|
||||
}
|
||||
|
||||
export class ResourcesManager extends (EventEmitter as new () => TypedEmitter<ResourceManagerEvents>) {
|
||||
// Source data (imported, not changing)
|
||||
sourceBlockStatesModels: any = null
|
||||
readonly sourceBlocksAtlases: any = blocksAtlases
|
||||
readonly sourceItemsAtlases: any = itemsAtlases
|
||||
readonly sourceItemDefinitionsJson: any = itemDefinitionsJson
|
||||
readonly itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceItemDefinitionsJson)
|
||||
|
||||
currentResources: LoadedResources | undefined
|
||||
currentConfig: ResourcesCurrentConfig | undefined
|
||||
abortController = new AbortController()
|
||||
|
||||
async loadMcData (version: string) {
|
||||
await loadMinecraftData(version)
|
||||
}
|
||||
|
||||
const itemSelector = playerState.getItemSelector({
|
||||
...specificProps
|
||||
})
|
||||
const modelFromDef = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
name: itemModelName,
|
||||
version: viewer.world.texturesVersion!,
|
||||
properties: itemSelector
|
||||
})?.model
|
||||
const model = (modelFromDef === 'minecraft:special' ? undefined : modelFromDef) ?? itemModelName
|
||||
return model
|
||||
async loadSourceData (version: string) {
|
||||
await this.loadMcData(version)
|
||||
this.sourceBlockStatesModels ??= await importLargeData('blockStatesModels')
|
||||
}
|
||||
|
||||
resetResources () {
|
||||
this.currentResources = new LoadedResources()
|
||||
}
|
||||
|
||||
async updateAssetsData (request: UpdateAssetsRequest, unstableSkipEvent = false) {
|
||||
if (!this.currentConfig) throw new Error('No config loaded')
|
||||
const abortController = new AbortController()
|
||||
await this.loadSourceData(this.currentConfig.version)
|
||||
if (abortController.signal.aborted) return
|
||||
|
||||
const resources = this.currentResources ?? new LoadedResources()
|
||||
resources.version = this.currentConfig.version
|
||||
resources.texturesVersion = this.currentConfig.texturesVersion ?? resources.version
|
||||
|
||||
resources.blockstatesModels = {
|
||||
blockstates: {},
|
||||
models: {}
|
||||
}
|
||||
// todo-low resolve version
|
||||
resources.blockstatesModels.blockstates.latest = {
|
||||
...this.sourceBlockStatesModels.blockstates.latest,
|
||||
...resources.customBlockStates
|
||||
}
|
||||
|
||||
resources.blockstatesModels.models.latest = {
|
||||
...this.sourceBlockStatesModels.models.latest,
|
||||
...resources.customModels
|
||||
}
|
||||
|
||||
|
||||
const blocksAssetsParser = new AtlasParser(this.sourceBlocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
|
||||
const itemsAssetsParser = new AtlasParser(this.sourceItemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
|
||||
|
||||
const blockTexturesChanges = {} as Record<string, string>
|
||||
const date = new Date()
|
||||
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) {
|
||||
Object.assign(blockTexturesChanges, christmasPack)
|
||||
}
|
||||
|
||||
const customBlockTextures = Object.keys(resources.customTextures.blocks?.textures ?? {})
|
||||
const customItemTextures = Object.keys(resources.customTextures.items?.textures ?? {})
|
||||
|
||||
console.time('createBlocksAtlas')
|
||||
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(
|
||||
resources.texturesVersion,
|
||||
(textureName) => {
|
||||
if (this.currentConfig!.includeOnlyBlocks && !this.currentConfig!.includeOnlyBlocks.includes(textureName)) return false
|
||||
const texture = resources.customTextures.blocks?.textures[textureName]
|
||||
return blockTexturesChanges[textureName] ?? texture
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
customBlockTextures
|
||||
)
|
||||
console.timeEnd('createBlocksAtlas')
|
||||
|
||||
if (abortController.signal.aborted) return
|
||||
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.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
|
||||
resources.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
|
||||
resources.blocksAtlasImage = await getLoadedImage(blocksCanvas.toDataURL())
|
||||
resources.itemsAtlasImage = await getLoadedImage(itemsCanvas.toDataURL())
|
||||
|
||||
if (resources.version && resources.blockstatesModels && resources.itemsAtlasParser && resources.blocksAtlasParser) {
|
||||
resources.itemsRenderer = new ItemsRenderer(
|
||||
resources.version,
|
||||
resources.blockstatesModels,
|
||||
resources.itemsAtlasParser,
|
||||
resources.blocksAtlasParser
|
||||
)
|
||||
resources.worldBlockProvider = worldBlockProvider(
|
||||
resources.blockstatesModels,
|
||||
resources.blocksAtlasParser.atlas,
|
||||
'latest'
|
||||
)
|
||||
}
|
||||
|
||||
if (abortController.signal.aborted) return
|
||||
|
||||
this.currentResources = resources
|
||||
if (!unstableSkipEvent) { // todo rework resourcepack optimization
|
||||
this.emit('assetsTexturesUpdated')
|
||||
void this.generateGuiTextures().then(() => {
|
||||
this.emit('assetsInventoryReady')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async generateGuiTextures () {
|
||||
await generateGuiAtlas()
|
||||
}
|
||||
|
||||
async downloadDebugAtlas (isItems = false) {
|
||||
const resources = this.currentResources
|
||||
if (!resources) throw new Error('No resources loaded')
|
||||
const atlasParser = (isItems ? resources.itemsAtlasParser : resources.blocksAtlasParser)!
|
||||
const dataUrl = await atlasParser.createDebugImage(true)
|
||||
const a = document.createElement('a')
|
||||
a.href = dataUrl
|
||||
a.download = `atlas-debug-${isItems ? 'items' : 'blocks'}.png`
|
||||
a.click()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.abortController.abort()
|
||||
this.currentResources = undefined
|
||||
this.abortController = new AbortController()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { restoreMinecraftData } from '../optimizeJson'
|
||||
// import minecraftInitialDataJson from '../../generated/minecraft-initial-data.json'
|
||||
import { toMajorVersion } from '../utils'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'renderer/viewer/common/utils'
|
||||
import { loadScript } from 'renderer/viewer/lib/utils'
|
||||
import type { Block } from 'prismarine-block'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
|
|
@ -46,7 +46,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
const isMuted = options.mutedSounds.includes(soundKey) || options.volume === 0
|
||||
if (position) {
|
||||
if (!isMuted) {
|
||||
viewer.playSound(
|
||||
appViewer.backend?.soundSystem?.playSound(
|
||||
position,
|
||||
soundData.url,
|
||||
soundData.volume * (options.volume / 100),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { versionsMapToMajor, versionToMajor, versionToNumber } from 'renderer/viewer/prepare/utils'
|
||||
import { versionsMapToMajor, versionToMajor, versionToNumber } from 'renderer/viewer/common/utils'
|
||||
|
||||
import { stopAllSounds } from '../basicSounds'
|
||||
import { musicSystem } from './musicSystem'
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
import Stats from 'stats.js'
|
||||
import StatsGl from 'stats-gl'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
|
||||
const stats = new Stats()
|
||||
const stats2 = new Stats()
|
||||
const hasRamPanel = stats2.dom.children.length === 3
|
||||
const statsGl = new StatsGl({ minimal: true })
|
||||
// in my case values are good: gpu: < 0.5, cpu < 0.15
|
||||
|
||||
stats2.showPanel(2)
|
||||
|
||||
// prod or small screen
|
||||
const denseMode = process.env.NODE_ENV === 'production' || window.innerHeight < 500
|
||||
|
||||
let total = 0
|
||||
const addStat = (dom, size = 80) => {
|
||||
dom.style.position = 'absolute'
|
||||
if (denseMode) dom.style.height = '12px'
|
||||
dom.style.overflow = 'hidden'
|
||||
dom.style.left = ''
|
||||
dom.style.top = 0
|
||||
dom.style.right = `${total}px`
|
||||
dom.style.width = '80px'
|
||||
dom.style.zIndex = 1
|
||||
dom.style.opacity = '0.8'
|
||||
document.body.appendChild(dom)
|
||||
total += size
|
||||
}
|
||||
const addStatsGlStat = (canvas) => {
|
||||
const container = document.createElement('div')
|
||||
canvas.style.position = 'static'
|
||||
canvas.style.display = 'block'
|
||||
container.appendChild(canvas)
|
||||
addStat(container)
|
||||
}
|
||||
addStat(stats.dom)
|
||||
if (hasRamPanel) {
|
||||
addStat(stats2.dom)
|
||||
}
|
||||
|
||||
export const toggleStatsVisibility = (visible: boolean) => {
|
||||
if (visible) {
|
||||
stats.dom.style.display = 'block'
|
||||
stats2.dom.style.display = 'block'
|
||||
statsGl.container.style.display = 'block'
|
||||
} else {
|
||||
stats.dom.style.display = 'none'
|
||||
stats2.dom.style.display = 'none'
|
||||
statsGl.container.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
const hideStats = localStorage.hideStats || isCypress()
|
||||
if (hideStats) {
|
||||
toggleStatsVisibility(false)
|
||||
}
|
||||
|
||||
export const initWithRenderer = (canvas) => {
|
||||
if (hideStats) return
|
||||
statsGl.init(canvas)
|
||||
// if (statsGl.gpuPanel && process.env.NODE_ENV !== 'production') {
|
||||
// addStatsGlStat(statsGl.gpuPanel.canvas)
|
||||
// }
|
||||
// addStatsGlStat(statsGl.msPanel.canvas)
|
||||
statsGl.container.style.display = 'flex'
|
||||
statsGl.container.style.justifyContent = 'flex-end'
|
||||
let i = 0
|
||||
for (const _child of statsGl.container.children) {
|
||||
const child = _child as HTMLElement
|
||||
if (i++ === 0) {
|
||||
child.style.display = 'none'
|
||||
}
|
||||
child.style.position = ''
|
||||
}
|
||||
}
|
||||
|
||||
export const statsStart = () => {
|
||||
stats.begin()
|
||||
stats2.begin()
|
||||
statsGl.begin()
|
||||
}
|
||||
export const statsEnd = () => {
|
||||
stats.end()
|
||||
stats2.end()
|
||||
statsGl.end()
|
||||
}
|
||||
|
||||
// for advanced debugging, use with watch expression
|
||||
|
||||
window.statsPerSecAvg = {}
|
||||
let currentStatsPerSec = {} as Record<string, number[]>
|
||||
const waitingStatsPerSec = {}
|
||||
window.markStart = (label) => {
|
||||
waitingStatsPerSec[label] ??= []
|
||||
waitingStatsPerSec[label][0] = performance.now()
|
||||
}
|
||||
window.markEnd = (label) => {
|
||||
if (!waitingStatsPerSec[label]?.[0]) return
|
||||
currentStatsPerSec[label] ??= []
|
||||
currentStatsPerSec[label].push(performance.now() - waitingStatsPerSec[label][0])
|
||||
delete waitingStatsPerSec[label]
|
||||
}
|
||||
const updateStatsPerSecAvg = () => {
|
||||
window.statsPerSecAvg = Object.fromEntries(Object.entries(currentStatsPerSec).map(([key, value]) => {
|
||||
return [key, {
|
||||
avg: value.reduce((a, b) => a + b, 0) / value.length,
|
||||
count: value.length
|
||||
}]
|
||||
}))
|
||||
currentStatsPerSec = {}
|
||||
}
|
||||
|
||||
|
||||
window.statsPerSec = {}
|
||||
let statsPerSecCurrent = {}
|
||||
let lastReset = performance.now()
|
||||
window.addStatPerSec = (name) => {
|
||||
statsPerSecCurrent[name] ??= 0
|
||||
statsPerSecCurrent[name]++
|
||||
}
|
||||
window.statsPerSecCurrent = statsPerSecCurrent
|
||||
setInterval(() => {
|
||||
window.statsPerSec = { duration: Math.floor(performance.now() - lastReset), ...statsPerSecCurrent, }
|
||||
statsPerSecCurrent = {}
|
||||
window.statsPerSecCurrent = statsPerSecCurrent
|
||||
updateStatsPerSecAvg()
|
||||
lastReset = performance.now()
|
||||
}, 1000)
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
// not all options are watched here
|
||||
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree'
|
||||
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
|
||||
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { reloadChunks } from './utils'
|
||||
import { miscUiState } from './globalState'
|
||||
import { toggleStatsVisibility } from './topRightStats'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
|
||||
subscribeKey(options, 'renderDistance', reloadChunks)
|
||||
subscribeKey(options, 'multiplayerRenderDistance', reloadChunks)
|
||||
|
|
@ -17,87 +17,104 @@ watchValue(options, o => {
|
|||
document.documentElement.style.setProperty('--chatHeight', `${o.chatHeight}px`)
|
||||
// gui scale is set in scaleInterface.ts
|
||||
})
|
||||
const updateTouch = (o) => {
|
||||
miscUiState.currentTouch = o.alwaysShowMobileControls || isMobile()
|
||||
}
|
||||
watchValue(options, updateTouch)
|
||||
window.matchMedia('(pointer: coarse)').addEventListener('change', (e) => {
|
||||
updateTouch(options)
|
||||
})
|
||||
|
||||
/** happens once */
|
||||
export const watchOptionsAfterViewerInit = () => {
|
||||
const updateTouch = (o) => {
|
||||
miscUiState.currentTouch = o.alwaysShowMobileControls || isMobile()
|
||||
}
|
||||
|
||||
watchValue(options, updateTouch)
|
||||
window.matchMedia('(pointer: coarse)').addEventListener('change', (e) => {
|
||||
updateTouch(options)
|
||||
watchValue(options, o => {
|
||||
appViewer.inWorldRenderingConfig.showChunkBorders = o.showChunkBorders
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
if (!viewer) return
|
||||
viewer.world.config.showChunkBorders = o.showChunkBorders
|
||||
viewer.entities.setDebugMode(o.showChunkBorders ? 'basic' : 'none')
|
||||
appViewer.inWorldRenderingConfig.mesherWorkers = o.lowMemoryMode ? 1 : o.numWorkers
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
if (!viewer) return
|
||||
// todo ideally there shouldnt be this setting and we don't need to send all same chunks to all workers
|
||||
viewer.world.config.numWorkers = o.lowMemoryMode ? 1 : o.numWorkers
|
||||
appViewer.inWorldRenderingConfig.renderEntities = o.renderEntities
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.entities.setRendering(o.renderEntities)
|
||||
const { renderDebug } = o
|
||||
if (renderDebug === 'none' || isCypress()) {
|
||||
appViewer.config.statsVisible = 0
|
||||
} else if (o.renderDebug === 'basic') {
|
||||
appViewer.config.statsVisible = 1
|
||||
} else if (o.renderDebug === 'advanced') {
|
||||
appViewer.config.statsVisible = 2
|
||||
}
|
||||
})
|
||||
|
||||
if (options.renderDebug === 'none') {
|
||||
toggleStatsVisibility(false)
|
||||
}
|
||||
subscribeKey(options, 'renderDebug', () => {
|
||||
if (options.renderDebug === 'none') {
|
||||
toggleStatsVisibility(false)
|
||||
// Track window focus state and update FPS limit accordingly
|
||||
let windowFocused = true
|
||||
const updateFpsLimit = (o: typeof options) => {
|
||||
const backgroundFpsLimit = o.backgroundRendering
|
||||
const normalFpsLimit = o.frameLimit
|
||||
|
||||
if (windowFocused) {
|
||||
appViewer.config.fpsLimit = normalFpsLimit || undefined
|
||||
} else if (backgroundFpsLimit === '5fps') {
|
||||
appViewer.config.fpsLimit = 5
|
||||
} else if (backgroundFpsLimit === '20fps') {
|
||||
appViewer.config.fpsLimit = 20
|
||||
} else {
|
||||
toggleStatsVisibility(true)
|
||||
appViewer.config.fpsLimit = undefined
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
windowFocused = true
|
||||
updateFpsLimit(options)
|
||||
})
|
||||
window.addEventListener('blur', () => {
|
||||
windowFocused = false
|
||||
updateFpsLimit(options)
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.world.displayStats = o.renderDebug === 'advanced'
|
||||
updateFpsLimit(o)
|
||||
})
|
||||
|
||||
watchValue(options, (o, isChanged) => {
|
||||
viewer.world.mesherConfig.clipWorldBelowY = o.clipWorldBelowY
|
||||
viewer.world.mesherConfig.disableSignsMapsSupport = o.disableSignsMapsSupport
|
||||
if (isChanged) {
|
||||
(viewer.world as WorldRendererThree).rerenderAllChunks()
|
||||
}
|
||||
appViewer.inWorldRenderingConfig.clipWorldBelowY = o.clipWorldBelowY
|
||||
appViewer.inWorldRenderingConfig.extraBlockRenderers = !o.disableSignsMapsSupport
|
||||
appViewer.inWorldRenderingConfig.fetchPlayerSkins = o.loadPlayerSkins
|
||||
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
|
||||
})
|
||||
|
||||
viewer.world.mesherConfig.smoothLighting = options.smoothLighting
|
||||
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
|
||||
subscribeKey(options, 'smoothLighting', () => {
|
||||
viewer.world.mesherConfig.smoothLighting = options.smoothLighting;
|
||||
(viewer.world as WorldRendererThree).rerenderAllChunks()
|
||||
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
|
||||
})
|
||||
|
||||
subscribeKey(options, 'newVersionsLighting', () => {
|
||||
viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting;
|
||||
(viewer.world as WorldRendererThree).rerenderAllChunks()
|
||||
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
})
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
appViewer.inWorldRenderingConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
if (!(viewer.world instanceof WorldRendererThree)) return
|
||||
viewer.world.starField.enabled = o.starfieldRendering
|
||||
appViewer.inWorldRenderingConfig.starfield = o.starfieldRendering
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
viewer.world.neighborChunkUpdates = o.neighborChunkUpdates
|
||||
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
|
||||
})
|
||||
}
|
||||
|
||||
let viewWatched = false
|
||||
export const watchOptionsAfterWorldViewInit = () => {
|
||||
if (viewWatched) return
|
||||
viewWatched = true
|
||||
export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
|
||||
watchValue(options, o => {
|
||||
if (!worldView) return
|
||||
worldView.keepChunksDistance = o.keepChunksDistance
|
||||
viewer.world.config.renderEars = o.renderEars
|
||||
viewer.world.config.showHand = o.showHand
|
||||
viewer.world.config.viewBobbing = o.viewBobbing
|
||||
appViewer.inWorldRenderingConfig.renderEars = o.renderEars
|
||||
appViewer.inWorldRenderingConfig.showHand = o.showHand
|
||||
appViewer.inWorldRenderingConfig.viewBobbing = o.viewBobbing
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import * as THREE from 'three'
|
||||
import { watchUnloadForCleanup } from './gameUnload'
|
||||
|
||||
let inWater = false
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
const cleanup = () => {
|
||||
viewer.scene.fog = null
|
||||
appViewer.playerState.reactive.inWater = false
|
||||
}
|
||||
watchUnloadForCleanup(cleanup)
|
||||
|
||||
const updateInWater = () => {
|
||||
const waterBr = Object.keys(bot.entity.effects).find((effect: any) => loadedData.effects[effect.id].name === 'water_breathing')
|
||||
if (inWater) {
|
||||
viewer.scene.fog = new THREE.Fog(0x00_00_ff, 0.1, waterBr ? 100 : 20) // Set the fog color to blue if the bot is in water.
|
||||
appViewer.playerState.reactive.inWater = true
|
||||
} else {
|
||||
cleanup()
|
||||
}
|
||||
|
|
@ -32,5 +31,5 @@ let sceneBg = { r: 0, g: 0, b: 0 }
|
|||
export const updateBackground = (newSceneBg = sceneBg) => {
|
||||
sceneBg = newSceneBg
|
||||
const color: [number, number, number] = inWater ? [0, 0, 1] : [sceneBg.r, sceneBg.g, sceneBg.b]
|
||||
viewer.world.changeBackgroundColor(color)
|
||||
appViewer.playerState.reactive.backgroundColor = color
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue