const { Vec3 } = require('vec3') const conv = require('../conversions') const NAMED_ENTITY_HEIGHT = 1.62 const NAMED_ENTITY_WIDTH = 0.6 const CROUCH_HEIGHT = NAMED_ENTITY_HEIGHT - 0.08 module.exports = inject const animationEvents = { 0: 'entitySwingArm', 1: 'entityHurt', 2: 'entityWake', 3: 'entityEat', 4: 'entityCriticalEffect', 5: 'entityMagicCriticalEffect' } const entityStatusEvents = { 2: 'entityHurt', 3: 'entityDead', 6: 'entityTaming', 7: 'entityTamed', 8: 'entityShakingOffWater', 10: 'entityEatingGrass', 55: 'entityHandSwap' } function inject (bot) { const { mobs, entitiesArray } = bot.registry const Entity = require('prismarine-entity')(bot.version) const Item = require('prismarine-item')(bot.version) const ChatMessage = require('prismarine-chat')(bot.registry) // ONLY 1.17 has this destroy_entity packet which is the same thing as entity_destroy packet except the entity is singular // 1.17.1 reverted this change so this is just a simpler fix bot._client.on('destroy_entity', (packet) => { bot._client.emit('entity_destroy', { entityIds: [packet.entityId] }) }) bot.findPlayer = bot.findPlayers = (filter) => { const filterFn = (entity) => { if (entity.type !== 'player') return false if (filter === null) return true if (typeof filter === 'object' && filter instanceof RegExp) { return entity.username.search(filter) !== -1 } else if (typeof filter === 'function') { return filter(entity) } else if (typeof filter === 'string') { return entity.username.toLowerCase() === filter.toLowerCase() } return false } const resultSet = Object.values(bot.entities) .filter(filterFn) if (typeof filter === 'string') { switch (resultSet.length) { case 0: return null case 1: return resultSet[0] default: return resultSet } } return resultSet } bot.players = {} bot.uuidToUsername = {} bot.entities = {} bot._playerFromUUID = (uuid) => Object.values(bot.players).find(player => player.uuid === uuid) bot.nearestEntity = (match = (entity) => { return true }) => { let best = null let bestDistance = Number.MAX_VALUE for (const entity of Object.values(bot.entities)) { if (entity === bot.entity || !match(entity)) { continue } const dist = bot.entity.position.distanceSquared(entity.position) if (dist < bestDistance) { best = entity bestDistance = dist } } return best } // Reset list of players and entities on login bot._client.on('login', (packet) => { bot.players = {} bot.uuidToUsername = {} bot.entities = {} // login bot.entity = fetchEntity(packet.entityId) bot.username = bot._client.username bot.entity.username = bot._client.username bot.entity.type = 'player' bot.entity.name = 'player' }) bot._client.on('entity_equipment', (packet) => { // entity equipment const entity = fetchEntity(packet.entityId) if (packet.equipments !== undefined) { packet.equipments.forEach(equipment => entity.setEquipment(equipment.slot, equipment.item ? Item.fromNotch(equipment.item) : null)) } else { entity.setEquipment(packet.slot, packet.item ? Item.fromNotch(packet.item) : null) } bot.emit('entityEquip', entity) }) bot._client.on('bed', (packet) => { // use bed const entity = fetchEntity(packet.entityId) entity.position.set(packet.location.x, packet.location.y, packet.location.z) bot.emit('entitySleep', entity) }) bot._client.on('animation', (packet) => { // animation const entity = fetchEntity(packet.entityId) const eventName = animationEvents[packet.animation] if (eventName) bot.emit(eventName, entity) }) bot._client.on('named_entity_spawn', (packet) => { // in case player_info packet was not sent before named_entity_spawn : ignore named_entity_spawn (see #213) if (packet.playerUUID in bot.uuidToUsername) { // spawn named entity const entity = fetchEntity(packet.entityId) entity.type = 'player' entity.name = 'player' entity.username = bot.uuidToUsername[packet.playerUUID] entity.uuid = packet.playerUUID entity.dataBlobs = packet.data if (bot.supportFeature('fixedPointPosition')) { entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32) } else if (bot.supportFeature('doublePosition')) { entity.position.set(packet.x, packet.y, packet.z) } entity.yaw = conv.fromNotchianYawByte(packet.yaw) entity.pitch = conv.fromNotchianPitchByte(packet.pitch) entity.height = NAMED_ENTITY_HEIGHT entity.width = NAMED_ENTITY_WIDTH entity.metadata = parseMetadata(packet.metadata, entity.metadata) if (bot.players[entity.username] !== undefined && !bot.players[entity.username].entity) { bot.players[entity.username].entity = entity } bot.emit('entitySpawn', entity) } }) bot.on('entityCrouch', (entity) => { entity.height = CROUCH_HEIGHT }) bot.on('entityUncrouch', (entity) => { entity.height = NAMED_ENTITY_HEIGHT }) bot._client.on('collect', (packet) => { // collect item const collector = fetchEntity(packet.collectorEntityId) const collected = fetchEntity(packet.collectedEntityId) bot.emit('playerCollect', collector, collected) }) function setEntityData (entity, type, entityData) { if (entityData === undefined) { entityData = entitiesArray.find(entity => entity.internalId === type) } if (entityData) { entity.displayName = entityData.displayName entity.entityType = entityData.id entity.name = entityData.name entity.kind = entityData.category entity.height = entityData.height entity.width = entityData.width } else { // unknown entity entity.type = 'other' entity.entityType = type entity.displayName = 'unknown' entity.name = 'unknown' entity.kind = 'unknown' } } // spawn object/vehicle on versions < 1.19, on versions > 1.19 handles all non-player entities bot._client.on('spawn_entity', (packet) => { const entity = fetchEntity(packet.entityId) const entityData = bot.registry.entities[packet.type] entity.type = entityData.type || 'object' setEntityData(entity, packet.type, entityData) if (bot.supportFeature('fixedPointPosition')) { entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32) } else if (bot.supportFeature('doublePosition')) { entity.position.set(packet.x, packet.y, packet.z) } else if (bot.supportFeature('consolidatedEntitySpawnPacket')) { entity.headPitch = conv.fromNotchianPitchByte(packet.headPitch) } entity.uuid = packet.objectUUID entity.yaw = conv.fromNotchianYawByte(packet.yaw) entity.pitch = conv.fromNotchianPitchByte(packet.pitch) entity.objectData = packet.objectData bot.emit('entitySpawn', entity) }) bot._client.on('spawn_entity_experience_orb', (packet) => { const entity = fetchEntity(packet.entityId) entity.type = 'orb' entity.name = 'experience_orb' entity.width = 0.5 entity.height = 0.5 if (bot.supportFeature('fixedPointPosition')) { entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32) } else if (bot.supportFeature('doublePosition')) { entity.position.set(packet.x, packet.y, packet.z) } entity.count = packet.count bot.emit('entitySpawn', entity) }) // This packet is removed since 1.19 and merged into spawn_entity bot._client.on('spawn_entity_living', (packet) => { // spawn mob const entity = fetchEntity(packet.entityId) entity.type = 'mob' entity.uuid = packet.entityUUID const entityData = mobs[packet.type] setEntityData(entity, packet.type, entityData) if (bot.supportFeature('fixedPointPosition')) { entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32) } else if (bot.supportFeature('doublePosition')) { entity.position.set(packet.x, packet.y, packet.z) } entity.yaw = conv.fromNotchianYawByte(packet.yaw) entity.pitch = conv.fromNotchianPitchByte(packet.pitch) entity.headPitch = conv.fromNotchianPitchByte(packet.headPitch) const notchVel = new Vec3(packet.velocityX, packet.velocityY, packet.velocityZ) entity.velocity.update(conv.fromNotchVelocity(notchVel)) entity.metadata = parseMetadata(packet.metadata, entity.metadata) bot.emit('entitySpawn', entity) }) bot._client.on('entity_velocity', (packet) => { // entity velocity const entity = fetchEntity(packet.entityId) const notchVel = new Vec3(packet.velocityX, packet.velocityY, packet.velocityZ) entity.velocity.update(conv.fromNotchVelocity(notchVel)) }) bot._client.on('entity_destroy', (packet) => { // destroy entity packet.entityIds.forEach((id) => { const entity = fetchEntity(id) bot.emit('entityGone', entity) entity.isValid = false if (entity.username && bot.players[entity.username]) { bot.players[entity.username].entity = null } delete bot.entities[id] }) }) bot._client.on('rel_entity_move', (packet) => { // entity relative move const entity = fetchEntity(packet.entityId) if (bot.supportFeature('fixedPointDelta')) { entity.position.translate(packet.dX / 32, packet.dY / 32, packet.dZ / 32) } else if (bot.supportFeature('fixedPointDelta128')) { entity.position.translate(packet.dX / (128 * 32), packet.dY / (128 * 32), packet.dZ / (128 * 32)) } bot.emit('entityMoved', entity) }) bot._client.on('entity_look', (packet) => { // entity look const entity = fetchEntity(packet.entityId) entity.yaw = conv.fromNotchianYawByte(packet.yaw) entity.pitch = conv.fromNotchianPitchByte(packet.pitch) bot.emit('entityMoved', entity) }) bot._client.on('entity_move_look', (packet) => { // entity look and relative move const entity = fetchEntity(packet.entityId) if (bot.supportFeature('fixedPointDelta')) { entity.position.translate(packet.dX / 32, packet.dY / 32, packet.dZ / 32) } else if (bot.supportFeature('fixedPointDelta128')) { entity.position.translate(packet.dX / (128 * 32), packet.dY / (128 * 32), packet.dZ / (128 * 32)) } entity.yaw = conv.fromNotchianYawByte(packet.yaw) entity.pitch = conv.fromNotchianPitchByte(packet.pitch) bot.emit('entityMoved', entity) }) bot._client.on('entity_teleport', (packet) => { // entity teleport const entity = fetchEntity(packet.entityId) if (bot.supportFeature('fixedPointPosition')) { entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32) } if (bot.supportFeature('doublePosition')) { entity.position.set(packet.x, packet.y, packet.z) } entity.yaw = conv.fromNotchianYawByte(packet.yaw) entity.pitch = conv.fromNotchianPitchByte(packet.pitch) bot.emit('entityMoved', entity) }) bot._client.on('entity_head_rotation', (packet) => { // entity head look const entity = fetchEntity(packet.entityId) entity.headYaw = conv.fromNotchianYawByte(packet.headYaw) bot.emit('entityMoved', entity) }) bot._client.on('entity_status', (packet) => { // entity status const entity = fetchEntity(packet.entityId) const eventName = entityStatusEvents[packet.entityStatus] if (eventName === 'entityHandSwap' && entity.equipment) { [entity.equipment[0], entity.equipment[1]] = [entity.equipment[1], entity.equipment[0]] entity.heldItem = entity.equipment[0] // Update held item like prismarine-entity does upon equipment updates } if (eventName) bot.emit(eventName, entity) }) bot._client.on('attach_entity', (packet) => { // attach entity const entity = fetchEntity(packet.entityId) if (packet.vehicleId === -1) { const vehicle = entity.vehicle delete entity.vehicle bot.emit('entityDetach', entity, vehicle) } else { entity.vehicle = fetchEntity(packet.vehicleId) bot.emit('entityAttach', entity, entity.vehicle) } }) bot.fireworkRocketDuration = 0 function setElytraFlyingState (entity, elytraFlying) { let startedFlying = false if (elytraFlying) { startedFlying = !entity.elytraFlying entity.elytraFlying = true } else if (entity.elytraFlying) { entity.elytraFlying = false } if (bot.fireworkRocketDuration !== 0 && entity.id === bot.entity?.id && !elytraFlying) { bot.fireworkRocketDuration = 0 knownFireworks.splice(0, knownFireworks.length) } if (startedFlying) { bot.emit('entityElytraFlew', entity) } } const knownFireworks = [] function handleBotUsedFireworkRocket (fireworkEntityId, fireworkInfo) { if (knownFireworks.includes(fireworkEntityId)) return knownFireworks.push(fireworkEntityId) let flightDur = 1 if (fireworkInfo?.nbtData != null) { let nbt = fireworkInfo.nbtData if (nbt.type === 'compound' && nbt.value.Fireworks != null) { nbt = nbt.value.Fireworks if (nbt.type === 'compound' && nbt.value.Flight != null) { nbt = nbt.value.Flight if (nbt.type === 'int') { flightDur += nbt.value } } } } const baseDuration = 10 * flightDur const randomDuration = Math.floor(Math.random() * 6) + Math.floor(Math.random() * 7) bot.fireworkRocketDuration = baseDuration + randomDuration bot.emit('usedFirework') } let fireworkEntityName if (bot.supportFeature('fireworkNamePlural')) { fireworkEntityName = 'fireworks_rocket' } else if (bot.supportFeature('fireworkNameSingular')) { fireworkEntityName = 'firework_rocket' } let fireworkMetadataIdx let fireworkMetadataIsOpt if (bot.supportFeature('fireworkMetadataVarInt7')) { fireworkMetadataIdx = 7 fireworkMetadataIsOpt = false } else if (bot.supportFeature('fireworkMetadataOptVarInt8')) { fireworkMetadataIdx = 8 fireworkMetadataIsOpt = true } else if (bot.supportFeature('fireworkMetadataOptVarInt9')) { fireworkMetadataIdx = 9 fireworkMetadataIsOpt = true } const hasFireworkSupport = fireworkEntityName !== undefined && fireworkMetadataIdx !== undefined && fireworkMetadataIsOpt !== undefined bot._client.on('entity_metadata', (packet) => { // entity metadata const entity = fetchEntity(packet.entityId) const metadata = parseMetadata(packet.metadata, entity.metadata) entity.metadata = metadata bot.emit('entityUpdate', entity) if (bot.supportFeature('mcDataHasEntityMetadata')) { const metadataKeys = bot.registry.entitiesByName[entity.name]?.metadataKeys const metas = metadataKeys ? Object.fromEntries(packet.metadata.map(e => [metadataKeys[e.key], e.value])) : {} if (packet.metadata.some(m => m.type === 'item_stack')) { bot.emit('itemDrop', entity) } if (metas.sleeping_pos || metas.pose === 2) { bot.emit('entitySleep', entity) } if (hasFireworkSupport && fireworkEntityName === entity.name && metas.attached_to_target !== undefined) { // fireworkMetadataOptVarInt9 and later is implied by // mcDataHasEntityMetadata, so no need to check metadata index and type // (eg fireworkMetadataOptVarInt8) if (metas.attached_to_target !== 0) { const entityId = metas.attached_to_target - 1 if (entityId === bot.entity?.id) { handleBotUsedFireworkRocket(entity.id, metas.fireworks_item) } } } if (metas.shared_flags != null) { if (bot.supportFeature('hasElytraFlying')) { const elytraFlying = metas.shared_flags & 0x80 setElytraFlyingState(entity, Boolean(elytraFlying)) } if (metas.shared_flags & 2) { entity.crouching = true bot.emit('entityCrouch', entity) } else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event entity.crouching = false bot.emit('entityUncrouch', entity) } } } else { const typeSlot = (bot.supportFeature('itemsAreAlsoBlocks') ? 5 : 6) + (bot.supportFeature('entityMetadataHasLong') ? 1 : 0) const slot = packet.metadata.find(e => e.type === typeSlot) if (entity.name && (entity.name.toLowerCase() === 'item' || entity.name === 'item_stack') && slot) { bot.emit('itemDrop', entity) } const typePose = bot.supportFeature('entityMetadataHasLong') ? 19 : 18 const pose = packet.metadata.find(e => e.type === typePose) if (pose && pose.value === 2) { bot.emit('entitySleep', entity) } if (hasFireworkSupport && fireworkEntityName === entity.name) { const attachedToTarget = packet.metadata.find(e => e.key === fireworkMetadataIdx) if (attachedToTarget !== undefined) { let entityId if (fireworkMetadataIsOpt) { if (attachedToTarget.value !== 0) { entityId = attachedToTarget.value - 1 } // else, not attached to an entity } else { entityId = attachedToTarget.value } if (entityId !== undefined && entityId === bot.entity?.id) { const fireworksItem = packet.metadata.find(e => e.key === (fireworkMetadataIdx - 1)) handleBotUsedFireworkRocket(entity.id, fireworksItem?.value) } } } const bitField = packet.metadata.find(p => p.key === 0) if (bitField !== undefined) { if (bot.supportFeature('hasElytraFlying')) { const elytraFlying = bitField.value & 0x80 setElytraFlyingState(entity, Boolean(elytraFlying)) } if ((bitField.value & 2) !== 0) { entity.crouching = true bot.emit('entityCrouch', entity) } else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event entity.crouching = false bot.emit('entityUncrouch', entity) } } } }) bot._client.on('entity_effect', (packet) => { // entity effect const entity = fetchEntity(packet.entityId) const effect = { id: packet.effectId, amplifier: packet.amplifier, duration: packet.duration } entity.effects[effect.id] = effect bot.emit('entityEffect', entity, effect) }) bot._client.on('remove_entity_effect', (packet) => { // remove entity effect const entity = fetchEntity(packet.entityId) let effect = entity.effects[packet.effectId] if (effect) { delete entity.effects[effect.id] } else { // unknown effect effect = { id: packet.effectId, amplifier: -1, duration: -1 } } bot.emit('entityEffectEnd', entity, effect) }) const updateAttributes = (packet) => { const entity = fetchEntity(packet.entityId) if (!entity.attributes) entity.attributes = {} for (const prop of packet.properties) { entity.attributes[prop.key] = { value: prop.value, modifiers: prop.modifiers } } bot.emit('entityAttributes', entity) } bot._client.on('update_attributes', updateAttributes) // 1.8 bot._client.on('entity_update_attributes', updateAttributes) // others bot._client.on('spawn_entity_weather', (packet) => { // spawn global entity const entity = fetchEntity(packet.entityId) entity.type = 'global' entity.globalType = 'thunderbolt' entity.uuid = packet.entityUUID entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32) bot.emit('entitySpawn', entity) }) bot.on('spawn', () => { bot.emit('entitySpawn', bot.entity) }) bot._client.on('player_info', (packet) => { // player list item(s) if (bot.supportFeature('playerInfoActionIsBitfield')) { for (const item of packet.data) { let player = bot.uuidToUsername[item.uuid] ? bot.players[bot.uuidToUsername[item.uuid]] : null let newPlayer = false const obj = { uuid: item.uuid } if (!player) newPlayer = true player = player || obj if (packet.action & 1) { obj.username = item.player.name obj.displayName = player.displayName || new ChatMessage({ text: '', extra: [{ text: item.player.name }] }) obj.skinData = extractSkinInformation(item.player.properties) } if (packet.action & 4) { obj.gamemode = item.gamemode } if (packet.action & 16) { obj.ping = item.latency } if (item.displayName) { obj.displayName = new ChatMessage(JSON.parse(item.displayName)) } else if (packet.action & 32) obj.displayName = new ChatMessage({ text: '', extra: [{ text: player.username || obj.username }] }) if (newPlayer) { if (!obj.username) continue // Should be unreachable player = bot.players[obj.username] = obj bot.uuidToUsername[obj.uuid] = obj.username } else { Object.assign(player, obj) } const playerEntity = Object.values(bot.entities).find(e => e.type === 'player' && e.username === player.username) player.entity = playerEntity if (playerEntity === bot.entity) { bot.player = player } if (newPlayer) { bot.emit('playerJoined', player) } else { bot.emit('playerUpdated', player) } } } else { for (const item of packet.data) { let player = bot.uuidToUsername[item.UUID] ? bot.players[bot.uuidToUsername[item.UUID]] : null if (packet.action === 0) { let newPlayer = false // New Player if (!player) { player = bot.players[item.name] = { username: item.name, ping: item.ping, uuid: item.UUID, displayName: new ChatMessage({ text: '', extra: [{ text: item.name }] }), skinData: extractSkinInformation(item.properties), profileKeys: item.crypto ? { publicKey: item.crypto.publicKey, // DER-encoded public key signature: item.crypto.signature // Signature } : null } bot.uuidToUsername[item.UUID] = item.name bot.emit('playerJoined', player) newPlayer = true } else { // Just an Update player.gamemode = item.gamemode player.ping = item.ping player.skinData = extractSkinInformation(item.properties) if (item.crypto) { player.profileKeys = { publicKey: item.crypto.publicKey, signature: item.crypto.signature } } } if (item.displayName) { player.displayName = new ChatMessage(JSON.parse(item.displayName)) } const playerEntity = Object.values(bot.entities).find(e => e.type === 'player' && e.username === item.name) player.entity = playerEntity if (playerEntity === bot.entity) { bot.player = player } if (!newPlayer) { bot.emit('playerUpdated', player) } } else if (player) { if (packet.action === 1) { player.gamemode = item.gamemode } else if (packet.action === 2) { player.ping = item.ping } else if (packet.action === 3 && !item.displayName) { player.displayName = new ChatMessage({ text: '', extra: [{ text: player.username }] }) } else if (packet.action === 3 && item.displayName) { player.displayName = new ChatMessage(JSON.parse(item.displayName)) } else if (packet.action === 4) { if (player.entity === bot.entity) continue player.entity = null delete bot.players[player.username] delete bot.uuidToUsername[item.UUID] bot.emit('playerLeft', player) continue } else { continue } bot.emit('playerUpdated', player) } } } }) // (1.19.3) player(s) leave the game bot._client.on('player_remove', (packet) => { for (const uuid of packet.players) { const player = bot.uuidToUsername[uuid] ? bot.players[bot.uuidToUsername[uuid]] : null if (!player || player.entity === bot.entity) continue player.entity = null delete bot.players[player.username] delete bot.uuidToUsername[uuid] bot.emit('playerLeft', player) } }) // attaching to a vehicle bot._client.on('attach_entity', (packet) => { if (packet.entityId !== bot.entity.id) return const vehicle = bot.vehicle if (packet.vehicleId === -1) { bot.vehicle = null bot.emit('dismount', vehicle) } else { bot.vehicle = bot.entities[packet.vehicleId] bot.emit('mount') } }) bot._client.on('set_passengers', ({ entityId, passengers }) => { if (passengers[0] !== bot.entity.id) return const vehicle = bot.vehicle if (entityId === -1) { bot.vehicle = null bot.emit('dismount', vehicle) } else { bot.vehicle = bot.entities[entityId] bot.emit('mount') } }) bot.swingArm = swingArm bot.attack = attack bot.mount = mount bot.dismount = dismount bot.useOn = useOn bot.moveVehicle = moveVehicle function swingArm (arm = 'right', showHand = true) { const hand = arm === 'right' ? 0 : 1 const packet = {} if (showHand) packet.hand = hand bot._client.write('arm_animation', packet) } function useOn (target) { // TODO: check if not crouching will make make this action always use the item useEntity(target, 0) } function attack (target, swing = true) { // arm animation comes before the use_entity packet on 1.8 if (bot.supportFeature('armAnimationBeforeUse')) { if (swing) { swingArm() } useEntity(target, 1) } else { useEntity(target, 1) if (swing) { swingArm() } } } function mount (target) { // TODO: check if crouching will make make this action always mount useEntity(target, 0) } function moveVehicle (left, forward) { bot._client.write('steer_vehicle', { sideways: left, forward, jump: 0x01 }) } function dismount () { if (bot.vehicle) { bot._client.write('steer_vehicle', { sideways: 0.0, forward: 0.0, jump: 0x02 }) } else { bot.emit('error', new Error('dismount: not mounted')) } } function useEntity (target, leftClick, x, y, z) { const sneaking = bot.getControlState('sneak') if (x && y && z) { bot._client.write('use_entity', { target: target.id, mouse: leftClick, x, y, z, sneaking }) } else { bot._client.write('use_entity', { target: target.id, mouse: leftClick, sneaking }) } } function fetchEntity (id) { return bot.entities[id] || (bot.entities[id] = new Entity(id)) } } function parseMetadata (metadata, entityMetadata = {}) { if (metadata !== undefined) { for (const { key, value } of metadata) { entityMetadata[key] = value } } return entityMetadata } function extractSkinInformation (properties) { if (!properties) { return undefined } const props = Object.fromEntries(properties.map((e) => [e.name, e])) if (!props.textures || !props.textures.value) { return undefined } const skinTexture = JSON.parse(Buffer.from(props.textures.value, 'base64').toString('utf8')) const skinTextureUrl = skinTexture?.textures?.SKIN?.url ?? undefined const skinTextureModel = skinTexture?.textures?.SKIN?.metadata?.model ?? undefined if (!skinTextureUrl) { return undefined } return { url: skinTextureUrl, model: skinTextureModel } }