Armor stand fixes (#1270)

Armor stands now show armor if invisible. This allows both names and armor to show on an armor stand, and should allow for custom models that use armor stands to show, to an extent.
This commit is contained in:
Camotoy 2021-02-25 12:54:30 -05:00 committed by GitHub
parent 58e2c7d709
commit 9f5a356180
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 274 additions and 24 deletions

View file

@ -273,13 +273,9 @@ public class Entity {
metadata.getFlags().setFlag(EntityFlag.SWIMMING, ((xd & 0x10) == 0x10) && metadata.getFlags().getFlag(EntityFlag.SPRINTING)); // Otherwise swimming is enabled on older servers
metadata.getFlags().setFlag(EntityFlag.GLIDING, (xd & 0x80) == 0x80);
if ((xd & 0x20) == 0x20) {
// Armour stands are handled in their own class
if (!this.is(ArmorStandEntity.class)) {
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
}
} else {
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false);
// Armour stands are handled in their own class
if (!this.is(ArmorStandEntity.class)) {
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, (xd & 0x20) == 0x20);
}
// Shield code

View file

@ -29,6 +29,9 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadat
import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket;
import lombok.Getter;
import org.geysermc.connector.entity.LivingEntity;
import org.geysermc.connector.entity.type.EntityType;
@ -42,15 +45,68 @@ public class ArmorStandEntity extends LivingEntity {
private boolean isInvisible = false;
private boolean isSmall = false;
/**
* On Java Edition, armor stands always show their name. Invisibility hides the name on Bedrock.
* By having a second entity, we can allow an invisible entity with the name tag.
* (This lets armor on armor stands still show)
*/
private ArmorStandEntity secondEntity = null;
/**
* Whether this is the primary armor stand that holds the armor and not the name tag.
*/
private boolean primaryEntity = true;
/**
* Whether the entity's position must be updated to included the offset.
*
* This should be true when the Java server marks the armor stand as invisible, but we shrink the entity
* to allow the nametag to appear. Basically:
* - Is visible: this is irrelevant (false)
* - Has armor, no name: false
* - Has armor, has name: false, with a second entity
* - No armor, no name: false
* - No armor, yes name: true
*/
private boolean positionRequiresOffset = false;
/**
* Whether we should update the position of this armor stand after metadata updates.
*/
private boolean positionUpdateRequired = false;
private GeyserSession session;
public ArmorStandEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
super(entityId, geyserId, entityType, position, motion, rotation);
}
@Override
public void spawnEntity(GeyserSession session) {
this.session = session;
this.rotation = Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX());
super.spawnEntity(session);
}
@Override
public boolean despawnEntity(GeyserSession session) {
if (secondEntity != null) {
secondEntity.despawnEntity(session);
}
return super.despawnEntity(session);
}
@Override
public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
if (secondEntity != null) {
secondEntity.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
}
super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
}
@Override
public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
// Fake the height to be above where it is so the nametag appears in the right location for invisible non-marker armour stands
if (!isMarker && isInvisible && passengers.isEmpty()) {
position = position.add(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d);
if (secondEntity != null) {
secondEntity.moveAbsolute(session, applyOffsetToPosition(position), rotation, isOnGround, teleported);
} else if (positionRequiresOffset) {
// Fake the height to be above where it is so the nametag appears in the right location for invisible non-marker armour stands
position = applyOffsetToPosition(position);
}
super.moveAbsolute(session, position, Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()), isOnGround, teleported);
@ -58,47 +114,245 @@ public class ArmorStandEntity extends LivingEntity {
@Override
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
super.updateBedrockMetadata(entityMetadata, session);
if (entityMetadata.getId() == 0 && entityMetadata.getType() == MetadataType.BYTE) {
byte xd = (byte) entityMetadata.getValue();
// Check if the armour stand is invisible and store accordingly
if ((xd & 0x20) == 0x20) {
metadata.put(EntityData.SCALE, 0.0f);
isInvisible = true;
if (primaryEntity) {
isInvisible = (xd & 0x20) == 0x20;
updateSecondEntityStatus(false);
}
} else if (entityMetadata.getId() == 2 || entityMetadata.getId() == 3) {
updateSecondEntityStatus(false);
} else if (entityMetadata.getId() == 14 && entityMetadata.getType() == MetadataType.BYTE) {
byte xd = (byte) entityMetadata.getValue();
// isSmall
if ((xd & 0x01) == 0x01) {
isSmall = true;
boolean newIsSmall = (xd & 0x01) == 0x01;
if ((newIsSmall != isSmall) && positionRequiresOffset) {
// Fix new inconsistency with offset
this.position = fixOffsetForSize(position, newIsSmall);
positionUpdateRequired = true;
}
isSmall = newIsSmall;
if (isSmall) {
if (metadata.getFloat(EntityData.SCALE) != 0.55f && metadata.getFloat(EntityData.SCALE) != 0.0f) {
float scale = metadata.getFloat(EntityData.SCALE);
if (scale != 0.55f && scale != 0.0f) {
metadata.put(EntityData.SCALE, 0.55f);
}
if (metadata.get(EntityData.BOUNDING_BOX_WIDTH) != null && metadata.get(EntityData.BOUNDING_BOX_WIDTH).equals(0.5f)) {
if (metadata.getFloat(EntityData.BOUNDING_BOX_WIDTH) == 0.5f) {
metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.25f);
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.9875f);
}
} else if (metadata.get(EntityData.BOUNDING_BOX_WIDTH) != null && metadata.get(EntityData.BOUNDING_BOX_WIDTH).equals(0.25f)) {
} else if (metadata.getFloat(EntityData.BOUNDING_BOX_WIDTH) == 0.25f) {
metadata.put(EntityData.BOUNDING_BOX_WIDTH, entityType.getWidth());
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, entityType.getHeight());
}
// setMarker
if ((xd & 0x10) == 0x10 && (metadata.get(EntityData.BOUNDING_BOX_WIDTH) == null || !metadata.get(EntityData.BOUNDING_BOX_WIDTH).equals(0.0f))) {
boolean oldIsMarker = isMarker;
isMarker = (xd & 0x10) == 0x10;
if (isMarker) {
metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.0f);
metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f);
isMarker = true;
}
if (oldIsMarker != isMarker) {
updateSecondEntityStatus(false);
}
}
super.updateBedrockMetadata(entityMetadata, session);
if (secondEntity != null) {
secondEntity.updateBedrockMetadata(entityMetadata, session);
}
}
@Override
public void spawnEntity(GeyserSession session) {
this.rotation = Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX());
super.spawnEntity(session);
public void updateBedrockMetadata(GeyserSession session) {
if (secondEntity != null) {
secondEntity.updateBedrockMetadata(session);
}
super.updateBedrockMetadata(session);
if (positionUpdateRequired) {
positionUpdateRequired = false;
updatePosition();
}
}
@Override
public void setHelmet(ItemData helmet) {
super.setHelmet(helmet);
updateSecondEntityStatus(true);
}
@Override
public void setChestplate(ItemData chestplate) {
super.setChestplate(chestplate);
updateSecondEntityStatus(true);
}
@Override
public void setLeggings(ItemData leggings) {
super.setLeggings(leggings);
updateSecondEntityStatus(true);
}
@Override
public void setBoots(ItemData boots) {
super.setBoots(boots);
updateSecondEntityStatus(true);
}
@Override
public void setHand(ItemData hand) {
super.setHand(hand);
updateSecondEntityStatus(true);
}
@Override
public void setOffHand(ItemData offHand) {
super.setOffHand(offHand);
updateSecondEntityStatus(true);
}
/**
* Determine if we need to load or unload the second entity.
*
* @param sendMetadata whether to send a metadata update after a change.
*/
private void updateSecondEntityStatus(boolean sendMetadata) {
// A secondary entity always has to have the offset applied, so it remains invisible and the nametag shows.
if (!primaryEntity) return;
if (!isInvisible || isMarker) {
// It is either impossible to show armor, or the armor stand isn't invisible. We good.
updateOffsetRequirement(false);
if (positionUpdateRequired) {
positionUpdateRequired = false;
updatePosition();
}
if (secondEntity != null) {
secondEntity.despawnEntity(session);
secondEntity = null;
}
return;
}
//boolean isNametagEmpty = metadata.getString(EntityData.NAMETAG).isEmpty() || metadata.getByte(EntityData.NAMETAG_ALWAYS_SHOW, (byte) -1) == (byte) 0; - may not be necessary?
boolean isNametagEmpty = metadata.getString(EntityData.NAMETAG).isEmpty();
if (!isNametagEmpty && (!helmet.equals(ItemData.AIR) || !chestplate.equals(ItemData.AIR) || !leggings.equals(ItemData.AIR)
|| !boots.equals(ItemData.AIR) || !hand.equals(ItemData.AIR) || !offHand.equals(ItemData.AIR))) {
// If the second entity exists, no need to recreate it.
// We can't stuff this check above or else it'll fall into another else case and delete the second entity
if (secondEntity != null) return;
// Create the second entity. It doesn't need to worry about the items, but it does need to worry about
// the metadata as it will hold the name tag.
secondEntity = new ArmorStandEntity(0, session.getEntityCache().getNextEntityId().incrementAndGet(),
EntityType.ARMOR_STAND, position, motion, rotation);
secondEntity.primaryEntity = false;
if (!this.positionRequiresOffset) {
// Ensure the offset is applied for the 0 scale
secondEntity.position = secondEntity.applyOffsetToPosition(secondEntity.position);
}
// Copy metadata
secondEntity.isSmall = isSmall;
secondEntity.getMetadata().putAll(metadata);
// Copy the flags so they aren't the same object in memory
secondEntity.getMetadata().putFlags(metadata.getFlags().copy());
// Guarantee this copy is NOT invisible
secondEntity.getMetadata().getFlags().setFlag(EntityFlag.INVISIBLE, false);
// Scale to 0 to show nametag
secondEntity.getMetadata().put(EntityData.SCALE, 0.0f);
// No bounding box as we don't want to interact with this entity
secondEntity.getMetadata().put(EntityData.BOUNDING_BOX_WIDTH, 0.0f);
secondEntity.getMetadata().put(EntityData.BOUNDING_BOX_HEIGHT, 0.0f);
secondEntity.spawnEntity(session);
// Reset scale of the proper armor stand
this.metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
// Set the proper armor stand to invisible to show armor
this.metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
// Update the position of the armor stand
updateOffsetRequirement(false);
} else if (isNametagEmpty) {
// We can just make an invisible entity
// Reset scale of the proper armor stand
metadata.put(EntityData.SCALE, isSmall ? 0.55f : 1f);
// Set the proper armor stand to invisible to show armor
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true);
// Update offset
updateOffsetRequirement(false);
if (secondEntity != null) {
secondEntity.despawnEntity(session);
secondEntity = null;
}
} else {
// Nametag is not empty and there is no armor
// We don't need to make a new entity
metadata.getFlags().setFlag(EntityFlag.INVISIBLE, false);
metadata.put(EntityData.SCALE, 0.0f);
// As the above is applied, we need an offset
updateOffsetRequirement(true);
if (secondEntity != null) {
secondEntity.despawnEntity(session);
secondEntity = null;
}
}
if (sendMetadata) {
this.updateBedrockMetadata(session);
}
}
/**
* @return the selected position with the position offset applied.
*/
private Vector3f applyOffsetToPosition(Vector3f position) {
return position.add(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d);
}
/**
* @return an adjusted offset for the new small status.
*/
private Vector3f fixOffsetForSize(Vector3f position, boolean isNowSmall) {
position = removeOffsetFromPosition(position);
return position.add(0d, entityType.getHeight() * (isNowSmall ? 0.55d : 1d), 0d);
}
/**
* @return the selected position with the position offset removed.
*/
private Vector3f removeOffsetFromPosition(Vector3f position) {
return position.sub(0d, entityType.getHeight() * (isSmall ? 0.55d : 1d), 0d);
}
/**
* Set the offset to a new value; if it changed, update the position, too.
*/
private void updateOffsetRequirement(boolean newValue) {
if (newValue != positionRequiresOffset) {
this.positionRequiresOffset = newValue;
if (positionRequiresOffset) {
this.position = applyOffsetToPosition(position);
} else {
this.position = removeOffsetFromPosition(position);
}
positionUpdateRequired = true;
}
}
/**
* Updates position without calling movement code.
*/
private void updatePosition() {
MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
moveEntityPacket.setRuntimeEntityId(geyserId);
moveEntityPacket.setPosition(position);
moveEntityPacket.setRotation(Vector3f.from(rotation.getX(), rotation.getX(), rotation.getX()));
moveEntityPacket.setOnGround(onGround);
moveEntityPacket.setTeleported(false);
session.sendUpstreamPacket(moveEntityPacket);
}
}