Geyser/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java

435 lines
19 KiB
Java
Raw Normal View History

2019-08-03 03:38:09 +00:00
/*
2022-01-01 19:03:05 +00:00
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
2019-08-03 03:38:09 +00:00
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.entity.type.player;
2019-08-03 03:38:09 +00:00
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
2021-11-18 03:02:38 +00:00
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition;
import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
2020-06-23 00:11:09 +00:00
import com.nukkitx.protocol.bedrock.data.AttributeData;
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
2020-06-23 00:11:09 +00:00
import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
import com.nukkitx.protocol.bedrock.packet.*;
2019-08-03 03:38:09 +00:00
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.entity.EntityDefinitions;
2021-12-11 21:05:12 +00:00
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Score;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
2021-12-11 21:05:12 +00:00
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.text.MessageTranslator;
2019-08-03 03:38:09 +00:00
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
2019-08-03 06:51:05 +00:00
@Getter @Setter
public class PlayerEntity extends LivingEntity {
2021-12-26 03:46:16 +00:00
public static final float SNEAKING_POSE_HEIGHT = 1.5f;
private GameProfile profile;
private String username;
Fix Skin Caching and Fix Skin Restorer (#680) * Fix Skin Caching Changes: * Instead of caching a skin based upon the player we cached it based upon the textureURL. This means multiple players with the same skin will benefit from the cache and more importantly will mean a player changing their skin will not get a false cache hit. * This should fix all issues with SkinRestorer and will now correctly show the skin both to the player themselves and to other players Closes #518 * Remove duplicated code * Minimize playerlist updates Changes: * All async skin stuff will now just update skins and not be involved with sending the session to the player. This eliminates issues where the player list changes whilst an async task is occuring plus it means no invisible players while retrieving skin. * Fix bug when retrieving cached skin * When sending PlayerList packets ensure the skins have appropriate skinIds so the Bedrock client will cache hit/miss as needed * Make sure to add and remove player when setting skin if they do not belong on the playerlist * Make use of AuthData UUID when removing the player * Revert removal of checking if entity is valid when initialized This section is supposed to send all spawned entities in the java world to a player only after they've initialized. By removing this check it would also be sending entities that exist but are not spawned. * Optimizations Changes: * Check for duplicate requests based on textureURL instead of player ID * Don't use the PlayerSkinPacket. It duplicates the data sent in the PlayerListPacket and without it the players still get skin updates. * Support caching of skins to disk based on configuration variable If a skin is downloaded it will be saved to `cache/skins` using a base64 encoded filename of the textureUrl, if allowed by setting a non 0 value for the configuration variable `cache-skins` When reading a skin we try load it from a cache file first before trying to download it. We don't yet expire them but do update their last modification so we know which ones have been accessed. * Update `config.yml` with cache-skins directive, defaulting to disabled * Merge Fixes * Cache all images instead of just skins Changes: * Move the image caching from skins to where images may get downloaded so this also covers capes and anything else that uses the same method of image retrieval * Updated config value from `cache-skins` to `cache-images` * Updated cache location from `cache/skins` to `cache/images` * Images are stored in png format with a uuid. This may make debugging easier as they can be directly opened. * Implement cached image expiry If `cache-images` is set to a value greater than 0 then a scheduled task will occur once a day that will remove images with a modification date older than the value in days. * Force skin changes as trusted * Resolve PR queries * Fix signed int causing issues calculating expiry time for images * Reset Defaults to 0 and implement Google Timed Eviction cache for Images * Add memory cache for Capes Co-authored-by: Brendan Grieve <brendan.grieve@zepli.com.au> Co-authored-by: bundabrg <bundabrg@grieve.com.au>
2020-08-07 16:33:21 +00:00
private boolean playerList = true; // Player is in the player list
2019-08-03 03:38:09 +00:00
private Vector3i bedPosition;
/**
* Saves the parrot currently on the player's left shoulder; otherwise null
*/
private ParrotEntity leftParrot;
/**
* Saves the parrot currently on the player's right shoulder; otherwise null
*/
private ParrotEntity rightParrot;
public PlayerEntity(GeyserSession session, int entityId, long geyserId, GameProfile gameProfile, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
2021-11-18 03:02:38 +00:00
super(session, entityId, geyserId, gameProfile.getId(), EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw);
2019-08-03 03:38:09 +00:00
profile = gameProfile;
username = gameProfile.getName();
2021-11-18 03:02:38 +00:00
}
2021-11-18 03:02:38 +00:00
@Override
protected void initializeMetadata() {
super.initializeMetadata();
// For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior
2021-11-18 03:02:38 +00:00
dirtyMetadata.put(EntityData.MARK_VARIANT, 0xff);
2019-08-03 03:38:09 +00:00
}
@Override
2021-11-18 03:02:38 +00:00
public void spawnEntity() {
// Check to see if the player should have a belowname counterpart added
Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
if (objective != null) {
2021-11-18 03:02:38 +00:00
setBelowNameText(objective);
}
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
2021-11-18 03:02:38 +00:00
updateDisplayName(null, false);
2019-08-03 03:38:09 +00:00
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
2019-08-03 06:51:05 +00:00
addPlayerPacket.setUuid(uuid);
addPlayerPacket.setUsername(username);
addPlayerPacket.setRuntimeEntityId(geyserId);
addPlayerPacket.setUniqueEntityId(geyserId);
2021-11-18 03:02:38 +00:00
addPlayerPacket.setPosition(position.sub(0, definition.offset(), 0));
addPlayerPacket.setRotation(getBedrockRotation());
addPlayerPacket.setMotion(motion);
2019-08-03 03:38:09 +00:00
addPlayerPacket.setHand(hand);
addPlayerPacket.getAdventureSettings().setCommandPermission(CommandPermission.NORMAL);
addPlayerPacket.getAdventureSettings().setPlayerPermission(PlayerPermission.MEMBER);
addPlayerPacket.setDeviceId("");
addPlayerPacket.setPlatformChatId("");
2021-11-18 03:02:38 +00:00
addPlayerPacket.getMetadata().putFlags(flags);
dirtyMetadata.apply(addPlayerPacket.getMetadata());
2021-11-18 03:02:38 +00:00
setFlagsDirty(false);
2019-08-03 06:51:05 +00:00
valid = true;
session.sendUpstreamPacket(addPlayerPacket);
2019-08-03 03:38:09 +00:00
}
2021-11-18 03:02:38 +00:00
public void sendPlayer() {
if (session.getEntityCache().getPlayerEntity(uuid) == null)
return;
2021-11-13 16:03:55 +00:00
if (session.getEntityCache().getEntityByGeyserId(geyserId) == null) {
session.getEntityCache().spawnEntity(this);
} else {
2021-11-18 03:02:38 +00:00
spawnEntity();
}
}
@Override
2021-11-18 03:02:38 +00:00
public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
setPosition(position);
2021-11-18 03:02:38 +00:00
setYaw(yaw);
setPitch(pitch);
setHeadYaw(headYaw);
setOnGround(isOnGround);
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(geyserId);
movePlayerPacket.setPosition(this.position);
movePlayerPacket.setRotation(getBedrockRotation());
movePlayerPacket.setOnGround(isOnGround);
movePlayerPacket.setMode(teleported ? MovePlayerPacket.Mode.TELEPORT : MovePlayerPacket.Mode.NORMAL);
if (teleported) {
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.UNKNOWN);
}
session.sendUpstreamPacket(movePlayerPacket);
if (leftParrot != null) {
2021-11-18 03:02:38 +00:00
leftParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
}
if (rightParrot != null) {
2021-11-18 03:02:38 +00:00
rightParrot.moveAbsolute(position, yaw, pitch, headYaw, true, teleported);
}
}
@Override
2021-11-18 03:02:38 +00:00
public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
setYaw(yaw);
setPitch(pitch);
setHeadYaw(headYaw);
this.position = Vector3f.from(position.getX() + relX, position.getY() + relY, position.getZ() + relZ);
setOnGround(isOnGround);
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(geyserId);
movePlayerPacket.setPosition(position);
movePlayerPacket.setRotation(getBedrockRotation());
movePlayerPacket.setOnGround(isOnGround);
movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL);
// If the player is moved while sleeping, we have to adjust their y, so it appears
// correctly on Bedrock. This fixes GSit's lay.
if (getFlag(EntityFlag.SLEEPING)) {
if (bedPosition != null && (bedPosition.getY() == 0 || bedPosition.distanceSquared(position.toInt()) > 4)) {
// Force the player movement by using a teleport
2021-11-18 03:02:38 +00:00
movePlayerPacket.setPosition(Vector3f.from(position.getX(), position.getY() - definition.offset() + 0.2f, position.getZ()));
movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.UNKNOWN);
}
}
session.sendUpstreamPacket(movePlayerPacket);
if (leftParrot != null) {
2021-11-18 03:02:38 +00:00
leftParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true);
}
if (rightParrot != null) {
2021-11-18 03:02:38 +00:00
rightParrot.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, true);
}
}
@Override
2021-11-18 03:02:38 +00:00
public void updateHeadLookRotation(float headYaw) {
moveRelative(0, 0, 0, yaw, pitch, headYaw, onGround);
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(geyserId);
movePlayerPacket.setPosition(position);
movePlayerPacket.setRotation(getBedrockRotation());
2020-06-23 00:11:09 +00:00
movePlayerPacket.setMode(MovePlayerPacket.Mode.HEAD_ROTATION);
session.sendUpstreamPacket(movePlayerPacket);
}
@Override
2021-11-18 03:02:38 +00:00
public void updatePositionAndRotation(double moveX, double moveY, double moveZ, float yaw, float pitch, boolean isOnGround) {
moveRelative(moveX, moveY, moveZ, yaw, pitch, isOnGround);
if (leftParrot != null) {
2021-11-18 03:02:38 +00:00
leftParrot.moveRelative(moveX, moveY, moveZ, yaw, pitch, isOnGround);
}
if (rightParrot != null) {
2021-11-18 03:02:38 +00:00
rightParrot.moveRelative(moveX, moveY, moveZ, yaw, pitch, isOnGround);
}
}
@Override
2021-11-18 03:02:38 +00:00
public void updateRotation(float yaw, float pitch, boolean isOnGround) {
super.updateRotation(yaw, pitch, isOnGround);
// Both packets need to be sent or else player head rotation isn't correctly updated
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
movePlayerPacket.setRuntimeEntityId(geyserId);
movePlayerPacket.setPosition(position);
movePlayerPacket.setRotation(getBedrockRotation());
movePlayerPacket.setOnGround(isOnGround);
2020-06-23 00:11:09 +00:00
movePlayerPacket.setMode(MovePlayerPacket.Mode.HEAD_ROTATION);
session.sendUpstreamPacket(movePlayerPacket);
if (leftParrot != null) {
2021-11-18 03:02:38 +00:00
leftParrot.updateRotation(yaw, pitch, isOnGround);
}
if (rightParrot != null) {
2021-11-18 03:02:38 +00:00
rightParrot.updateRotation(yaw, pitch, isOnGround);
}
}
@Override
public void setPosition(Vector3f position) {
2021-11-18 03:02:38 +00:00
super.setPosition(position.add(0, definition.offset(), 0));
}
@Override
public Vector3i setBedPosition(EntityMetadata<Optional<Position>, ?> entityMetadata) {
return bedPosition = super.setBedPosition(entityMetadata);
}
public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) {
// Extra hearts - is not metadata but an attribute on Bedrock
2021-11-18 03:02:38 +00:00
UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket();
attributesPacket.setRuntimeEntityId(geyserId);
// Setting to a higher maximum since plugins/datapacks can probably extend the Bedrock soft limit
attributesPacket.setAttributes(Collections.singletonList(
new AttributeData("minecraft:absorption", 0.0f, 1024f, entityMetadata.getPrimitiveValue(), 0.0f)));
2021-11-18 03:02:38 +00:00
session.sendUpstreamPacket(attributesPacket);
}
public void setSkinVisibility(ByteEntityMetadata entityMetadata) {
2021-11-18 03:02:38 +00:00
// OptionalPack usage for toggling skin bits
// In Java Edition, a bit being set means that part should be enabled
// However, to ensure that the pack still works on other servers, we invert the bit so all values by default
// are true (0).
dirtyMetadata.put(EntityData.MARK_VARIANT, ~entityMetadata.getPrimitiveValue() & 0xff);
2021-11-18 03:02:38 +00:00
}
public void setLeftParrot(EntityMetadata<CompoundTag, ?> entityMetadata) {
2021-11-18 03:02:38 +00:00
setParrot(entityMetadata.getValue(), true);
}
public void setRightParrot(EntityMetadata<CompoundTag, ?> entityMetadata) {
2021-11-18 03:02:38 +00:00
setParrot(entityMetadata.getValue(), false);
}
/**
* Sets the parrot occupying the shoulder. Bedrock Edition requires a full entity whereas Java Edition just
* spawns it from the NBT data provided
*/
private void setParrot(CompoundTag tag, boolean isLeft) {
if (tag != null && !tag.isEmpty()) {
if ((isLeft && leftParrot != null) || (!isLeft && rightParrot != null)) {
// No need to update a parrot's data when it already exists
return;
}
// The parrot is a separate entity in Bedrock, but part of the player entity in Java //TODO is a UUID provided in NBT?
ParrotEntity parrot = new ParrotEntity(session, 0, session.getEntityCache().getNextEntityId().incrementAndGet(),
null, EntityDefinitions.PARROT, position, motion, yaw, pitch, headYaw);
parrot.spawnEntity();
parrot.getDirtyMetadata().put(EntityData.VARIANT, tag.get("Variant").getValue());
// Different position whether the parrot is left or right
float offset = isLeft ? 0.4f : -0.4f;
parrot.getDirtyMetadata().put(EntityData.RIDER_SEAT_POSITION, Vector3f.from(offset, -0.22, -0.1));
parrot.getDirtyMetadata().put(EntityData.RIDER_ROTATION_LOCKED, 1);
parrot.updateBedrockMetadata();
SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
EntityLinkData.Type type = isLeft ? EntityLinkData.Type.RIDER : EntityLinkData.Type.PASSENGER;
linkPacket.setEntityLink(new EntityLinkData(geyserId, parrot.getGeyserId(), type, false, false));
// Delay, or else spawned-in players won't get the link
// TODO: Find a better solution.
session.scheduleInEventLoop(() -> session.sendUpstreamPacket(linkPacket), 500, TimeUnit.MILLISECONDS);
if (isLeft) {
leftParrot = parrot;
} else {
rightParrot = parrot;
}
} else {
Entity parrot = isLeft ? leftParrot : rightParrot;
if (parrot != null) {
parrot.despawnEntity();
if (isLeft) {
2021-11-18 03:02:38 +00:00
leftParrot = null;
} else {
2021-11-18 03:02:38 +00:00
rightParrot = null;
}
}
}
}
@Override
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// Doesn't do anything for players
}
//todo this will become common entity logic once UUID support is implemented for them
/**
* @param useGivenTeam even if there is no team, update the username in the entity metadata anyway, and don't look for a team
*/
2021-11-18 03:02:38 +00:00
public void updateDisplayName(@Nullable Team team, boolean useGivenTeam) {
if (team == null && !useGivenTeam) {
// Only search for the team if we are not supposed to use the given team
// If the given team is null, this is intentional that we are being removed from the team
team = session.getWorldCache().getScoreboard().getTeamFor(username);
}
boolean needsUpdate;
String newDisplayName = this.username;
if (team != null) {
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
TeamColor color = team.getColor();
2021-12-13 18:24:58 +00:00
String chatColor = MessageTranslator.toChatColor(color);
// We have to emulate what modern Java text already does for us and add the color to each section
String prefix = team.getCurrentData().getPrefix();
String suffix = team.getCurrentData().getSuffix();
newDisplayName = chatColor + prefix + chatColor + this.username + chatColor + suffix;
} else {
// The name is not visible to the session player; clear name
newDisplayName = "";
}
needsUpdate = useGivenTeam && !newDisplayName.equals(nametag);
nametag = newDisplayName;
2021-11-18 03:02:38 +00:00
dirtyMetadata.put(EntityData.NAMETAG, newDisplayName);
} else if (useGivenTeam) {
// The name has reset, if it was previously something else
needsUpdate = !newDisplayName.equals(nametag);
2021-11-18 03:02:38 +00:00
dirtyMetadata.put(EntityData.NAMETAG, this.username);
} else {
needsUpdate = false;
}
if (needsUpdate) {
// Update the metadata as it won't be updated later
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.getMetadata().put(EntityData.NAMETAG, newDisplayName);
packet.setRuntimeEntityId(geyserId);
session.sendUpstreamPacket(packet);
}
}
@Override
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
// Doesn't do anything for players
}
@Override
protected void setDimensions(Pose pose) {
float height;
float width;
switch (pose) {
case SNEAKING -> {
height = SNEAKING_POSE_HEIGHT;
width = definition.width();
}
case FALL_FLYING, SPIN_ATTACK, SWIMMING -> {
height = 0.6f;
width = definition.width();
}
case DYING -> {
height = 0.2f;
width = 0.2f;
}
default -> {
super.setDimensions(pose);
return;
}
}
setBoundingBoxWidth(width);
setBoundingBoxHeight(height);
}
2021-11-18 03:02:38 +00:00
public void setBelowNameText(Objective objective) {
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) {
int amount;
Score score = objective.getScores().get(username);
if (score != null) {
amount = score.getCurrentData().getScore();
} else {
amount = 0;
}
String displayString = amount + " " + objective.getDisplayName();
if (valid) {
// Already spawned - we still need to run the rest of this code because the spawn packet will be
// providing the information
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityData.SCORE_TAG, displayString);
session.sendUpstreamPacket(packet);
}
} else if (valid) {
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityData.SCORE_TAG, "");
session.sendUpstreamPacket(packet);
}
}
2019-08-03 03:38:09 +00:00
}