diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 0ba1c12771c..cf544d480c8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -136,6 +136,11 @@ public void updateRotation(float yaw, float pitch, boolean isOnGround) { moveRelative(0, 0, 0, yaw + 90, 0, 0, isOnGround); } + @Override + public Vector3f position() { + return this.position.down(definition.offset()); + } + public void setPaddlingLeft(BooleanEntityMetadata entityMetadata) { isPaddlingLeft = entityMetadata.getPrimitiveValue(); if (!isPaddlingLeft) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index b03dea3ed56..54eba7b0891 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -412,6 +412,24 @@ public void setFlags(ByteEntityMetadata entityMetadata) { setInvisible((xd & 0x20) == 0x20); } + /** + * Gets the position of the entity without it being offset. + * + * @return the position of the entity without the offset value + */ + public Vector3f position() { + return this.position; + } + + /** + * Tests whether an entity can be pushed by a player. For player entities, the team they're in determines if they can be pushed. + * @param session the Bedrock client session + * @return if this entity can collide with the session's player + */ + public boolean isPushable(GeyserSession session) { + return false; + } + /** * Set a boolean - whether the entity is invisible or visible * diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java index 49eb9ddc494..68666d723e1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java @@ -124,6 +124,12 @@ protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, .thenApply(BlockStateValues::getWaterLevel); } + @Override + public Vector3f position() { + float offset = definition.offset(); + return this.position.down(waterLevel.join() == 0 ? -offset : offset); + } + @Override protected float getGravity() { if (getFlag(EntityFlag.HAS_GRAVITY) && !isOnGround() && !isInWater()) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 92048eb250e..7d40aed1348 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -46,9 +46,14 @@ import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.block.type.TrapDoorBlock; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.BlockTag; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.EntityUtils; @@ -278,6 +283,29 @@ public void setParticles(ObjectEntityMetadata> entityMetadata) { } } + public boolean isOnClimbableBlock() { + Vector3i blockPos = this.position().toInt(); + BlockState state = session.getGeyser().getWorldManager().blockAt(session, blockPos); + if (session.getTagCache().is(BlockTag.CLIMBABLE, state.block())) { + return true; + } + + if (state.block() instanceof TrapDoorBlock) { + if (!state.getValue(Properties.OPEN)) { + return false; + } else { + BlockState belowState = session.getGeyser().getWorldManager().blockAt(session, blockPos.down()); + return belowState.is(Blocks.LADDER) && belowState.getValue(Properties.HORIZONTAL_FACING) == state.getValue(Properties.HORIZONTAL_FACING); + } + } + return false; + } + + @Override + public boolean isPushable(GeyserSession session) { + return this.getFlag(EntityFlag.HAS_GRAVITY) && this.isAlive() && !this.isOnClimbableBlock(); + } + protected boolean hasShield(boolean offhand) { ItemMapping shieldMapping = session.getItemMappings().getStoredItems().shield(); if (offhand) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java index 1f69d34e79e..acf4ea18c20 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java @@ -210,6 +210,11 @@ public Vector3f getBedrockRotation() { return Vector3f.from(0, getYaw(), 0); } + @Override + public Vector3f position() { + return this.position.down(definition.offset()); + } + @Override protected InteractiveTag testInteraction(Hand hand) { if (definition == EntityDefinitions.CHEST_MINECART || definition == EntityDefinitions.HOPPER_MINECART) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java index 47d97b8f7ed..ccb1dd07cf2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TNTEntity.java @@ -52,6 +52,11 @@ public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYa super.moveAbsolute(position.add(Vector3f.from(0, definition.offset(), 0)), yaw, pitch, headYaw, isOnGround, teleported); } + @Override + public Vector3f position() { + return this.position.down(definition.offset()); + } + public void setFuseLength(IntEntityMetadata entityMetadata) { currentTick = entityMetadata.getPrimitiveValue(); setFlag(EntityFlag.IGNITED, true); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java index 80e5f3d18be..e36e06c663d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -59,6 +59,11 @@ public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYa super.moveAbsolute(position.add(Vector3f.from(0, definition.offset(), 0)), yaw, pitch, headYaw, isOnGround, teleported); } + @Override + public Vector3f position() { + return this.position.down(definition.offset()); + } + @Override protected void initializeMetadata() { super.initializeMetadata(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java index afbdcca31f7..835ddb0b7c5 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java @@ -109,6 +109,11 @@ public void despawnEntity() { super.despawnEntity(); } + @Override + public boolean isPushable(GeyserSession session) { + return false; + } + @Override public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { moveAbsolute(position.add(relX, relY, relZ), yaw, pitch, headYaw, onGround, false); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java index 8b0e77c738d..0b6c73cc815 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java @@ -296,4 +296,9 @@ protected boolean canUseSlot(EquipmentSlot slot) { return isAlive() && !isBaby() && getFlag(EntityFlag.TAMED); } } + + @Override + public boolean isPushable(GeyserSession session) { + return this.getPassengers().isEmpty(); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java index 2341b8c321c..cbc3c05bdf0 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java @@ -71,6 +71,11 @@ public void setAngerLevel(IntEntityMetadata entityMetadata) { dirtyMetadata.put(EntityDataTypes.HEARTBEAT_INTERVAL_TICKS, heartBeatDelay); } + @Override + public boolean isPushable(GeyserSession session) { + return super.isPushable(session) && !getFlag(EntityFlag.DIGGING) && !getFlag(EntityFlag.EMERGING); + } + @Override public void tick() { if (++tickCount % heartBeatDelay == 0 && !silent) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index 7dbbf0b5289..c2845b402b9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -52,6 +52,7 @@ import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity; import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; @@ -295,6 +296,23 @@ public void setPosition(Vector3f position) { return bedPosition; } + @Override + public boolean isPushable(GeyserSession session) { + boolean canCollide = super.isPushable(session); + if (this instanceof SessionPlayerEntity) { // This is the session player, don't check for team collision rule. + return canCollide; + } + + Team team = session.getWorldCache().getScoreboard().getTeamFor(username); + if (team == null) return true; + return switch (team.collisionRule()) { + case NEVER -> false; + case PUSH_OWN_TEAM -> canCollide && team.hasEntity(session.getPlayerEntity().getUsername()); + case PUSH_OTHER_TEAMS -> canCollide && !team.hasEntity(session.getPlayerEntity().getUsername()); + default -> canCollide; + }; + } + public void setAbsorptionHearts(FloatEntityMetadata entityMetadata) { // Extra hearts - is not metadata but an attribute on Bedrock UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index b6835151eb8..47a1f0314b6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -506,25 +506,12 @@ public float getJumpVelocity() { return velocity + 0.1F * session.getEffectCache().getJumpPower(); } + @Override public boolean isOnClimbableBlock() { if (session.getGameMode() == GameMode.SPECTATOR) { return false; } - Vector3i pos = getPosition().down(EntityDefinitions.PLAYER.offset()).toInt(); - BlockState state = session.getGeyser().getWorldManager().blockAt(session, pos); - if (session.getTagCache().is(BlockTag.CLIMBABLE, state.block())) { - return true; - } - - if (state.block() instanceof TrapDoorBlock) { - if (!state.getValue(Properties.OPEN)) { - return false; - } else { - BlockState belowState = session.getGeyser().getWorldManager().blockAt(session, pos.down()); - return belowState.is(Blocks.LADDER) && belowState.getValue(Properties.HORIZONTAL_FACING) == state.getValue(Properties.HORIZONTAL_FACING); - } - } - return false; + return super.isOnClimbableBlock(); } public boolean canStartGliding() { diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java index 10d8aa52536..ab3915af260 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java @@ -41,6 +41,7 @@ import org.geysermc.geyser.scoreboard.display.slot.PlayerlistDisplaySlot; import org.geysermc.geyser.scoreboard.display.slot.SidebarDisplaySlot; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor; @@ -173,7 +174,8 @@ public void registerNewTeam( Component prefix, Component suffix, NameTagVisibility visibility, - TeamColor color + TeamColor color, + CollisionRule collisionRule ) { Team team = teams.get(teamName); if (team != null) { @@ -183,7 +185,7 @@ public void registerNewTeam( return; } - team = new Team(this, teamName, players, name, prefix, suffix, visibility, color); + team = new Team(this, teamName, players, name, prefix, suffix, visibility, color, collisionRule); teams.put(teamName, team); // Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51) diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java index 50752353996..356a75b1330 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Team.java @@ -34,6 +34,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor; @@ -48,6 +49,7 @@ public final class Team { private final Set managedEntities; @NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS; private TeamColor color; + private CollisionRule collisionRule; private String name; private String prefix; @@ -62,7 +64,8 @@ public Team( Component prefix, Component suffix, NameTagVisibility visibility, - TeamColor color + TeamColor color, + CollisionRule collisionRule ) { this.scoreboard = scoreboard; this.id = id; @@ -70,6 +73,8 @@ public Team( this.managedEntities = new ObjectOpenHashSet<>(); this.lastUpdate = LAST_UPDATE_DEFAULT; + this.collisionRule = collisionRule; + // doesn't call entity update updateProperties(name, prefix, suffix, visibility, color); // calls entity update @@ -316,6 +321,10 @@ public TeamColor color() { return color; } + public CollisionRule collisionRule() { + return collisionRule; + } + public long lastUpdate() { return lastUpdate; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java index 9bad0b83066..733e0df934d 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java @@ -25,12 +25,16 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player.input; +import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.level.physics.CollisionResult; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; @@ -216,6 +220,54 @@ static void translate(GeyserSession session, PlayerAuthInputPacket packet) { if (entity.getRightParrot() != null) { entity.getRightParrot().moveAbsolute(entity.getPosition(), entity.getYaw(), entity.getPitch(), entity.getHeadYaw(), true, false); } + + // If the player is riding a vehicle or if the player itself cannot be pushed, then don't push. + if (entity.getVehicle() == null && entity.isPushable(session)) { + boolean affectedByPushMotion = false; + for (Entity other : session.getEntityCache().getEntities().values()) { + if (other == entity) { + continue; + } + + if (!other.isPushable(session)) { + continue; + } + + final BoundingBox entityBoundingBox = new BoundingBox(0, 0, 0, other.getBoundingBoxWidth(), other.getBoundingBoxHeight(), other.getBoundingBoxWidth()); + entityBoundingBox.translate(other.position().toDouble()); + + // If this entity can't collide with the player or isn't near the player or is the player itself, continue. + if (!entityBoundingBox.checkIntersection(session.getCollisionManager().getPlayerBoundingBox())) { + continue; + } + + // 1.21.7 entity pushing logic. + float xDistance = entity.getPosition().getX() - other.getPosition().getX(); + float zDistance = entity.getPosition().getZ() - other.getPosition().getZ(); + float largestDistance = Math.max(Math.abs(xDistance), Math.abs(zDistance)); + + Vector3f pushVelocity = Vector3f.from(xDistance, 0, zDistance); + if (largestDistance >= 0.01F) { + largestDistance = (float) GenericMath.sqrt(largestDistance); + pushVelocity = pushVelocity.div(largestDistance); + pushVelocity = pushVelocity.mul(Math.min(1 / largestDistance, 1)); + pushVelocity = pushVelocity.mul(0.05F); + + // The push motion should be relative to the current motion, we don't want player to fly by sending y vel 0. + entity.setMotion(entity.getMotion().add(pushVelocity)); + affectedByPushMotion = true; + } + } + + if (affectedByPushMotion) { + SetEntityMotionPacket motionPacket = new SetEntityMotionPacket(); + motionPacket.setRuntimeEntityId(entity.getGeyserId()); + motionPacket.setMotion(entity.getMotion()); + // This is really important, or else player going to have weird motion without this tick id. + motionPacket.setTick(packet.getTick()); + session.sendUpstreamPacket(motionPacket); + } + } } private static boolean isInvalidNumber(float val) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/scoreboard/JavaSetPlayerTeamTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/scoreboard/JavaSetPlayerTeamTranslator.java index 3a1ee63739d..c5347715ae9 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/scoreboard/JavaSetPlayerTeamTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/scoreboard/JavaSetPlayerTeamTranslator.java @@ -63,7 +63,8 @@ public void translate(GeyserSession session, ClientboundSetPlayerTeamPacket pack packet.getPrefix(), packet.getSuffix(), packet.getNameTagVisibility(), - packet.getColor() + packet.getColor(), + packet.getCollisionRule() ); } else { Team team = scoreboard.getTeam(packet.getTeamName());