diff --git a/prismarine-viewer/viewer/lib/entities.ts b/prismarine-viewer/viewer/lib/entities.ts index 5e7a96bfe..f5f4b21fd 100644 --- a/prismarine-viewer/viewer/lib/entities.ts +++ b/prismarine-viewer/viewer/lib/entities.ts @@ -20,6 +20,7 @@ import { getMesh } from './entity/EntityMesh' import { WalkingGeneralSwing } from './entity/animations' import { disposeObject } from './threeJsUtils' import { armorModels } from './entity/objModels' +import { Viewer } from './viewer' const { loadTexture } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils') export const TWEEN_DURATION = 120 @@ -163,12 +164,12 @@ const nametags = {} const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase() -function getEntityMesh (entity, scene, options, overrides) { +function getEntityMesh (entity, world, options, overrides) { if (entity.name) { try { // https://github.com/PrismarineJS/prismarine-viewer/pull/410 const entityName = (isFirstUpperCase(entity.name) ? snakeCase(entity.name) : entity.name).toLowerCase() - const e = new Entity.EntityMesh('1.16.4', entityName, scene, overrides) + const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides) if (e.mesh) { addNametag(entity, options, e.mesh) @@ -211,6 +212,8 @@ export class Entities extends EventEmitter { clock = new THREE.Clock() rendering = true itemsTexture: THREE.Texture | null = null + cachedMapsImages = {} as Record<number, string> + itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>> getItemUv: undefined | ((idOrName: number | string) => { texture: THREE.Texture; u: number; @@ -220,7 +223,7 @@ export class Entities extends EventEmitter { size?: number; }) - constructor (public scene: THREE.Scene) { + constructor (public viewer: Viewer) { super() this.entitiesOptions = {} this.debugMode = 'none' @@ -229,7 +232,7 @@ export class Entities extends EventEmitter { clear () { for (const mesh of Object.values(this.entities)) { - this.scene.remove(mesh) + this.viewer.scene.remove(mesh) disposeObject(mesh) } this.entities = {} @@ -251,9 +254,9 @@ export class Entities extends EventEmitter { this.rendering = rendering for (const ent of entity ? [entity] : Object.values(this.entities)) { if (rendering) { - if (!this.scene.children.includes(ent)) this.scene.add(ent) + if (!this.viewer.scene.children.includes(ent)) this.viewer.scene.add(ent) } else { - this.scene.remove(ent) + this.viewer.scene.remove(ent) } } } @@ -417,6 +420,7 @@ export class Entities extends EventEmitter { } getItemMesh (item) { + // TODO: Render proper model (especially for blocks) instead of flat texture const textureUv = this.getItemUv?.(item.itemId ?? item.blockId) if (textureUv) { // todo use geometry buffer uv instead! @@ -470,9 +474,13 @@ export class Entities extends EventEmitter { update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) { const isPlayerModel = entity.name === 'player' - if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') { + 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`}` } + if (entity.name === 'glow_item_frame') { + if (!overrides.textures) overrides.textures = [] + overrides.textures['background'] = 'block:glow_item_frame' + } // this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted) let e = this.entities[entity.id] @@ -480,7 +488,7 @@ export class Entities extends EventEmitter { if (!e) return if (e.additionalCleanup) e.additionalCleanup() this.emit('remove', entity) - this.scene.remove(e) + this.viewer.scene.remove(e) disposeObject(e) // todo dispose textures as well ? delete this.entities[entity.id] @@ -551,7 +559,7 @@ export class Entities extends EventEmitter { //@ts-expect-error playerObject.animation.isMoving = false } else { - mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides) + mesh = getEntityMesh(entity, this.viewer.world, this.entitiesOptions, overrides) } if (!mesh) return mesh.name = 'mesh' @@ -570,7 +578,7 @@ export class Entities extends EventEmitter { group.add(mesh) group.add(boxHelper) boxHelper.visible = false - this.scene.add(group) + this.viewer.scene.add(group) e = group this.entities[entity.id] = e @@ -694,31 +702,51 @@ export class Entities extends EventEmitter { } // todo handle map, map_chunks events - // if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') { - // const example = { - // "present": true, - // "itemId": 847, - // "itemCount": 1, - // "nbtData": { - // "type": "compound", - // "name": "", - // "value": { - // "map": { - // "type": "int", - // "value": 2146483444 - // }, - // "interactiveboard": { - // "type": "byte", - // "value": 1 - // } - // } - // } - // } - // const item = entity.metadata?.[8] - // if (item.nbtData) { - // const nbt = nbt.simplify(item.nbtData) - // } - // } + let itemFrameMeta = getSpecificEntityMetadata('item_frame', entity) + if (!itemFrameMeta) { + itemFrameMeta = getSpecificEntityMetadata('glow_item_frame', entity) + } + if (itemFrameMeta) { + // TODO: fix type + // todo! fix errors in mc-data (no entities data prior 1.18.2) + const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } } + mesh.scale.set(1, 1, 1) + e.rotation.x = -entity.pitch + e.children.find(c => { + if (c.name.startsWith('map_')) { + disposeObject(c) + const existingMapNumber = parseInt(c.name.split('_')[1], 10) + this.itemFrameMaps[existingMapNumber] = this.itemFrameMaps[existingMapNumber]?.filter(mesh => mesh !== c) + if (c instanceof THREE.Mesh) { + c.material?.map?.dispose() + } + return true + } else if (c.name === 'item') { + disposeObject(c) + return true + } + return false + })?.removeFromParent() + if (item && (item.itemId ?? item.blockId ?? 0) !== 0) { + const rotation = (itemFrameMeta.rotation as any as number) ?? 0 + const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data + if (mapNumber) { + // TODO: Use proper larger item frame model when a map exists + mesh.scale.set(16 / 12, 16 / 12, 1) + this.addMapModel(e, mapNumber, rotation) + } else { + const itemMesh = this.getItemMesh(item) + if (itemMesh) { + itemMesh.mesh.position.set(0, 0, 0.43) + itemMesh.mesh.scale.set(0.5, 0.5, 0.5) + itemMesh.mesh.rotateY(Math.PI) + itemMesh.mesh.rotateZ(rotation * Math.PI / 4) + itemMesh.mesh.name = 'item' + e.add(itemMesh.mesh) + } + } + } + } if (entity.username) { e.username = entity.username @@ -741,6 +769,74 @@ export class Entities extends EventEmitter { } } + updateMap (mapNumber: string | number, data: string) { + this.cachedMapsImages[mapNumber] = data + let itemFrameMeshes = this.itemFrameMaps[mapNumber] + if (!itemFrameMeshes) return + itemFrameMeshes = itemFrameMeshes.filter(mesh => mesh.parent) + this.itemFrameMaps[mapNumber] = itemFrameMeshes + if (itemFrameMeshes) { + for (const mesh of itemFrameMeshes) { + mesh.material.map = this.loadMap(data) + mesh.material.needsUpdate = true + mesh.visible = true + } + } + } + + addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) { + const imageData = this.cachedMapsImages?.[mapNumber] + let texture: THREE.Texture | null = null + if (imageData) { + texture = this.loadMap(imageData) + } + const parameters = { + transparent: true, + alphaTest: 0.1, + } + if (texture) { + parameters['map'] = texture + } + const material = new THREE.MeshLambertMaterial(parameters) + + const mapMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material) + + mapMesh.rotation.set(0, Math.PI, 0) + entityMesh.add(mapMesh) + let isInvisible = false + entityMesh.traverseVisible(c => { + if (c.name === 'geometry_frame') { + isInvisible = false + } + }) + if (isInvisible) { + mapMesh.position.set(0, 0, 0.499) + } else { + mapMesh.position.set(0, 0, 0.437) + } + mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2) + mapMesh.name = `map_${mapNumber}` + + if (!texture) { + mapMesh.visible = false + } + + if (!this.itemFrameMaps[mapNumber]) { + this.itemFrameMaps[mapNumber] = [] + } + this.itemFrameMaps[mapNumber].push(mapMesh) + } + + loadMap (data: any) { + const texture = new THREE.TextureLoader().load(data) + if (texture) { + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.needsUpdate = true + } + return texture + } + handleDamageEvent (entityId, damageAmount) { const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh') if (entityMesh) { @@ -808,7 +904,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item material.map = texture }) } else { - mesh = getMesh(texturePath, armorModels.armorModel[slotType]) + mesh = getMesh(viewer.world, texturePath, armorModels.armorModel[slotType]) mesh.name = meshName material = mesh.material material.side = THREE.DoubleSide diff --git a/prismarine-viewer/viewer/lib/entity/EntityMesh.js b/prismarine-viewer/viewer/lib/entity/EntityMesh.js index 69dd95d65..9033489af 100644 --- a/prismarine-viewer/viewer/lib/entity/EntityMesh.js +++ b/prismarine-viewer/viewer/lib/entity/EntityMesh.js @@ -94,7 +94,7 @@ function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] } -function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror = false) { +function addCube(attr, boneId, bone, cube, sameTextureForAllFaces = false, texWidth = 64, texHeight = 64, mirror = false) { const cubeRotation = new THREE.Euler(0, 0, 0) if (cube.rotation) { cubeRotation.x = -cube.rotation[0] * Math.PI / 180 @@ -107,8 +107,15 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror const eastOrWest = dir[0] !== 0 const faceUvs = [] for (const pos of corners) { - const u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth - const v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight + let u + let v + if (sameTextureForAllFaces) { + u = (cube.uv[0] + pos[3] * cube.size[0]) / texWidth + v = (cube.uv[1] + pos[4] * cube.size[1]) / texHeight + } else { + u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth + v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight + } const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0] const posY = pos[1] @@ -148,7 +155,23 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror } } -export function getMesh(texture, jsonModel, overrides = {}) { +export function getMesh(worldRenderer, texture, jsonModel, overrides = {}) { + let textureWidth = jsonModel.texturewidth ?? 64 + let textureHeight = jsonModel.textureheight ?? 64 + let textureOffset + const useBlockTexture = texture.startsWith('block:') + if (useBlockTexture) { + const blockName = texture.slice(6) + const textureInfo = worldRenderer.blocksAtlasParser.getTextureInfo(blockName) + if (textureInfo) { + textureWidth = worldRenderer.material.map.image.width + textureHeight = worldRenderer.material.map.image.height + textureOffset = [textureInfo.u, textureInfo.v] + } else { + console.error(`Unknown block ${blockName}`) + } + } + const bones = {} const geoData = { @@ -186,7 +209,7 @@ export function getMesh(texture, jsonModel, overrides = {}) { if (jsonBone.cubes) { for (const cube of jsonBone.cubes) { - addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight, jsonBone.mirror) + addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror) } } i++ @@ -215,18 +238,25 @@ export function getMesh(texture, jsonModel, overrides = {}) { mesh.bind(skeleton) mesh.scale.set(1 / 16, 1 / 16, 1 / 16) - loadTexture(texture, texture => { - if (material.map) { - // texture is already loaded - return - } - texture.magFilter = THREE.NearestFilter - texture.minFilter = THREE.NearestFilter - texture.flipY = false - texture.wrapS = THREE.RepeatWrapping - texture.wrapT = THREE.RepeatWrapping + if (textureOffset) { + texture = worldRenderer.material.map.clone() + texture.offset.set(textureOffset[0], textureOffset[1]) + texture.needsUpdate = true material.map = texture - }) + } else { + loadTexture(texture.endsWith('.png') || texture.startsWith('data:image/') ? texture : texture + '.png', texture => { + if (material.map) { + // texture is already loaded + return + } + texture.magFilter = THREE.NearestFilter + texture.minFilter = THREE.NearestFilter + texture.flipY = false + texture.wrapS = THREE.RepeatWrapping + texture.wrapT = THREE.RepeatWrapping + material.map = texture + }) + } return mesh } @@ -252,6 +282,7 @@ export const temporaryMap = { 'hopper_minecart': 'minecart', 'command_block_minecart': 'minecart', 'tnt_minecart': 'minecart', + 'glow_item_frame': 'item_frame', 'glow_squid': 'squid', 'trader_llama': 'llama', 'chest_boat': 'boat', @@ -321,7 +352,7 @@ const offsetEntity = { // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class EntityMesh { - constructor(version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) { + constructor(version, type, worldRenderer, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) { const originalType = type const mappedValue = temporaryMap[type] if (mappedValue) type = mappedValue @@ -388,7 +419,7 @@ export class EntityMesh { const texture = overrides.textures?.[name] ?? e.textures[name] if (!texture) continue // console.log(JSON.stringify(jsonModel, null, 2)) - const mesh = getMesh(texture + '.png', jsonModel, overrides) + const mesh = getMesh(worldRenderer, texture, jsonModel, overrides) mesh.name = `geometry_${name}` this.mesh.add(mesh) diff --git a/prismarine-viewer/viewer/lib/entity/entities.json b/prismarine-viewer/viewer/lib/entity/entities.json index 9824d4182..4436a44bf 100644 --- a/prismarine-viewer/viewer/lib/entity/entities.json +++ b/prismarine-viewer/viewer/lib/entity/entities.json @@ -7838,6 +7838,53 @@ } } }, + "item_frame": { + "identifier": "minecraft:item_frame", + "materials": {"default": "item_frame"}, + "textures": { + "background": "block:item_frame", + "frame": "block:oak_planks" + }, + "geometry": { + "background": { + "bones": [ + { + "name": "base" + }, + { + "name": "background", + "parent": "base", + "rotation": [0, 180, 0], + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-5, -5, -8], "size": [10, 10, 0.5], "uv": [3, 3]} + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + }, + "frame": { + "bones": [ + { + "name": "frame", + "parent": "base", + "rotation": [0, 180, 0], + "pivot": [0, 0, 0], + "cubes": [ + {"origin": [-6, -6, -8], "size": [12, 1, 1], "uv": [2, 2]}, + {"origin": [-6, 5, -8], "size": [12, 1, 1], "uv": [2, 13]}, + {"origin": [-6, -5, -8], "size": [1, 10, 1], "uv": [2, 3]}, + {"origin": [5, -5, -8], "size": [1, 10, 1], "uv": [13, 3]} + ] + } + ], + "texturewidth": 16, + "textureheight": 16 + } + }, + "render_controllers": ["controller.render.item_frame"] + }, "leash_knot": { "identifier": "minecraft:leash_knot", "materials": {"default": "leash_knot"}, @@ -7847,7 +7894,8 @@ "bones": [ { "name": "knot", - "cubes": [{"origin": [-3, 2, -3], "size": [6, 8, 6]}] + "rotation": [0, 180, 0], + "cubes": [{"origin": [5, 6, 5], "size": [6, 8, 6], "uv": [0, 0]}] } ], "texturewidth": 32, diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index c7dd7fe56..82c3e6614 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -48,7 +48,7 @@ export class Viewer { this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig) this.setWorld() this.resetScene() - this.entities = new Entities(this.scene) + this.entities = new Entities(this) // this.primitives = new Primitives(this.scene, this.camera) this.domElement = renderer.domElement diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 61d5a503a..e556f7a32 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -75,6 +75,10 @@ export class WorldDataEmitter extends EventEmitter { this.eventListeners = { // 'move': botPosition, entitySpawn (e: any) { + if (e.name === 'item_frame' || e.name === 'glow_item_frame') { + // Item frames use block positions in the protocol, not their center. Fix that. + e.position.translate(0.5, 0.5, 0.5) + } emitEntity(e) }, entityUpdate (e: any) { diff --git a/src/mineflayer/maps.ts b/src/mineflayer/maps.ts index 75169a9f6..c5d4f7165 100644 --- a/src/mineflayer/maps.ts +++ b/src/mineflayer/maps.ts @@ -16,5 +16,8 @@ setImageConverter((buf: Uint8Array) => { customEvents.on('mineflayerBotCreated', () => { bot.on('login', () => { bot.loadPlugin(mapDownloader) + bot.mapDownloader.on('new_map', ({ png, id }) => { + viewer.entities.updateMap(id, png) + }) }) }) diff --git a/src/react/HeldMapUi.tsx b/src/react/HeldMapUi.tsx index b4eaea605..4fadf64f5 100644 --- a/src/react/HeldMapUi.tsx +++ b/src/react/HeldMapUi.tsx @@ -19,7 +19,7 @@ export default () => { updateHeldMap() }) - bot.on('new_map', () => { + bot.on('new_map', ({ id }) => { // total maps: Object.keys(bot.mapDownloader.maps).length updateHeldMap() })