mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Projectile Movement (#1929)
Fixes #1780 #1778 The laggy fireballs Llama spit Shulker bullets
This commit is contained in:
parent
c4573bb73d
commit
b7828267a5
11 changed files with 332 additions and 78 deletions
|
@ -35,6 +35,8 @@ public class AbstractArrowEntity extends Entity {
|
||||||
|
|
||||||
public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
||||||
super(entityId, geyserId, entityType, position, motion, rotation);
|
super(entityId, geyserId, entityType, position, motion, rotation);
|
||||||
|
|
||||||
|
setMotion(motion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -47,4 +49,20 @@ public class AbstractArrowEntity extends Entity {
|
||||||
|
|
||||||
super.updateBedrockMetadata(entityMetadata, session);
|
super.updateBedrockMetadata(entityMetadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRotation(Vector3f rotation) {
|
||||||
|
// Ignore the rotation sent by the Java server since the
|
||||||
|
// Java client calculates the rotation from the motion
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMotion(Vector3f motion) {
|
||||||
|
super.setMotion(motion);
|
||||||
|
|
||||||
|
double horizontalSpeed = Math.sqrt(motion.getX() * motion.getX() + motion.getZ() * motion.getZ());
|
||||||
|
float yaw = (float) Math.toDegrees(Math.atan2(motion.getX(), motion.getZ()));
|
||||||
|
float pitch = (float) Math.toDegrees(Math.atan2(motion.getY(), horizontalSpeed));
|
||||||
|
rotation = Vector3f.from(yaw, pitch, yaw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,43 +26,167 @@
|
||||||
package org.geysermc.connector.entity;
|
package org.geysermc.connector.entity;
|
||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.object.ProjectileData;
|
|
||||||
import com.nukkitx.math.vector.Vector3f;
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||||
import org.geysermc.connector.GeyserConnector;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
|
import com.nukkitx.protocol.bedrock.packet.PlaySoundPacket;
|
||||||
|
import org.geysermc.connector.entity.player.PlayerEntity;
|
||||||
import org.geysermc.connector.entity.type.EntityType;
|
import org.geysermc.connector.entity.type.EntityType;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.network.translators.collision.BoundingBox;
|
||||||
|
import org.geysermc.connector.network.translators.collision.CollisionManager;
|
||||||
|
import org.geysermc.connector.network.translators.collision.CollisionTranslator;
|
||||||
|
import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
|
||||||
|
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||||
|
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
||||||
|
|
||||||
public class FishingHookEntity extends Entity {
|
import java.util.List;
|
||||||
public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, ProjectileData data) {
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class FishingHookEntity extends ThrowableEntity {
|
||||||
|
|
||||||
|
private boolean hooked = false;
|
||||||
|
|
||||||
|
private final BoundingBox boundingBox;
|
||||||
|
|
||||||
|
private boolean inWater = false;
|
||||||
|
|
||||||
|
public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, PlayerEntity owner) {
|
||||||
super(entityId, geyserId, entityType, position, motion, rotation);
|
super(entityId, geyserId, entityType, position, motion, rotation);
|
||||||
|
|
||||||
for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) {
|
this.boundingBox = new BoundingBox(0.125, 0.125, 0.125, 0.25, 0.25, 0.25);
|
||||||
Entity entity = session.getEntityCache().getEntityByJavaId(data.getOwnerId());
|
|
||||||
if (entity == null && session.getPlayerEntity().getEntityId() == data.getOwnerId()) {
|
|
||||||
entity = session.getPlayerEntity();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity != null) {
|
// In Java, the splash sound depends on the entity's velocity, but in Bedrock the volume doesn't change.
|
||||||
this.metadata.put(EntityData.OWNER_EID, entity.getGeyserId());
|
// This splash can be confused with the sound from catching a fish. This silences the splash from Bedrock,
|
||||||
return;
|
// so that it can be handled by moveAbsoluteImmediate.
|
||||||
}
|
this.metadata.putFloat(EntityData.BOUNDING_BOX_HEIGHT, 128);
|
||||||
}
|
|
||||||
|
this.metadata.put(EntityData.OWNER_EID, owner.getGeyserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
|
public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) {
|
||||||
if (entityMetadata.getId() == 7) {
|
if (entityMetadata.getId() == 7) { // Hooked entity
|
||||||
Entity entity = session.getEntityCache().getEntityByJavaId((Integer) entityMetadata.getValue() - 1);
|
int hookedEntityId = (int) entityMetadata.getValue() - 1;
|
||||||
if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue() - 1) {
|
Entity entity = session.getEntityCache().getEntityByJavaId(hookedEntityId);
|
||||||
|
if (entity == null && session.getPlayerEntity().getEntityId() == hookedEntityId) {
|
||||||
entity = session.getPlayerEntity();
|
entity = session.getPlayerEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity != null) {
|
if (entity != null) {
|
||||||
metadata.put(EntityData.TARGET_EID, entity.getGeyserId());
|
metadata.put(EntityData.TARGET_EID, entity.getGeyserId());
|
||||||
|
hooked = true;
|
||||||
|
} else {
|
||||||
|
hooked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.updateBedrockMetadata(entityMetadata, session);
|
super.updateBedrockMetadata(entityMetadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
|
||||||
|
boundingBox.setMiddleX(position.getX());
|
||||||
|
boundingBox.setMiddleY(position.getY() + boundingBox.getSizeY() / 2);
|
||||||
|
boundingBox.setMiddleZ(position.getZ());
|
||||||
|
|
||||||
|
CollisionManager collisionManager = session.getCollisionManager();
|
||||||
|
List<Vector3i> collidableBlocks = collisionManager.getCollidableBlocks(boundingBox);
|
||||||
|
boolean touchingWater = false;
|
||||||
|
boolean collided = false;
|
||||||
|
for (Vector3i blockPos : collidableBlocks) {
|
||||||
|
if (0 <= blockPos.getY() && blockPos.getY() <= 255) {
|
||||||
|
int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
|
||||||
|
BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
||||||
|
if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) {
|
||||||
|
// TODO Push bounding box out of collision to improve movement
|
||||||
|
collided = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int waterLevel = BlockStateValues.getWaterLevel(blockID);
|
||||||
|
if (BlockTranslator.isWaterlogged(blockID)) {
|
||||||
|
waterLevel = 0;
|
||||||
|
}
|
||||||
|
if (waterLevel >= 0) {
|
||||||
|
double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0;
|
||||||
|
// Falling water is a full block
|
||||||
|
if (waterLevel >= 8) {
|
||||||
|
waterMaxY = blockPos.getY() + 1;
|
||||||
|
}
|
||||||
|
if (position.getY() <= waterMaxY) {
|
||||||
|
touchingWater = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inWater && touchingWater) {
|
||||||
|
sendSplashSound(session);
|
||||||
|
}
|
||||||
|
inWater = touchingWater;
|
||||||
|
|
||||||
|
if (!collided) {
|
||||||
|
super.moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported);
|
||||||
|
} else {
|
||||||
|
super.moveAbsoluteImmediate(session, this.position, rotation, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendSplashSound(GeyserSession session) {
|
||||||
|
if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) {
|
||||||
|
float volume = (float) (0.2f * Math.sqrt(0.2 * (motion.getX() * motion.getX() + motion.getZ() * motion.getZ()) + motion.getY() * motion.getY()));
|
||||||
|
if (volume > 1) {
|
||||||
|
volume = 1;
|
||||||
|
}
|
||||||
|
PlaySoundPacket playSoundPacket = new PlaySoundPacket();
|
||||||
|
playSoundPacket.setSound("random.splash");
|
||||||
|
playSoundPacket.setPosition(position);
|
||||||
|
playSoundPacket.setVolume(volume);
|
||||||
|
playSoundPacket.setPitch(1f + ThreadLocalRandom.current().nextFloat() * 0.3f);
|
||||||
|
session.sendUpstreamPacket(playSoundPacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(GeyserSession session) {
|
||||||
|
if (hooked || !isInAir(session) && !isInWater(session) || isOnGround()) {
|
||||||
|
motion = Vector3f.ZERO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float gravity = getGravity(session);
|
||||||
|
motion = motion.down(gravity);
|
||||||
|
|
||||||
|
moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
|
||||||
|
|
||||||
|
float drag = getDrag(session);
|
||||||
|
motion = motion.mul(drag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected float getGravity(GeyserSession session) {
|
||||||
|
if (!isInWater(session) && !onGround) {
|
||||||
|
return 0.03f;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param session the session of the Bedrock client.
|
||||||
|
* @return true if this entity is currently in air.
|
||||||
|
*/
|
||||||
|
protected boolean isInAir(GeyserSession session) {
|
||||||
|
if (session.getConnector().getConfig().isCacheChunks()) {
|
||||||
|
if (0 <= position.getFloorY() && position.getFloorY() <= 255) {
|
||||||
|
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
|
||||||
|
return block == BlockTranslator.JAVA_AIR_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected float getDrag(GeyserSession session) {
|
||||||
|
return 0.92f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,19 +32,44 @@ import org.geysermc.connector.network.session.GeyserSession;
|
||||||
public class ItemedFireballEntity extends ThrowableEntity {
|
public class ItemedFireballEntity extends ThrowableEntity {
|
||||||
private final Vector3f acceleration;
|
private final Vector3f acceleration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of ticks to advance movement before sending to Bedrock
|
||||||
|
*/
|
||||||
|
protected int futureTicks = 3;
|
||||||
|
|
||||||
public ItemedFireballEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
public ItemedFireballEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
||||||
super(entityId, geyserId, entityType, position, Vector3f.ZERO, rotation);
|
super(entityId, geyserId, entityType, position, Vector3f.ZERO, rotation);
|
||||||
acceleration = motion;
|
|
||||||
|
float magnitude = motion.length();
|
||||||
|
if (magnitude != 0) {
|
||||||
|
acceleration = motion.div(magnitude).mul(0.1f);
|
||||||
|
} else {
|
||||||
|
acceleration = Vector3f.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3f tickMovement(GeyserSession session, Vector3f position) {
|
||||||
|
position = position.add(motion);
|
||||||
|
float drag = getDrag(session);
|
||||||
|
motion = motion.add(acceleration).mul(drag);
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
|
||||||
|
// Advance the position by a few ticks before sending it to Bedrock
|
||||||
|
Vector3f lastMotion = motion;
|
||||||
|
Vector3f newPosition = position;
|
||||||
|
for (int i = 0; i < futureTicks; i++) {
|
||||||
|
newPosition = tickMovement(session, newPosition);
|
||||||
|
}
|
||||||
|
super.moveAbsoluteImmediate(session, newPosition, rotation, isOnGround, teleported);
|
||||||
|
this.position = position;
|
||||||
|
this.motion = lastMotion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void tick(GeyserSession session) {
|
public void tick(GeyserSession session) {
|
||||||
position = position.add(motion);
|
moveAbsoluteImmediate(session, tickMovement(session, position), rotation, false, false);
|
||||||
// TODO: While this reduces latency in position updating (needed for better fireball reflecting),
|
|
||||||
// TODO: movement is incredibly stiff.
|
|
||||||
// TODO: Only use this laggy movement for fireballs that be reflected
|
|
||||||
moveAbsoluteImmediate(session, position, rotation, false, true);
|
|
||||||
float drag = getDrag(session);
|
|
||||||
motion = motion.add(acceleration).mul(drag);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,20 +29,21 @@ import com.nukkitx.math.vector.Vector3f;
|
||||||
import com.nukkitx.protocol.bedrock.data.LevelEventType;
|
import com.nukkitx.protocol.bedrock.data.LevelEventType;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
|
import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
|
||||||
|
import com.nukkitx.protocol.bedrock.packet.MoveEntityDeltaPacket;
|
||||||
import org.geysermc.connector.entity.type.EntityType;
|
import org.geysermc.connector.entity.type.EntityType;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used as a class for any object-like entity that moves as a projectile
|
* Used as a class for any object-like entity that moves as a projectile
|
||||||
*/
|
*/
|
||||||
public class ThrowableEntity extends Entity implements Tickable {
|
public class ThrowableEntity extends Entity implements Tickable {
|
||||||
|
|
||||||
private Vector3f lastPosition;
|
protected Vector3f lastJavaPosition;
|
||||||
|
|
||||||
public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
||||||
super(entityId, geyserId, entityType, position, motion, rotation);
|
super(entityId, geyserId, entityType, position, motion, rotation);
|
||||||
this.lastPosition = position;
|
this.lastJavaPosition = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,22 +53,65 @@ public class ThrowableEntity extends Entity implements Tickable {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void tick(GeyserSession session) {
|
public void tick(GeyserSession session) {
|
||||||
super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround);
|
moveAbsoluteImmediate(session, position.add(motion), rotation, onGround, false);
|
||||||
float drag = getDrag(session);
|
float drag = getDrag(session);
|
||||||
float gravity = getGravity();
|
float gravity = getGravity(session);
|
||||||
motion = motion.mul(drag).down(gravity);
|
motion = motion.mul(drag).down(gravity);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
|
protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
|
||||||
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
|
MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
|
||||||
|
moveEntityDeltaPacket.setRuntimeEntityId(geyserId);
|
||||||
|
|
||||||
|
if (isOnGround) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND);
|
||||||
|
}
|
||||||
|
setOnGround(isOnGround);
|
||||||
|
|
||||||
|
if (teleported) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.TELEPORTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.position.getX() != position.getX()) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
|
||||||
|
moveEntityDeltaPacket.setX(position.getX());
|
||||||
|
}
|
||||||
|
if (this.position.getY() != position.getY()) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
|
||||||
|
moveEntityDeltaPacket.setY(position.getY());
|
||||||
|
}
|
||||||
|
if (this.position.getZ() != position.getZ()) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
|
||||||
|
moveEntityDeltaPacket.setZ(position.getZ());
|
||||||
|
}
|
||||||
|
setPosition(position);
|
||||||
|
|
||||||
|
if (this.rotation.getX() != rotation.getX()) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW);
|
||||||
|
moveEntityDeltaPacket.setYaw(rotation.getX());
|
||||||
|
}
|
||||||
|
if (this.rotation.getY() != rotation.getY()) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
|
||||||
|
moveEntityDeltaPacket.setPitch(rotation.getY());
|
||||||
|
}
|
||||||
|
if (this.rotation.getZ() != rotation.getZ()) {
|
||||||
|
moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW);
|
||||||
|
moveEntityDeltaPacket.setHeadYaw(rotation.getZ());
|
||||||
|
}
|
||||||
|
setRotation(rotation);
|
||||||
|
|
||||||
|
if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
|
||||||
|
session.sendUpstreamPacket(moveEntityDeltaPacket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the gravity of this entity type. Used for applying gravity while the entity is in motion.
|
* Get the gravity of this entity type. Used for applying gravity while the entity is in motion.
|
||||||
*
|
*
|
||||||
|
* @param session the session of the Bedrock client.
|
||||||
* @return the amount of gravity to apply to this entity while in motion.
|
* @return the amount of gravity to apply to this entity while in motion.
|
||||||
*/
|
*/
|
||||||
protected float getGravity() {
|
protected float getGravity(GeyserSession session) {
|
||||||
if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) {
|
if (metadata.getFlags().getFlag(EntityFlag.HAS_GRAVITY)) {
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
case THROWN_POTION:
|
case THROWN_POTION:
|
||||||
|
@ -76,11 +120,14 @@ public class ThrowableEntity extends Entity implements Tickable {
|
||||||
case THROWN_EXP_BOTTLE:
|
case THROWN_EXP_BOTTLE:
|
||||||
return 0.07f;
|
return 0.07f;
|
||||||
case FIREBALL:
|
case FIREBALL:
|
||||||
|
case SHULKER_BULLET:
|
||||||
return 0;
|
return 0;
|
||||||
case SNOWBALL:
|
case SNOWBALL:
|
||||||
case THROWN_EGG:
|
case THROWN_EGG:
|
||||||
case THROWN_ENDERPEARL:
|
case THROWN_ENDERPEARL:
|
||||||
return 0.03f;
|
return 0.03f;
|
||||||
|
case LLAMA_SPIT:
|
||||||
|
return 0.06f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -101,11 +148,14 @@ public class ThrowableEntity extends Entity implements Tickable {
|
||||||
case SNOWBALL:
|
case SNOWBALL:
|
||||||
case THROWN_EGG:
|
case THROWN_EGG:
|
||||||
case THROWN_ENDERPEARL:
|
case THROWN_ENDERPEARL:
|
||||||
|
case LLAMA_SPIT:
|
||||||
return 0.99f;
|
return 0.99f;
|
||||||
case FIREBALL:
|
case FIREBALL:
|
||||||
case SMALL_FIREBALL:
|
case SMALL_FIREBALL:
|
||||||
case DRAGON_FIREBALL:
|
case DRAGON_FIREBALL:
|
||||||
return 0.95f;
|
return 0.95f;
|
||||||
|
case SHULKER_BULLET:
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -117,8 +167,10 @@ public class ThrowableEntity extends Entity implements Tickable {
|
||||||
*/
|
*/
|
||||||
protected boolean isInWater(GeyserSession session) {
|
protected boolean isInWater(GeyserSession session) {
|
||||||
if (session.getConnector().getConfig().isCacheChunks()) {
|
if (session.getConnector().getConfig().isCacheChunks()) {
|
||||||
|
if (0 <= position.getFloorY() && position.getFloorY() <= 255) {
|
||||||
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
|
int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
|
||||||
return block == BlockTranslator.BEDROCK_WATER_ID;
|
return BlockStateValues.getWaterLevel(block) != -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -136,14 +188,13 @@ public class ThrowableEntity extends Entity implements Tickable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
|
public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
|
||||||
position = lastPosition;
|
moveAbsoluteImmediate(session, lastJavaPosition.add(relX, relY, relZ), rotation, isOnGround, false);
|
||||||
super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
|
lastJavaPosition = position;
|
||||||
lastPosition = position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
|
public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) {
|
||||||
super.moveAbsolute(session, position, rotation, isOnGround, teleported);
|
moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported);
|
||||||
lastPosition = position;
|
lastJavaPosition = position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ public class WitherSkullEntity extends ItemedFireballEntity {
|
||||||
|
|
||||||
public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
||||||
super(entityId, geyserId, entityType, position, motion, rotation);
|
super(entityId, geyserId, entityType, position, motion, rotation);
|
||||||
|
|
||||||
|
this.futureTicks = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -118,7 +118,7 @@ public enum EntityType {
|
||||||
TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"),
|
TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"),
|
||||||
TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f),
|
TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f),
|
||||||
CAT(CatEntity.class, 75, 0.35f, 0.3f),
|
CAT(CatEntity.class, 75, 0.35f, 0.3f),
|
||||||
SHULKER_BULLET(Entity.class, 76, 0.3125f),
|
SHULKER_BULLET(ThrowableEntity.class, 76, 0.3125f),
|
||||||
FISHING_BOBBER(FishingHookEntity.class, 77, 0f, 0f, 0f, 0f, "minecraft:fishing_hook"),
|
FISHING_BOBBER(FishingHookEntity.class, 77, 0f, 0f, 0f, 0f, "minecraft:fishing_hook"),
|
||||||
CHALKBOARD(Entity.class, 78, 0f),
|
CHALKBOARD(Entity.class, 78, 0f),
|
||||||
DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f),
|
DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f),
|
||||||
|
@ -145,7 +145,7 @@ public enum EntityType {
|
||||||
MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"),
|
MINECART_SPAWNER(SpawnerMinecartEntity.class, 98, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:minecart"),
|
||||||
MINECART_COMMAND_BLOCK(CommandBlockMinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"),
|
MINECART_COMMAND_BLOCK(CommandBlockMinecartEntity.class, 100, 0.7f, 0.98f, 0.98f, 0.35f, "minecraft:command_block_minecart"),
|
||||||
LINGERING_POTION(ThrowableEntity.class, 101, 0f),
|
LINGERING_POTION(ThrowableEntity.class, 101, 0f),
|
||||||
LLAMA_SPIT(Entity.class, 102, 0.25f),
|
LLAMA_SPIT(ThrowableEntity.class, 102, 0.25f),
|
||||||
EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"),
|
EVOKER_FANGS(Entity.class, 103, 0.8f, 0.5f, 0.5f, 0f, "minecraft:evocation_fang"),
|
||||||
EVOKER(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"),
|
EVOKER(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"),
|
||||||
VEX(VexEntity.class, 105, 0.8f, 0.4f),
|
VEX(VexEntity.class, 105, 0.8f, 0.4f),
|
||||||
|
|
|
@ -177,24 +177,24 @@ public class CollisionManager {
|
||||||
session.sendUpstreamPacket(movePlayerPacket);
|
session.sendUpstreamPacket(movePlayerPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Vector3i> getPlayerCollidableBlocks() {
|
public List<Vector3i> getCollidableBlocks(BoundingBox box) {
|
||||||
List<Vector3i> blocks = new ArrayList<>();
|
List<Vector3i> blocks = new ArrayList<>();
|
||||||
|
|
||||||
Vector3d position = Vector3d.from(playerBoundingBox.getMiddleX(),
|
Vector3d position = Vector3d.from(box.getMiddleX(),
|
||||||
playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2),
|
box.getMiddleY() - (box.getSizeY() / 2),
|
||||||
playerBoundingBox.getMiddleZ());
|
box.getMiddleZ());
|
||||||
|
|
||||||
// Loop through all blocks that could collide with the player
|
// Loop through all blocks that could collide
|
||||||
int minCollisionX = (int) Math.floor(position.getX() - ((playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE));
|
int minCollisionX = (int) Math.floor(position.getX() - ((box.getSizeX() / 2) + COLLISION_TOLERANCE));
|
||||||
int maxCollisionX = (int) Math.floor(position.getX() + (playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE);
|
int maxCollisionX = (int) Math.floor(position.getX() + (box.getSizeX() / 2) + COLLISION_TOLERANCE);
|
||||||
|
|
||||||
// Y extends 0.5 blocks down because of fence hitboxes
|
// Y extends 0.5 blocks down because of fence hitboxes
|
||||||
int minCollisionY = (int) Math.floor(position.getY() - 0.5);
|
int minCollisionY = (int) Math.floor(position.getY() - 0.5);
|
||||||
|
|
||||||
int maxCollisionY = (int) Math.floor(position.getY() + playerBoundingBox.getSizeY());
|
int maxCollisionY = (int) Math.floor(position.getY() + box.getSizeY());
|
||||||
|
|
||||||
int minCollisionZ = (int) Math.floor(position.getZ() - ((playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE));
|
int minCollisionZ = (int) Math.floor(position.getZ() - ((box.getSizeZ() / 2) + COLLISION_TOLERANCE));
|
||||||
int maxCollisionZ = (int) Math.floor(position.getZ() + (playerBoundingBox.getSizeZ() / 2) + COLLISION_TOLERANCE);
|
int maxCollisionZ = (int) Math.floor(position.getZ() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE);
|
||||||
|
|
||||||
for (int y = minCollisionY; y < maxCollisionY + 1; y++) {
|
for (int y = minCollisionY; y < maxCollisionY + 1; y++) {
|
||||||
for (int x = minCollisionX; x < maxCollisionX + 1; x++) {
|
for (int x = minCollisionX; x < maxCollisionX + 1; x++) {
|
||||||
|
@ -207,6 +207,10 @@ public class CollisionManager {
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Vector3i> getPlayerCollidableBlocks() {
|
||||||
|
return getCollidableBlocks(playerBoundingBox);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns false if the movement is invalid, and in this case it shouldn't be sent to the server and should be
|
* Returns false if the movement is invalid, and in this case it shouldn't be sent to the server and should be
|
||||||
* cancelled
|
* cancelled
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
|
||||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnEntityPacket;
|
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnEntityPacket;
|
||||||
import com.nukkitx.math.vector.Vector3f;
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
import org.geysermc.connector.entity.*;
|
import org.geysermc.connector.entity.*;
|
||||||
|
import org.geysermc.connector.entity.player.PlayerEntity;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslator;
|
import org.geysermc.connector.network.translators.PacketTranslator;
|
||||||
import org.geysermc.connector.network.translators.Translator;
|
import org.geysermc.connector.network.translators.Translator;
|
||||||
|
@ -69,8 +70,18 @@ public class JavaSpawnEntityTranslator extends PacketTranslator<ServerSpawnEntit
|
||||||
type, position, motion, rotation, (HangingDirection) packet.getData());
|
type, position, motion, rotation, (HangingDirection) packet.getData());
|
||||||
} else if (packet.getType() == EntityType.FISHING_BOBBER) {
|
} else if (packet.getType() == EntityType.FISHING_BOBBER) {
|
||||||
// Fishing bobbers need the owner for the line
|
// Fishing bobbers need the owner for the line
|
||||||
|
int ownerEntityId = ((ProjectileData) packet.getData()).getOwnerId();
|
||||||
|
Entity owner = session.getEntityCache().getEntityByJavaId(ownerEntityId);
|
||||||
|
if (owner == null && session.getPlayerEntity().getEntityId() == ownerEntityId) {
|
||||||
|
owner = session.getPlayerEntity();
|
||||||
|
}
|
||||||
|
// Java clients only spawn fishing hooks with a player as its owner
|
||||||
|
if (owner instanceof PlayerEntity) {
|
||||||
entity = new FishingHookEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
|
entity = new FishingHookEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
|
||||||
type, position, motion, rotation, (ProjectileData) packet.getData());
|
type, position, motion, rotation, (PlayerEntity) owner);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (packet.getType() == EntityType.BOAT) {
|
} else if (packet.getType() == EntityType.BOAT) {
|
||||||
// Initial rotation is incorrect
|
// Initial rotation is incorrect
|
||||||
entity = new BoatEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
|
entity = new BoatEntity(packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
|
||||||
|
|
|
@ -49,70 +49,72 @@ public class BlockStateValues {
|
||||||
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
|
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
|
||||||
private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap();
|
private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap();
|
||||||
private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap();
|
private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new Int2ByteOpenHashMap();
|
||||||
|
private static final Int2IntMap WATER_LEVEL = new Int2IntOpenHashMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the block state contains Bedrock block information
|
* Determines if the block state contains Bedrock block information
|
||||||
*
|
*
|
||||||
* @param entry The String to JsonNode map used in BlockTranslator
|
* @param javaId The Java Identifier of the block
|
||||||
* @param javaBlockState the Java Block State of the block
|
* @param javaBlockState the Java Block State of the block
|
||||||
|
* @param blockData JsonNode of info about the block from blocks.json
|
||||||
*/
|
*/
|
||||||
public static void storeBlockStateValues(Map.Entry<String, JsonNode> entry, int javaBlockState) {
|
public static void storeBlockStateValues(String javaId, int javaBlockState, JsonNode blockData) {
|
||||||
JsonNode bannerColor = entry.getValue().get("banner_color");
|
JsonNode bannerColor = blockData.get("banner_color");
|
||||||
if (bannerColor != null) {
|
if (bannerColor != null) {
|
||||||
BANNER_COLORS.put(javaBlockState, (byte) bannerColor.intValue());
|
BANNER_COLORS.put(javaBlockState, (byte) bannerColor.intValue());
|
||||||
return; // There will never be a banner color and a skull variant
|
return; // There will never be a banner color and a skull variant
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode bedColor = entry.getValue().get("bed_color");
|
JsonNode bedColor = blockData.get("bed_color");
|
||||||
if (bedColor != null) {
|
if (bedColor != null) {
|
||||||
BED_COLORS.put(javaBlockState, (byte) bedColor.intValue());
|
BED_COLORS.put(javaBlockState, (byte) bedColor.intValue());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.getKey().contains("command_block")) {
|
if (javaId.contains("command_block")) {
|
||||||
COMMAND_BLOCK_VALUES.put(javaBlockState, entry.getKey().contains("conditional=true") ? (byte) 1 : (byte) 0);
|
COMMAND_BLOCK_VALUES.put(javaBlockState, javaId.contains("conditional=true") ? (byte) 1 : (byte) 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.getValue().get("double_chest_position") != null) {
|
if (blockData.get("double_chest_position") != null) {
|
||||||
boolean isX = (entry.getValue().get("x") != null);
|
boolean isX = (blockData.get("x") != null);
|
||||||
boolean isDirectionPositive = ((entry.getValue().get("x") != null && entry.getValue().get("x").asBoolean()) ||
|
boolean isDirectionPositive = ((blockData.get("x") != null && blockData.get("x").asBoolean()) ||
|
||||||
(entry.getValue().get("z") != null && entry.getValue().get("z").asBoolean()));
|
(blockData.get("z") != null && blockData.get("z").asBoolean()));
|
||||||
boolean isLeft = (entry.getValue().get("double_chest_position").asText().contains("left"));
|
boolean isLeft = (blockData.get("double_chest_position").asText().contains("left"));
|
||||||
DOUBLE_CHEST_VALUES.put(javaBlockState, new DoubleChestValue(isX, isDirectionPositive, isLeft));
|
DOUBLE_CHEST_VALUES.put(javaBlockState, new DoubleChestValue(isX, isDirectionPositive, isLeft));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.getKey().contains("potted_") || entry.getKey().contains("flower_pot")) {
|
if (javaId.contains("potted_") || javaId.contains("flower_pot")) {
|
||||||
FLOWER_POT_VALUES.put(javaBlockState, entry.getKey().replace("potted_", ""));
|
FLOWER_POT_VALUES.put(javaBlockState, javaId.replace("potted_", ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode notePitch = entry.getValue().get("note_pitch");
|
JsonNode notePitch = blockData.get("note_pitch");
|
||||||
if (notePitch != null) {
|
if (notePitch != null) {
|
||||||
NOTEBLOCK_PITCHES.put(javaBlockState, entry.getValue().get("note_pitch").intValue());
|
NOTEBLOCK_PITCHES.put(javaBlockState, blockData.get("note_pitch").intValue());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.getKey().contains("piston")) {
|
if (javaId.contains("piston")) {
|
||||||
// True if extended, false if not
|
// True if extended, false if not
|
||||||
PISTON_VALUES.put(javaBlockState, entry.getKey().contains("extended=true"));
|
PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true"));
|
||||||
IS_STICKY_PISTON.put(javaBlockState, entry.getKey().contains("sticky"));
|
IS_STICKY_PISTON.put(javaBlockState, javaId.contains("sticky"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode skullVariation = entry.getValue().get("variation");
|
JsonNode skullVariation = blockData.get("variation");
|
||||||
if (skullVariation != null) {
|
if (skullVariation != null) {
|
||||||
SKULL_VARIANTS.put(javaBlockState, (byte) skullVariation.intValue());
|
SKULL_VARIANTS.put(javaBlockState, (byte) skullVariation.intValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode skullRotation = entry.getValue().get("skull_rotation");
|
JsonNode skullRotation = blockData.get("skull_rotation");
|
||||||
if (skullRotation != null) {
|
if (skullRotation != null) {
|
||||||
SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue());
|
SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.getKey().contains("wall_skull") || entry.getKey().contains("wall_head")) {
|
if (javaId.contains("wall_skull") || javaId.contains("wall_head")) {
|
||||||
String direction = entry.getKey().substring(entry.getKey().lastIndexOf("facing=") + 7);
|
String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7);
|
||||||
int rotation = 0;
|
int rotation = 0;
|
||||||
switch (direction.substring(0, direction.length() - 1)) {
|
switch (direction.substring(0, direction.length() - 1)) {
|
||||||
case "north":
|
case "north":
|
||||||
|
@ -131,10 +133,16 @@ public class BlockStateValues {
|
||||||
SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation);
|
SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode shulkerDirection = entry.getValue().get("shulker_direction");
|
JsonNode shulkerDirection = blockData.get("shulker_direction");
|
||||||
if (shulkerDirection != null) {
|
if (shulkerDirection != null) {
|
||||||
BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue());
|
BlockStateValues.SHULKERBOX_DIRECTIONS.put(javaBlockState, (byte) shulkerDirection.intValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (javaId.startsWith("minecraft:water")) {
|
||||||
|
String strLevel = javaId.substring(javaId.lastIndexOf("level=") + 6, javaId.length() - 1);
|
||||||
|
int level = Integer.parseInt(strLevel);
|
||||||
|
WATER_LEVEL.put(javaBlockState, level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -263,4 +271,15 @@ public class BlockStateValues {
|
||||||
public static byte getShulkerBoxDirection(int state) {
|
public static byte getShulkerBoxDirection(int state) {
|
||||||
return SHULKERBOX_DIRECTIONS.getOrDefault(state, (byte) -1);
|
return SHULKERBOX_DIRECTIONS.getOrDefault(state, (byte) -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the level of water from the block state.
|
||||||
|
* This is used in FishingHookEntity to create splash sounds when the hook hits the water.
|
||||||
|
*
|
||||||
|
* @param state BlockState of the block
|
||||||
|
* @return The water level or -1 if the block isn't water
|
||||||
|
*/
|
||||||
|
public static int getWaterLevel(int state) {
|
||||||
|
return WATER_LEVEL.getOrDefault(state, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,7 @@ public class BlockTranslator {
|
||||||
|
|
||||||
JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId);
|
JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId);
|
||||||
|
|
||||||
BlockStateValues.storeBlockStateValues(entry, javaRuntimeId);
|
BlockStateValues.storeBlockStateValues(entry.getKey(), javaRuntimeId, entry.getValue());
|
||||||
|
|
||||||
String cleanJavaIdentifier = entry.getKey().split("\\[")[0];
|
String cleanJavaIdentifier = entry.getKey().split("\\[")[0];
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2e52b01cc541c8925346f93be8940087d9af1661
|
Subproject commit bf0610450ce94507a18286e94af2965550ff9eaa
|
Loading…
Reference in a new issue