Replicate Bedrock shield behavior more accurately

If the player swings, then they cannot be holding their shield at the same time.

Also fixes an animation edge case with other players.
This commit is contained in:
Camotoy 2022-03-15 13:34:56 -04:00
parent 7f5d81772b
commit 0829b5cd4e
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
7 changed files with 156 additions and 54 deletions

View file

@ -99,13 +99,15 @@ public class LivingEntity extends Entity {
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
byte xd = entityMetadata.getPrimitiveValue();
// Blocking gets triggered when using a bow, but if we set USING_ITEM for all items, it may look like
// you're "mining" with ex. a shield.
boolean isUsingItem = (xd & 0x01) == 0x01;
boolean isUsingOffhand = (xd & 0x02) == 0x02;
ItemMapping shield = session.getItemMappings().getStoredItems().shield();
boolean isUsingShield = (getHand().getId() == shield.getBedrockId() ||
getHand().equals(ItemData.AIR) && getOffHand().getId() == shield.getBedrockId());
setFlag(EntityFlag.USING_ITEM, (xd & 0x01) == 0x01 && !isUsingShield);
setFlag(EntityFlag.BLOCKING, (xd & 0x01) == 0x01);
boolean isUsingShield = hasShield(isUsingOffhand, shield);
setFlag(EntityFlag.USING_ITEM, isUsingItem && !isUsingShield);
// Override the blocking
setFlag(EntityFlag.BLOCKING, isUsingItem && isUsingShield);
// Riptide spin attack
setFlag(EntityFlag.DAMAGE_NEARBY_MOBS, (xd & 0x04) == 0x04);
@ -142,6 +144,14 @@ public class LivingEntity extends Entity {
}
}
protected boolean hasShield(boolean offhand, ItemMapping shieldMapping) {
if (offhand) {
return offHand.getId() == shieldMapping.getBedrockId();
} else {
return hand.getId() == shieldMapping.getBedrockId();
}
}
@Override
protected boolean isShaking() {
return isMaxFrozenState;

View file

@ -38,6 +38,7 @@ import com.nukkitx.protocol.bedrock.packet.UpdateAttributesPacket;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.AttributeUtils;
@ -167,6 +168,16 @@ public class SessionPlayerEntity extends PlayerEntity {
return super.createHealthAttribute();
}
@Override
protected boolean hasShield(boolean offhand, ItemMapping shieldMapping) {
// Must be overridden to point to the player's inventory cache
if (offhand) {
return session.getPlayerInventory().getOffhand().getJavaId() == shieldMapping.getJavaId();
} else {
return session.getPlayerInventory().getItemInHand().getJavaId() == shieldMapping.getJavaId();
}
}
@Override
public void updateBedrockMetadata() {
super.updateBedrockMetadata();

View file

@ -36,8 +36,11 @@ import com.github.steveice10.mc.protocol.MinecraftProtocol;
import com.github.steveice10.mc.protocol.data.ProtocolState;
import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.entity.player.HandPreference;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
import com.github.steveice10.mc.protocol.data.game.setting.ChatVisibility;
import com.github.steveice10.mc.protocol.data.game.setting.SkinPart;
import com.github.steveice10.mc.protocol.data.game.statistic.CustomStatistic;
@ -46,6 +49,8 @@ import com.github.steveice10.mc.protocol.packet.handshake.serverbound.ClientInte
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientInformationPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket;
import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryPacket;
import com.github.steveice10.packetlib.BuiltinFlags;
import com.github.steveice10.packetlib.Session;
@ -97,6 +102,7 @@ import org.geysermc.geyser.level.physics.CollisionManager;
import org.geysermc.geyser.network.netty.LocalSession;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.auth.AuthData;
import org.geysermc.geyser.session.auth.AuthType;
@ -107,10 +113,7 @@ import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.geyser.util.LoginEncryptionUtils;
import org.geysermc.geyser.util.MathUtils;
import org.geysermc.geyser.util.*;
import javax.annotation.Nonnull;
import java.net.ConnectException;
@ -422,6 +425,13 @@ public class GeyserSession implements GeyserConnection, CommandSender {
@Setter
private long lastVehicleMoveTimestamp = System.currentTimeMillis();
/**
* Counts how many ticks have occurred since an arm animation started.
* -1 means there is no active arm swing.
*/
@Getter(AccessLevel.NONE)
private int armAnimationTicks = -1;
/**
* Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless.
*/
@ -1107,6 +1117,34 @@ public class GeyserSession implements GeyserConnection, CommandSender {
for (Tickable entity : entityCache.getTickableEntities()) {
entity.tick();
}
if (armAnimationTicks != -1) {
// As of 1.18.2 Java Edition, it appears that the swing time is dynamically updated depending on the
// player's effect status, but the animation can cut short if the duration suddenly decreases
// (from suddenly no longer having mining fatigue, for example)
// This math is referenced from Java Edition 1.18.2
int swingTotalDuration;
int hasteLevel = Math.max(effectCache.getHaste(), effectCache.getConduitPower());
if (hasteLevel > 0) {
swingTotalDuration = 6 - hasteLevel;
} else {
int miningFatigueLevel = effectCache.getMiningFatigue();
if (miningFatigueLevel > 0) {
swingTotalDuration = 6 + miningFatigueLevel * 2;
} else {
swingTotalDuration = 6;
}
}
if (++armAnimationTicks >= swingTotalDuration) {
if (sneaking) {
// Attempt to re-activate blocking as our swing animation is up
if (attemptToBlock()) {
playerEntity.updateBedrockMetadata();
}
}
armAnimationTicks = -1;
}
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
@ -1116,7 +1154,23 @@ public class GeyserSession implements GeyserConnection, CommandSender {
this.authData = authData;
}
public void setSneaking(boolean sneaking) {
public void startSneaking() {
// Toggle the shield, if there is no ongoing arm animation
// This matches Bedrock Edition behavior as of 1.18.12
if (armAnimationTicks == -1) {
attemptToBlock();
}
setSneaking(true);
}
public void stopSneaking() {
disableBlocking();
setSneaking(false);
}
private void setSneaking(boolean sneaking) {
this.sneaking = sneaking;
// Update pose and bounding box on our end
@ -1201,6 +1255,54 @@ public class GeyserSession implements GeyserConnection, CommandSender {
return null;
}
/**
* Checks to see if a shield is in either hand to activate blocking. If so, it sets the Bedrock client to display
* blocking and sends a packet to the Java server.
*/
private boolean attemptToBlock() {
ItemMapping shield = itemMappings.getStoredItems().shield();
ServerboundUseItemPacket useItemPacket;
if (playerInventory.getItemInHand().getJavaId() == shield.getJavaId()) {
useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND);
} else if (playerInventory.getOffhand().getJavaId() == shield.getJavaId()) {
useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND);
} else {
// No blocking
return false;
}
sendDownstreamPacket(useItemPacket);
playerEntity.setFlag(EntityFlag.BLOCKING, true);
// Metadata should be updated later
return true;
}
/**
* Starts ticking the amount of time that the Bedrock client has been swinging their arm, and disables blocking if
* blocking.
*/
public void activateArmAnimationTicking() {
armAnimationTicks = 0;
if (disableBlocking()) {
playerEntity.updateBedrockMetadata();
}
}
/**
* Indicates to the client to stop blocking and tells the Java server the same.
*/
private boolean disableBlocking() {
if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
BlockUtils.POSITION_ZERO, Direction.DOWN);
sendDownstreamPacket(releaseItemPacket);
playerEntity.setFlag(EntityFlag.BLOCKING, false);
return true;
}
return false;
}
/**
* Will be overwritten for GeyserConnect.
*/

View file

@ -48,8 +48,10 @@ public class BedrockAnimateTranslator extends PacketTranslator<AnimatePacket> {
switch (packet.getAction()) {
case SWING_ARM ->
// Delay so entity damage can be processed first
session.scheduleInEventLoop(() ->
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND)),
session.scheduleInEventLoop(() -> {
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
session.activateArmAnimationTicking();
},
25,
TimeUnit.MILLISECONDS
);

View file

@ -411,22 +411,20 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
//https://wiki.vg/Protocol#Interact_Entity
switch (packet.getActionType()) {
case 0: //Interact
processEntityInteraction(session, packet, entity);
break;
case 1: //Attack
case 0 -> processEntityInteraction(session, packet, entity); // Interact
case 1 -> { // Attack
int entityId;
if (entity.getDefinition() == EntityDefinitions.ENDER_DRAGON) {
// Redirects the attack to its body entity, this only happens when
// attacking the underbelly of the ender dragon
ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(entity.getEntityId() + 3,
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
entityId = entity.getEntityId() + 3;
} else {
ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(entity.getEntityId(),
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
entityId = entity.getEntityId();
}
break;
ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(entityId,
InteractAction.ATTACK, session.isSneaking());
session.sendDownstreamPacket(attackPacket);
}
}
break;
}

View file

@ -28,7 +28,10 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
import com.github.steveice10.mc.protocol.data.game.entity.player.*;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.math.vector.Vector3i;
import com.nukkitx.protocol.bedrock.data.LevelEventType;
@ -39,10 +42,8 @@ import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
@ -105,38 +106,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
ServerboundPlayerCommandPacket startSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
session.sendDownstreamPacket(startSneakPacket);
// Toggle the shield, if relevant
PlayerInventory playerInv = session.getPlayerInventory();
ItemMapping shield = session.getItemMappings().getMapping("minecraft:shield");
if ((playerInv.getItemInHand().getJavaId() == shield.getJavaId()) ||
(playerInv.getOffhand().getJavaId() == shield.getJavaId())) {
ServerboundUseItemPacket useItemPacket;
if (playerInv.getItemInHand().getJavaId() == shield.getJavaId()) {
useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND);
} else {
// Else we just assume it's the offhand, to simplify logic and to assure the packet gets sent
useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND);
}
session.sendDownstreamPacket(useItemPacket);
session.getPlayerEntity().setFlag(EntityFlag.BLOCKING, true);
// metadata will be updated when sneaking
}
session.setSneaking(true);
session.startSneaking();
break;
case STOP_SNEAK:
ServerboundPlayerCommandPacket stopSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SNEAKING);
session.sendDownstreamPacket(stopSneakPacket);
// Stop shield, if necessary
if (session.getPlayerEntity().getFlag(EntityFlag.BLOCKING)) {
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO, Direction.DOWN);
session.sendDownstreamPacket(releaseItemPacket);
session.getPlayerEntity().setFlag(EntityFlag.BLOCKING, false);
// metadata will be updated when sneaking
}
session.setSneaking(false);
session.stopSneaking();
break;
case START_SPRINT:
if (!entity.getFlag(EntityFlag.SWIMMING)) {

View file

@ -50,6 +50,9 @@ public class JavaAnimateTranslator extends PacketTranslator<ClientboundAnimatePa
switch (packet.getAnimation()) {
case SWING_ARM:
animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
if (entity.getEntityId() == session.getPlayerEntity().getEntityId()) {
session.activateArmAnimationTicking();
}
break;
case SWING_OFFHAND:
// Use the OptionalPack to trigger the animation