diff --git a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java index 9ebe8c4823c..6085e253b00 100644 --- a/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java +++ b/bootstrap/sponge/src/main/java/org/geysermc/platform/sponge/GeyserSpongePlugin.java @@ -102,7 +102,7 @@ public void onEnable() { } } - if (geyserConfig.getBedrock().isCloneRemotePort()){ + if (geyserConfig.getBedrock().isCloneRemotePort()) { geyserConfig.getBedrock().setPort(geyserConfig.getRemote().getPort()); } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java index 66b7cc74efe..7b3ddb73605 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/GeyserStandaloneBootstrap.java @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import lombok.Getter; import net.minecrell.terminalconsole.TerminalConsoleAppender; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Logger; @@ -168,11 +169,6 @@ public void onEnable(boolean useGui, String configFilename) { this.onEnable(); } - public void onEnable(boolean useGui) { - this.useGui = useGui; - this.onEnable(); - } - @Override public void onEnable() { Logger logger = (Logger) LogManager.getRootLogger(); @@ -214,6 +210,9 @@ public void onEnable() { } GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); + // Allow libraries like Protocol to have their debug information passthrough + logger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO); + connector = GeyserConnector.start(PlatformType.STANDALONE, this); geyserCommandManager = new GeyserCommandManager(connector); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java index 9c10234f3e7..7eeba84bd7c 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/platform/standalone/LoopbackUtil.java @@ -55,7 +55,7 @@ public static void checkLoopback(GeyserStandaloneLogger geyserLogger) { if (!result.contains("minecraftuwp")) { Files.write(Paths.get(System.getenv("temp") + "/loopback_minecraft.bat"), loopbackCommand.getBytes(), new OpenOption[0]); - process = Runtime.getRuntime().exec(startScript); + Runtime.getRuntime().exec(startScript); geyserLogger.info(ChatColor.AQUA + LanguageUtils.getLocaleStringLog("geyser.bootstrap.loopback.added")); } diff --git a/connector/pom.xml b/connector/pom.xml index 69149f4edb7..b3b97697382 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -10,6 +10,10 @@ connector + + 4.1.59.Final + + org.geysermc @@ -20,26 +24,19 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.9.8 - compile - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.8 + 2.10.2 compile com.github.CloudburstMC.Protocol bedrock-v422 - d41b84e86c + 294e7e5 compile net.sf.trove4j trove - com.nukkitx.network raknet @@ -47,10 +44,16 @@ - com.nukkitx.network + com.github.CloudburstMC.Network raknet - 1.6.20 + a94d2dd compile + + + io.netty + * + + com.nukkitx.fastutil @@ -153,15 +156,51 @@ io.netty netty-resolver-dns - 4.1.43.Final + ${netty.version} compile + + io.netty + netty-resolver-dns-native-macos + ${netty.version} + compile + osx-x86_64 + io.netty netty-codec-haproxy - 4.1.56.Final + ${netty.version} + compile + + + + io.netty + netty-handler + ${netty.version} + compile + + + io.netty + netty-transport-native-epoll + ${netty.version} + compile + linux-x86_64 + + + io.netty + netty-transport-native-epoll + ${netty.version} + compile + linux-aarch_64 + + + io.netty + netty-transport-native-kqueue + ${netty.version} compile + osx-x86_64 + org.reflections reflections @@ -175,25 +214,25 @@ net.kyori adventure-api - 4.3.0 + 4.5.0 compile net.kyori adventure-text-serializer-gson - 4.3.0 + 4.5.0 compile net.kyori adventure-text-serializer-legacy - 4.3.0 + 4.5.0 compile net.kyori adventure-text-serializer-gson-legacy-impl - 4.3.0 + 4.5.0 compile diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 1eb4862a6d3..188027a516b 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.nukkitx.network.raknet.RakNetConstants; +import com.nukkitx.network.util.EventLoops; import com.nukkitx.protocol.bedrock.BedrockServer; import lombok.Getter; import lombok.Setter; @@ -59,10 +60,7 @@ import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.event.events.geyser.GeyserStopEvent; -import org.geysermc.connector.utils.DimensionUtils; -import org.geysermc.connector.utils.LanguageUtils; -import org.geysermc.connector.utils.LocaleUtils; -import org.geysermc.connector.utils.ResourcePack; +import org.geysermc.connector.utils.*; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; @@ -83,7 +81,8 @@ public class GeyserConnector { .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) + .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); public static final String NAME = "Geyser"; public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs @@ -201,6 +200,7 @@ private GeyserConnector(PlatformType platformType, GeyserBootstrap bootstrap) { remoteServer = new RemoteServer(config.getRemote().getAddress(), remotePort); authType = AuthType.getByName(config.getRemote().getAuthType()); + CooldownUtils.setShowCooldown(config.isShowCooldown()); DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls(); @@ -208,7 +208,13 @@ private GeyserConnector(PlatformType platformType, GeyserBootstrap bootstrap) { RakNetConstants.MAXIMUM_MTU_SIZE = (short) config.getMtu(); logger.debug("Setting MTU to " + config.getMtu()); - bedrockServer = new BedrockServer(new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort())); + boolean enableProxyProtocol = config.getBedrock().isEnableProxyProtocol(); + bedrockServer = new BedrockServer( + new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort()), + 1, + EventLoops.commonGroup(), + enableProxyProtocol + ); bedrockServer.setHandler(new ConnectorServerEventHandler(this)); bedrockServer.bind().whenComplete((avoid, throwable) -> { if (throwable == null) { diff --git a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java index 3f674d7faee..f91da11b590 100644 --- a/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java +++ b/connector/src/main/java/org/geysermc/connector/common/main/IGeyserMain.java @@ -52,7 +52,7 @@ public void displayMessage() { * @return The formatted message */ private String createMessage() { - String message = ""; + StringBuilder message = new StringBuilder(); InputStream helpStream = IGeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/" + Locale.getDefault().toString() + ".txt"); @@ -68,10 +68,10 @@ private String createMessage() { line = line.replace("${plugin_type}", this.getPluginType()); line = line.replace("${plugin_folder}", this.getPluginFolder()); - message += line + "\n"; + message.append(line).append("\n"); } - return message; + return message.toString(); } /** diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java index e21aa6bb814..6052bd28328 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -27,9 +27,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.geysermc.connector.GeyserLogger; +import org.geysermc.connector.network.CIDRMatcher; import org.geysermc.connector.utils.LanguageUtils; import java.nio.file.Path; +import java.util.List; import java.util.Map; public interface GeyserConfiguration { @@ -59,6 +61,8 @@ public interface GeyserConfiguration { int getPingPassthroughInterval(); + boolean isForwardPlayerPing(); + int getMaxPlayers(); boolean isDebugMode(); @@ -104,6 +108,15 @@ interface IBedrockConfiguration { String getMotd2(); String getServerName(); + + boolean isEnableProxyProtocol(); + + List getProxyProtocolWhitelistedIPs(); + + /** + * @return Unmodifiable list of {@link CIDRMatcher}s from {@link #getProxyProtocolWhitelistedIPs()} + */ + List getWhitelistedIPsMatchers(); } interface IRemoteConfiguration { diff --git a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java index 7c9532ff849..70aa3ff5ddc 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -25,16 +25,21 @@ package org.geysermc.connector.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.serializer.AsteriskSerializer; +import org.geysermc.connector.network.CIDRMatcher; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; @Getter @JsonIgnoreProperties(ignoreUnknown = true) @@ -74,6 +79,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("ping-passthrough-interval") private int pingPassthroughInterval = 3; + @JsonProperty("forward-player-ping") + private boolean forwardPlayerPing = false; + @JsonProperty("max-players") private int maxPlayers = 100; @@ -119,6 +127,7 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration private MetricsInfo metrics = new MetricsInfo(); @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class BedrockConfiguration implements IBedrockConfiguration { @AsteriskSerializer.Asterisk(sensitive = true) private String address = "0.0.0.0"; @@ -134,9 +143,33 @@ public static class BedrockConfiguration implements IBedrockConfiguration { @JsonProperty("server-name") private String serverName = GeyserConnector.NAME; + + @JsonProperty("enable-proxy-protocol") + private boolean enableProxyProtocol = false; + + @JsonProperty("proxy-protocol-whitelisted-ips") + private List proxyProtocolWhitelistedIPs = Collections.emptyList(); + + @JsonIgnore + private List whitelistedIPsMatchers = null; + + @Override + public List getWhitelistedIPsMatchers() { + // Effective Java, Third Edition; Item 83: Use lazy initialization judiciously + List matchers = this.whitelistedIPsMatchers; + if (matchers == null) { + synchronized (this) { + this.whitelistedIPsMatchers = matchers = proxyProtocolWhitelistedIPs.stream() + .map(CIDRMatcher::new) + .collect(Collectors.toList()); + } + } + return Collections.unmodifiableList(matchers); + } } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class RemoteConfiguration implements IRemoteConfiguration { @Setter @AsteriskSerializer.Asterisk(sensitive = true) @@ -170,6 +203,7 @@ public static class UserAuthenticationInfo implements IUserAuthenticationInfo { } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) public static class MetricsInfo implements IMetricsInfo { private boolean enabled = true; diff --git a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java index e9a4a1f9812..70dbdf95933 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/AbstractArrowEntity.java @@ -35,6 +35,8 @@ public class AbstractArrowEntity extends Entity { public AbstractArrowEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + setMotion(motion); } @Override @@ -47,4 +49,20 @@ public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession s 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); + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java index 9f9b7bc3884..06e56ad03cd 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java @@ -26,43 +26,167 @@ 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.object.ProjectileData; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.math.vector.Vector3i; 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.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 { - public FishingHookEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, ProjectileData data) { +import java.util.List; +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); - for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) { - Entity entity = session.getEntityCache().getEntityByJavaId(data.getOwnerId()); - if (entity == null && session.getPlayerEntity().getEntityId() == data.getOwnerId()) { - entity = session.getPlayerEntity(); - } + this.boundingBox = new BoundingBox(0.125, 0.125, 0.125, 0.25, 0.25, 0.25); - if (entity != null) { - this.metadata.put(EntityData.OWNER_EID, entity.getGeyserId()); - return; - } - } + // In Java, the splash sound depends on the entity's velocity, but in Bedrock the volume doesn't change. + // This splash can be confused with the sound from catching a fish. This silences the splash from Bedrock, + // 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 public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { - if (entityMetadata.getId() == 7) { - Entity entity = session.getEntityCache().getEntityByJavaId((Integer) entityMetadata.getValue() - 1); - if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue() - 1) { + if (entityMetadata.getId() == 7) { // Hooked entity + int hookedEntityId = (int) entityMetadata.getValue() - 1; + Entity entity = session.getEntityCache().getEntityByJavaId(hookedEntityId); + if (entity == null && session.getPlayerEntity().getEntityId() == hookedEntityId) { entity = session.getPlayerEntity(); } if (entity != null) { metadata.put(EntityData.TARGET_EID, entity.getGeyserId()); + hooked = true; + } else { + hooked = false; } } 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 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; + } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java index 2b411109add..58a3b6f6c1c 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java @@ -32,19 +32,44 @@ public class ItemedFireballEntity extends ThrowableEntity { 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) { 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; + } } - @Override - public void tick(GeyserSession session) { + private Vector3f tickMovement(GeyserSession session, Vector3f position) { position = position.add(motion); - // 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); + 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 + public void tick(GeyserSession session) { + moveAbsoluteImmediate(session, tickMovement(session, position), rotation, false, false); } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java index 4e0c25ab528..1088b2a0be6 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java @@ -29,20 +29,21 @@ import com.nukkitx.protocol.bedrock.data.LevelEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; 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.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 */ 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) { super(entityId, geyserId, entityType, position, motion, rotation); - this.lastPosition = position; + this.lastJavaPosition = position; } /** @@ -52,22 +53,65 @@ public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vect */ @Override 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 gravity = getGravity(); + float gravity = getGravity(session); motion = motion.mul(drag).down(gravity); } 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. * + * @param session the session of the Bedrock client. * @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)) { switch (entityType) { case THROWN_POTION: @@ -76,11 +120,14 @@ protected float getGravity() { case THROWN_EXP_BOTTLE: return 0.07f; case FIREBALL: + case SHULKER_BULLET: return 0; case SNOWBALL: case THROWN_EGG: case THROWN_ENDERPEARL: return 0.03f; + case LLAMA_SPIT: + return 0.06f; } } return 0; @@ -101,11 +148,14 @@ protected float getDrag(GeyserSession session) { case SNOWBALL: case THROWN_EGG: case THROWN_ENDERPEARL: + case LLAMA_SPIT: return 0.99f; case FIREBALL: case SMALL_FIREBALL: case DRAGON_FIREBALL: return 0.95f; + case SHULKER_BULLET: + return 1; } } return 1; @@ -117,8 +167,10 @@ protected float getDrag(GeyserSession session) { */ protected boolean isInWater(GeyserSession session) { if (session.getConnector().getConfig().isCacheChunks()) { - int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); - return block == BlockTranslator.BEDROCK_WATER_ID; + if (0 <= position.getFloorY() && position.getFloorY() <= 255) { + int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt()); + return BlockStateValues.getWaterLevel(block) != -1; + } } return false; } @@ -136,14 +188,13 @@ public boolean despawnEntity(GeyserSession session) { @Override public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) { - position = lastPosition; - super.moveRelative(session, relX, relY, relZ, rotation, isOnGround); - lastPosition = position; + moveAbsoluteImmediate(session, lastJavaPosition.add(relX, relY, relZ), rotation, isOnGround, false); + lastJavaPosition = position; } @Override public void moveAbsolute(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); - lastPosition = position; + moveAbsoluteImmediate(session, position, rotation, isOnGround, teleported); + lastJavaPosition = position; } } diff --git a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java index ba5b9eb55e3..3548e0dfe34 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/WitherSkullEntity.java @@ -35,6 +35,8 @@ public class WitherSkullEntity extends ItemedFireballEntity { public WitherSkullEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); + + this.futureTicks = 1; } @Override diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java index 544014115ce..752a0d1062f 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/animal/RabbitEntity.java @@ -44,7 +44,7 @@ public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession s if (entityMetadata.getId() == 15) { metadata.put(EntityData.SCALE, .55f); boolean isBaby = (boolean) entityMetadata.getValue(); - if(isBaby) { + if (isBaby) { metadata.put(EntityData.SCALE, .35f); metadata.getFlags().setFlag(EntityFlag.BABY, true); } diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java index d481cd0c5ab..56354774df9 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java @@ -125,7 +125,7 @@ public void moveRelative(GeyserSession session, double relX, double relY, double Pattern r = Pattern.compile("facing=([a-z]+)"); Matcher m = r.matcher(bedRotationZ); if (m.find()) { - switch (m.group(0)){ + switch (m.group(0)) { case "facing=south": //bed is facing south z = 180; diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java index 8dcce6a7f23..e024b4e55a8 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/WitherEntity.java @@ -46,7 +46,7 @@ public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession s if (entityMetadata.getId() >= 15 && entityMetadata.getId() <= 17) { Entity entity = session.getEntityCache().getEntityByJavaId((int) entityMetadata.getValue()); - if (entity == null && session.getPlayerEntity().getEntityId() == (Integer) entityMetadata.getValue()) { + if (entity == null && session.getPlayerEntity().getEntityId() == (int) entityMetadata.getValue()) { entity = session.getPlayerEntity(); } @@ -62,7 +62,7 @@ public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession s } else if (entityMetadata.getId() == 17) { metadata.put(EntityData.WITHER_TARGET_3, targetID); } else if (entityMetadata.getId() == 18) { - metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, (int) entityMetadata.getValue()); + metadata.put(EntityData.WITHER_INVULNERABLE_TICKS, entityMetadata.getValue()); // Show the shield for the first few seconds of spawning (like Java) if ((int) entityMetadata.getValue() >= 165) { diff --git a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java index e1e531f42f1..f38e56fd88d 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java +++ b/connector/src/main/java/org/geysermc/connector/entity/type/EntityType.java @@ -30,8 +30,11 @@ import org.geysermc.connector.entity.living.*; import org.geysermc.connector.entity.living.animal.*; import org.geysermc.connector.entity.living.animal.horse.*; -import org.geysermc.connector.entity.living.animal.tameable.*; -import org.geysermc.connector.entity.living.merchant.*; +import org.geysermc.connector.entity.living.animal.tameable.CatEntity; +import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity; +import org.geysermc.connector.entity.living.animal.tameable.WolfEntity; +import org.geysermc.connector.entity.living.merchant.AbstractMerchantEntity; +import org.geysermc.connector.entity.living.merchant.VillagerEntity; import org.geysermc.connector.entity.living.monster.*; import org.geysermc.connector.entity.living.monster.raid.AbstractIllagerEntity; import org.geysermc.connector.entity.living.monster.raid.PillagerEntity; @@ -39,6 +42,9 @@ import org.geysermc.connector.entity.living.monster.raid.SpellcasterIllagerEntity; import org.geysermc.connector.entity.player.PlayerEntity; +import java.util.ArrayList; +import java.util.List; + @Getter public enum EntityType { @@ -112,7 +118,7 @@ public enum EntityType { TRIDENT(TridentEntity.class, 73, 0f, 0f, 0f, 0f, "minecraft:thrown_trident"), TURTLE(TurtleEntity.class, 74, 0.4f, 1.2f), 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"), CHALKBOARD(Entity.class, 78, 0f), DRAGON_FIREBALL(ItemedFireballEntity.class, 79, 1.0f), @@ -139,7 +145,7 @@ public enum EntityType { 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"), 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(SpellcasterIllagerEntity.class, 104, 1.95f, 0.6f, 0.6f, 0f, "minecraft:evocation_illager"), VEX(VexEntity.class, 105, 0.8f, 0.4f), @@ -174,17 +180,33 @@ public enum EntityType { */ ENDER_DRAGON_PART(EnderDragonPartEntity.class, 32, 0, 0, 0, 0, "minecraft:armor_stand"); + /** + * A list of all Java identifiers for use with command suggestions + */ + public static final String[] ALL_JAVA_IDENTIFIERS; private static final EntityType[] VALUES = values(); - private Class entityClass; + static { + List allJavaIdentifiers = new ArrayList<>(); + for (EntityType type : values()) { + if (type == AGENT || type == BALLOON || type == CHALKBOARD || type == NPC || type == TRIPOD_CAMERA || type == ENDER_DRAGON_PART) { + continue; + } + allJavaIdentifiers.add("minecraft:" + type.name().toLowerCase()); + } + ALL_JAVA_IDENTIFIERS = allJavaIdentifiers.toArray(new String[0]); + } + + private final Class entityClass; private final int type; private final float height; private final float width; private final float length; private final float offset; - private String identifier; + private final String identifier; EntityType(Class entityClass, int type, float height) { + //noinspection SuspiciousNameCombination this(entityClass, type, height, height); } @@ -198,8 +220,6 @@ public enum EntityType { EntityType(Class entityClass, int type, float height, float width, float length, float offset) { this(entityClass, type, height, width, length, offset, null); - - this.identifier = "minecraft:" + name().toLowerCase(); } EntityType(Class entityClass, int type, float height, float width, float length, float offset, String identifier) { @@ -209,7 +229,7 @@ public enum EntityType { this.width = width; this.length = length; this.offset = offset + 0.00001f; - this.identifier = identifier; + this.identifier = identifier == null ? "minecraft:" + name().toLowerCase() : identifier; } public static EntityType getFromIdentifier(String identifier) { diff --git a/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java b/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java new file mode 100644 index 00000000000..57e58ecc297 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/CIDRMatcher.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * 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.connector.network; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/* + * Taken & modified from TCPShield, licensed under MIT. See https://github.com/TCPShield/RealIP/blob/master/LICENSE + * + * https://github.com/TCPShield/RealIP/blob/32d422a9523cb6e25b571072851f3306bb8bbc4f/src/main/java/net/tcpshield/tcpshield/validation/cidr/CIDRMatcher.java + */ +public class CIDRMatcher { + private final int maskBits; + private final int maskBytes; + private final boolean simpleCIDR; + private final InetAddress cidrAddress; + + public CIDRMatcher(String ipAddress) { + String[] split = ipAddress.split("/", 2); + + String parsedIPAddress; + if (split.length == 2) { + parsedIPAddress = split[0]; + + this.maskBits = Integer.parseInt(split[1]); + this.simpleCIDR = maskBits == 32; + } else { + parsedIPAddress = ipAddress; + + this.maskBits = -1; + this.simpleCIDR = true; + } + + this.maskBytes = simpleCIDR ? -1 : maskBits / 8; + + try { + cidrAddress = InetAddress.getByName(parsedIPAddress); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + public boolean matches(InetAddress inetAddress) { + // check if IP is IPv4 or IPv6 + if (cidrAddress.getClass() != inetAddress.getClass()) { + return false; + } + + // check for equality if it's a simple CIDR + if (simpleCIDR) { + return inetAddress.equals(cidrAddress); + } + + byte[] inetAddressBytes = inetAddress.getAddress(); + byte[] requiredAddressBytes = cidrAddress.getAddress(); + + byte finalByte = (byte) (0xFF00 >> (maskBits & 0x07)); + + for (int i = 0; i < maskBytes; i++) { + if (inetAddressBytes[i] != requiredAddressBytes[i]) { + return false; + } + } + + if (finalByte != 0) { + return (inetAddressBytes[maskBytes] & finalByte) == (requiredAddressBytes[maskBytes] & finalByte); + } + + return true; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java index 39783deb6f5..af6e8e4149a 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -42,8 +42,18 @@ import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.List; public class ConnectorServerEventHandler implements BedrockServerEventHandler { + /* + The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client + */ + private static final int MINECRAFT_VERSION_BYTES_LENGTH = BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length; + private static final int BRAND_BYTES_LENGTH = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8).length; + /** + * The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length. + */ + private static final int MAGIC_RAKNET_LENGTH = 338; private final GeyserConnector connector; @@ -53,6 +63,21 @@ public ConnectorServerEventHandler(GeyserConnector connector) { @Override public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) { + List allowedProxyIPs = connector.getConfig().getBedrock().getProxyProtocolWhitelistedIPs(); + if (connector.getConfig().getBedrock().isEnableProxyProtocol() && !allowedProxyIPs.isEmpty()) { + boolean isWhitelistedIP = false; + for (CIDRMatcher matcher : connector.getConfig().getBedrock().getWhitelistedIPsMatchers()) { + if (matcher.matches(inetSocketAddress.getAddress())) { + isWhitelistedIP = true; + break; + } + } + + if (!isWhitelistedIP) { + return false; + } + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.attempt_connect", inetSocketAddress)); return true; } @@ -71,16 +96,16 @@ public BedrockPong onQuery(InetSocketAddress inetSocketAddress) { BedrockPong pong = new BedrockPong(); pong.setEdition("MCPE"); - pong.setGameType("Default"); + pong.setGameType("Survival"); // Can only be Survival or Creative as of 1.16.210.59 pong.setNintendoLimited(false); pong.setProtocolVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()); - pong.setVersion(null); // Server tries to connect either way and it looks better + pong.setVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); // Required to not be empty as of 1.16.210.59. Can only contain . and numbers. pong.setIpv4Port(config.getBedrock().getPort()); if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) { String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n"); String mainMotd = motd[0]; // First line of the motd. - String subMotd = (motd.length != 1) ? motd[1] : ""; // Second line of the motd if present, otherwise blank. + String subMotd = (motd.length != 1) ? motd[1] : GeyserConnector.NAME; // Second line of the motd if present, otherwise default. pong.setMotd(mainMotd.trim()); pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit. @@ -97,15 +122,28 @@ public BedrockPong onQuery(InetSocketAddress inetSocketAddress) { pong.setMaximumPlayerCount(config.getMaxPlayers()); } + // Fallbacks to prevent errors and allow Bedrock to see the server + if (pong.getMotd() == null || pong.getMotd().trim().isEmpty()) { + pong.setMotd(GeyserConnector.NAME); + } + if (pong.getSubMotd() == null || pong.getSubMotd().trim().isEmpty()) { + // Sub-MOTD cannot be empty as of 1.16.210.59 + pong.setSubMotd(GeyserConnector.NAME); + } + // The ping will not appear if the MOTD + sub-MOTD is of a certain length. // We don't know why, though byte[] motdArray = pong.getMotd().getBytes(StandardCharsets.UTF_8); - if (motdArray.length + pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length > 338) { - // Remove the sub-MOTD first since that only appears locally - pong.setSubMotd(""); - if (motdArray.length > 338) { + int subMotdLength = pong.getSubMotd().getBytes(StandardCharsets.UTF_8).length; + if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) { + // Shorten the sub-MOTD first since that only appears locally + if (subMotdLength > BRAND_BYTES_LENGTH) { + pong.setSubMotd(GeyserConnector.NAME); + subMotdLength = BRAND_BYTES_LENGTH; + } + if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) { // If the top MOTD is still too long, we chop it down - byte[] newMotdArray = new byte[339]; + byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength]; System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8)); } diff --git a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java index 637f6d99d08..87541f70409 100644 --- a/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/QueryPacketHandler.java @@ -64,7 +64,7 @@ public class QueryPacketHandler { * @param buffer The Query data */ public QueryPacketHandler(GeyserConnector connector, InetSocketAddress sender, ByteBuf buffer) { - if(!isQueryPacket(buffer)) + if (!isQueryPacket(buffer)) return; this.connector = connector; @@ -225,7 +225,7 @@ private byte[] getPlayers() { query.write(new byte[] { 0x00, 0x00 }); // Fill player names - if(pingInfo != null) { + if (pingInfo != null) { for (String username : pingInfo.getPlayerList()) { query.write(username.getBytes()); query.write((byte) 0x00); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 9c2c8de6e7d..d86be8b8887 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -102,8 +102,7 @@ import org.geysermc.floodgate.util.EncryptionUtil; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -182,6 +181,9 @@ public class GeyserSession implements CommandSender { @Setter private boolean sprinting; + /** + * Not updated if cache chunks is enabled. + */ @Setter private boolean jumping; @@ -391,8 +393,8 @@ public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServ bedrockServerSession.addDisconnectHandler(disconnectReason -> { EventManager.getInstance().triggerEvent(new SessionDisconnectEvent(this, disconnectReason)); - - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", bedrockServerSession.getAddress().getAddress(), disconnectReason)); + InetAddress address = bedrockServerSession.getRealAddress().getAddress(); + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", address, disconnectReason)); disconnect(disconnectReason.name()); connector.removePlayer(this); @@ -588,8 +590,10 @@ private void connectDownstream() { downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); } - // Let Geyser handle sending the keep alive - downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + if (connector.getConfig().isForwardPlayerPing()) { + // Let Geyser handle sending the keep alive + downstream.getSession().setFlag(MinecraftConstants.AUTOMATIC_KEEP_ALIVE_MANAGEMENT, false); + } downstream.getSession().addListener(new SessionAdapter() { @Override public void packetSending(PacketSendingEvent event) { @@ -604,7 +608,7 @@ public void packetSending(PacketSendingEvent event) { clientData.getDeviceOS().ordinal(), clientData.getLanguageCode(), clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() + upstream.getAddress().getAddress().getHostAddress() )); } catch (Exception e) { connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); @@ -869,7 +873,14 @@ private void startGame() { startGamePacket.setMultiplayerCorrelationId(""); startGamePacket.setItemEntries(ItemRegistry.ITEMS); startGamePacket.setVanillaVersion("*"); - startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); + startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); // can be removed once 1.16.200 support is dropped + + SyncedPlayerMovementSettings settings = new SyncedPlayerMovementSettings(); + settings.setMovementMode(AuthoritativeMovementMode.CLIENT); + settings.setRewindHistorySize(0); + settings.setServerAuthoritativeBlockBreaking(false); + startGamePacket.setPlayerMovementSettings(settings); + sendUpstreamPacket(startGamePacket); } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java index 04e208af3fc..f973574b0ce 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/UpstreamSession.java @@ -61,6 +61,6 @@ public boolean isClosed() { } public InetSocketAddress getAddress() { - return session.getAddress(); + return session.getRealAddress(); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index 10075a9a479..16e06c06644 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -103,6 +103,8 @@ public class BedrockClientData { private String skinColor; @JsonProperty(value = "ThirdPartyNameOnly") private boolean thirdPartyNameOnly; + @JsonProperty(value = "PlayFabId") + private String playFabId; public enum UIProfile { @JsonEnumDefaultValue diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java index f4c0f9abc13..fb6d5b93dec 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/EntityIdentifierRegistry.java @@ -38,7 +38,7 @@ */ public class EntityIdentifierRegistry { - public static NbtMap ENTITY_IDENTIFIERS; + public static final NbtMap ENTITY_IDENTIFIERS; private EntityIdentifierRegistry() { } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java index f48331b76ef..6f76326eeef 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/PacketTranslatorRegistry.java @@ -30,6 +30,7 @@ import com.github.steveice10.packetlib.packet.Packet; import com.nukkitx.protocol.bedrock.BedrockPacket; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.geysermc.common.PlatformType; import lombok.Getter; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.event.EventManager; @@ -96,12 +97,15 @@ public static void init() { public

boolean translate(Class clazz, P packet, GeyserSession session) { if (!session.getUpstream().isClosed() && !session.isClosed()) { try { - if (translators.containsKey(clazz)) { - ((PacketTranslator

) translators.get(clazz)).translate(packet, session); + PacketTranslator

translator = (PacketTranslator

) translators.get(clazz); + if (translator != null) { + translator.translate(packet, session); return true; } else { - if (!IGNORED_PACKETS.contains(clazz)) + if ((GeyserConnector.getInstance().getPlatformType() != PlatformType.STANDALONE || !(packet instanceof BedrockPacket)) && !IGNORED_PACKETS.contains(clazz)) { + // Other debug logs already take care of Bedrock packets for us if on standalone GeyserConnector.getInstance().getLogger().debug("Could not find packet for " + (packet.toString().length() > 25 ? packet.getClass().getSimpleName() : packet)); + } } } catch (Throwable ex) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.network.translator.packet.failed", packet.getClass().getSimpleName()), ex); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java index 38e5981e608..56387fd5816 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java @@ -33,23 +33,25 @@ import org.geysermc.floodgate.util.DeviceOS; /** - * Used to send the keep alive packet back to the server + * Used to send the forwarded keep alive packet back to the server */ @Translator(packet = NetworkStackLatencyPacket.class) public class BedrockNetworkStackLatencyTranslator extends PacketTranslator { @Override public void translate(NetworkStackLatencyPacket packet, GeyserSession session) { - long pingId; - // so apparently, as of 1.16.200 - // PS4 divides the network stack latency timestamp FOR US!!! - // WTF - if (session.getClientData().getDeviceOS().equals(DeviceOS.NX)) { - // Ignore the weird DeviceOS, our order is wrong and will be fixed in Floodgate 2.0 - pingId = packet.getTimestamp(); - } else { - pingId = packet.getTimestamp() / 1000; + if (session.getConnector().getConfig().isForwardPlayerPing()) { + long pingId; + // so apparently, as of 1.16.200 + // PS4 divides the network stack latency timestamp FOR US!!! + // WTF + if (session.getClientData().getDeviceOS().equals(DeviceOS.NX)) { + // Ignore the weird DeviceOS, our order is wrong and will be fixed in Floodgate 2.0 + pingId = packet.getTimestamp(); + } else { + pingId = packet.getTimestamp() / 1000; + } + session.sendDownstreamPacket(new ClientKeepAlivePacket(pingId)); } - session.sendDownstreamPacket(new ClientKeepAlivePacket(pingId)); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java index c4dbbec405e..c248b57a543 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java @@ -37,6 +37,7 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.PlayerActionType; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; @@ -64,7 +65,7 @@ public void translate(PlayerActionPacket packet, GeyserSession session) { return; // Send book update before any player action - if (packet.getAction() != PlayerActionPacket.Action.RESPAWN) { + if (packet.getAction() != PlayerActionType.RESPAWN) { session.getBookEditCache().checkForSend(); } @@ -205,10 +206,11 @@ public void translate(PlayerActionPacket packet, GeyserSession session) { session.getEntityCache().updateBossBars(); break; case JUMP: - session.setJumping(true); - session.getConnector().getGeneralThreadPool().schedule(() -> { - session.setJumping(false); - }, 1, TimeUnit.SECONDS); + if (!session.getConnector().getConfig().isCacheChunks()) { + // Save the jumping status for determining teleport status + session.setJumping(true); + session.getConnector().getGeneralThreadPool().schedule(() -> session.setJumping(false), 1, TimeUnit.SECONDS); + } break; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java index 203e4406f3a..77392a99ae1 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/collision/CollisionManager.java @@ -177,24 +177,24 @@ public void recalculatePosition() { session.sendUpstreamPacket(movePlayerPacket); } - public List getPlayerCollidableBlocks() { + public List getCollidableBlocks(BoundingBox box) { List blocks = new ArrayList<>(); - Vector3d position = Vector3d.from(playerBoundingBox.getMiddleX(), - playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), - playerBoundingBox.getMiddleZ()); + Vector3d position = Vector3d.from(box.getMiddleX(), + box.getMiddleY() - (box.getSizeY() / 2), + box.getMiddleZ()); - // Loop through all blocks that could collide with the player - int minCollisionX = (int) Math.floor(position.getX() - ((playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE)); - int maxCollisionX = (int) Math.floor(position.getX() + (playerBoundingBox.getSizeX() / 2) + COLLISION_TOLERANCE); + // Loop through all blocks that could collide + int minCollisionX = (int) Math.floor(position.getX() - ((box.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 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 maxCollisionZ = (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() + (box.getSizeZ() / 2) + COLLISION_TOLERANCE); for (int y = minCollisionY; y < maxCollisionY + 1; y++) { for (int x = minCollisionX; x < maxCollisionX + 1; x++) { @@ -207,6 +207,10 @@ public List getPlayerCollidableBlocks() { return blocks; } + public List 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 * cancelled diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java index 769cbd63aea..a3b4b6c3193 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java @@ -69,6 +69,18 @@ public enum Enchantment { QUICK_CHARGE, SOUL_SPEED; + /** + * A list of all enchantment Java identifiers for use with command suggestions. + */ + public static final String[] ALL_JAVA_IDENTIFIERS; + + static { + ALL_JAVA_IDENTIFIERS = new String[values().length]; + for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) { + ALL_JAVA_IDENTIFIERS[i] = values()[i].javaIdentifier; + } + } + private final String javaIdentifier; Enchantment() { diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java index e9b821588db..9d1921731e0 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemRegistry.java @@ -63,6 +63,11 @@ public class ItemRegistry { public static final List ITEMS = new ArrayList<>(); public static final Int2ObjectMap ITEM_ENTRIES = new Int2ObjectOpenHashMap<>(); + /** + * A list of all Java item names. + */ + public static final String[] ITEM_NAMES; + /** * Bamboo item entry, used in PandaEntity.java */ @@ -116,6 +121,8 @@ public static void init() { // Used to get the Bedrock namespaced ID (in instances where there are small differences) Int2ObjectMap bedrockIdToIdentifier = new Int2ObjectOpenHashMap<>(); + List itemNames = new ArrayList<>(); + List itemEntries; try { itemEntries = GeyserConnector.JSON_MAPPER.readValue(stream, itemEntriesType); @@ -207,6 +214,8 @@ public static void init() { BUCKETS.add(entry.getValue().get("bedrock_id").intValue()); } + itemNames.add(entry.getKey()); + itemIndex++; } @@ -235,6 +244,8 @@ public static void init() { creativeItems.add(ItemData.fromNet(netId++, item.getId(), item.getDamage(), item.getCount(), item.getTag())); } CREATIVE_ITEMS = creativeItems.toArray(new ItemData[0]); + + ITEM_NAMES = itemNames.toArray(new String[0]); } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java index f25ccab82a1..64d8d7730cc 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java @@ -309,9 +309,8 @@ public CompoundTag translateToJavaNBT(String name, NbtMap tag) { CompoundTag javaTag = new CompoundTag(name); Map javaValue = javaTag.getValue(); if (tag != null && !tag.isEmpty()) { - for (String str : tag.keySet()) { - Object bedrockTag = tag.get(str); - Tag translatedTag = translateToJavaNBT(str, bedrockTag); + for (Map.Entry entry : tag.entrySet()) { + Tag translatedTag = translateToJavaNBT(entry.getKey(), entry.getValue()); if (translatedTag == null) continue; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java index f6664c1a697..7de1018111f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareCommandsTranslator.java @@ -33,21 +33,69 @@ import com.nukkitx.protocol.bedrock.data.command.CommandParamData; import com.nukkitx.protocol.bedrock.data.command.CommandParamType; import com.nukkitx.protocol.bedrock.packet.AvailableCommandsPacket; +import it.unimi.dsi.fastutil.Hash; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap; import lombok.Getter; +import lombok.ToString; +import net.kyori.adventure.text.format.NamedTextColor; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.item.Enchantment; +import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; @Translator(packet = ServerDeclareCommandsPacket.class) public class JavaDeclareCommandsTranslator extends PacketTranslator { + + private static final String[] ENUM_BOOLEAN = {"true", "false"}; + private static final String[] VALID_COLORS; + private static final String[] VALID_SCOREBOARD_SLOTS; + + private static final Hash.Strategy PARAM_STRATEGY = new Hash.Strategy() { + @Override + public int hashCode(CommandParamData[][] o) { + return Arrays.deepHashCode(o); + } + + @Override + public boolean equals(CommandParamData[][] a, CommandParamData[][] b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + CommandParamData[] a1 = a[i]; + CommandParamData[] b1 = b[i]; + if (a1.length != b1.length) return false; + + for (int j = 0; j < a1.length; j++) { + if (!a1[j].equals(b1[j])) return false; + } + } + return true; + } + }; + + static { + List validColors = new ArrayList<>(NamedTextColor.NAMES.keys()); + validColors.add("reset"); + VALID_COLORS = validColors.toArray(new String[0]); + + List teamOptions = new ArrayList<>(Arrays.asList("list", "sidebar", "belowName")); + for (String color : NamedTextColor.NAMES.keys()) { + teamOptions.add("sidebar.team." + color); + } + VALID_SCOREBOARD_SLOTS = teamOptions.toArray(new String[0]); + } + @Override public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) { // Don't send command suggestions if they are disabled @@ -60,48 +108,50 @@ public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) return; } + CommandNode[] nodes = packet.getNodes(); List commandData = new ArrayList<>(); - Int2ObjectMap commands = new Int2ObjectOpenHashMap<>(); + IntSet commandNodes = new IntOpenHashSet(); + Set knownAliases = new HashSet<>(); + Map> commands = new Object2ObjectOpenCustomHashMap<>(PARAM_STRATEGY); Int2ObjectMap> commandArgs = new Int2ObjectOpenHashMap<>(); // Get the first node, it should be a root node - CommandNode rootNode = packet.getNodes()[packet.getFirstNodeIndex()]; + CommandNode rootNode = nodes[packet.getFirstNodeIndex()]; // Loop through the root nodes to get all commands for (int nodeIndex : rootNode.getChildIndices()) { - CommandNode node = packet.getNodes()[nodeIndex]; + CommandNode node = nodes[nodeIndex]; // Make sure we don't have duplicated commands (happens if there is more than 1 root node) - if (commands.containsKey(nodeIndex)) { continue; } - if (commands.containsValue(node.getName())) { continue; } + if (!commandNodes.add(nodeIndex) || !knownAliases.add(node.getName().toLowerCase())) continue; // Get and update the commandArgs list with the found arguments if (node.getChildIndices().length >= 1) { for (int childIndex : node.getChildIndices()) { - commandArgs.putIfAbsent(nodeIndex, new ArrayList<>()); - commandArgs.get(nodeIndex).add(packet.getNodes()[childIndex]); + commandArgs.computeIfAbsent(nodeIndex, ArrayList::new).add(nodes[childIndex]); } } - // Insert the command name into the list - commands.put(nodeIndex, node.getName()); + // Get and parse all params + CommandParamData[][] params = getParams(nodes[nodeIndex], nodes); + + // Insert the alias name into the command list + commands.computeIfAbsent(params, index -> new HashSet<>()).add(node.getName().toLowerCase()); } // The command flags, not sure what these do apart from break things List flags = Collections.emptyList(); // Loop through all the found commands - for (int commandID : commands.keySet()) { - String commandName = commands.get(commandID); - // Create a basic alias - CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", new String[] { commandName.toLowerCase() }, false); + for (Map.Entry> entry : commands.entrySet()) { + String commandName = entry.getValue().iterator().next(); // We know this has a value - // Get and parse all params - CommandParamData[][] params = getParams(packet.getNodes()[commandID], packet.getNodes()); + // Create a basic alias + CommandEnumData aliases = new CommandEnumData(commandName + "Aliases", entry.getValue().toArray(new String[0]), false); // Build the completed command and add it to the final list - CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, params); + CommandData data = new CommandData(commandName, session.getConnector().getCommandManager().getDescription(commandName), flags, (byte) 0, aliases, entry.getKey()); commandData.add(data); } @@ -109,7 +159,7 @@ public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket(); availableCommandsPacket.getCommands().addAll(commandData); - GeyserConnector.getInstance().getLogger().debug("Sending command packet of " + commandData.size() + " commands"); + session.getConnector().getLogger().debug("Sending command packet of " + commandData.size() + " commands"); // Finally, send the commands to the client session.sendUpstreamPacket(availableCommandsPacket); @@ -119,11 +169,10 @@ public void translate(ServerDeclareCommandsPacket packet, GeyserSession session) * Build the command parameter array for the given command * * @param commandNode The command to build the parameters for - * @param allNodes Every command node - * + * @param allNodes Every command node * @return An array of parameter option arrays */ - private CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] allNodes) { + private static CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] allNodes) { // Check if the command is an alias and redirect it if (commandNode.getRedirectIndex() != -1) { GeyserConnector.getInstance().getLogger().debug("Redirecting command " + commandNode.getName() + " to " + allNodes[commandNode.getRedirectIndex()].getName()); @@ -136,16 +185,8 @@ private CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] al rootParam.buildChildren(allNodes); List treeData = rootParam.getTree(); - CommandParamData[][] params = new CommandParamData[treeData.size()][]; - - // Fill the nested params array - int i = 0; - for (CommandParamData[] tree : treeData) { - params[i] = tree; - i++; - } - return params; + return treeData.toArray(new CommandParamData[0][]); } return new CommandParamData[0][0]; @@ -155,14 +196,17 @@ private CommandParamData[][] getParams(CommandNode commandNode, CommandNode[] al * Convert Java edition command types to Bedrock edition * * @param parser Command type to convert - * * @return Bedrock parameter data type */ - private CommandParamType mapCommandType(CommandParser parser) { - if (parser == null) { return CommandParamType.STRING; } + private static Object mapCommandType(CommandParser parser) { + if (parser == null) { + return CommandParamType.STRING; + } switch (parser) { case FLOAT: + case ROTATION: + case DOUBLE: return CommandParamType.FLOAT; case INTEGER: @@ -189,50 +233,44 @@ private CommandParamType mapCommandType(CommandParser parser) { return CommandParamType.JSON; case RESOURCE_LOCATION: + case FUNCTION: return CommandParamType.FILE_PATH; - case INT_RANGE: - return CommandParamType.INT_RANGE; - case BOOL: - case DOUBLE: - case STRING: - case VEC2: + return ENUM_BOOLEAN; + + case OPERATION: // ">=", "==", etc + return CommandParamType.OPERATOR; + case BLOCK_STATE: - case BLOCK_PREDICATE: + return BlockTranslator.getAllBlockIdentifiers(); + case ITEM_STACK: - case ITEM_PREDICATE: - case COLOR: - case COMPONENT: - case OBJECTIVE: - case OBJECTIVE_CRITERIA: - case OPERATION: // Possibly OPERATOR - case PARTICLE: - case ROTATION: - case SCOREBOARD_SLOT: - case SCORE_HOLDER: - case SWIZZLE: - case TEAM: - case ITEM_SLOT: - case MOB_EFFECT: - case FUNCTION: - case ENTITY_ANCHOR: - case RANGE: - case FLOAT_RANGE: + return ItemRegistry.ITEM_NAMES; + case ITEM_ENCHANTMENT: + return Enchantment.ALL_JAVA_IDENTIFIERS; //TODO: inventory branch use Java enums + case ENTITY_SUMMON: - case DIMENSION: - case TIME: + return EntityType.ALL_JAVA_IDENTIFIERS; + + case COLOR: + return VALID_COLORS; + + case SCOREBOARD_SLOT: + return VALID_SCOREBOARD_SLOTS; + default: return CommandParamType.STRING; } } @Getter - private class ParamInfo { - private CommandNode paramNode; - private CommandParamData paramData; - private List children; + @ToString + private static class ParamInfo { + private final CommandNode paramNode; + private final CommandParamData paramData; + private final List children; /** * Create a new parameter info object @@ -252,33 +290,50 @@ public ParamInfo(CommandNode paramNode, CommandParamData paramData) { * @param allNodes Every command node */ public void buildChildren(CommandNode[] allNodes) { - int enumIndex = -1; - for (int paramID : paramNode.getChildIndices()) { CommandNode paramNode = allNodes[paramID]; if (paramNode.getParser() == null) { - if (enumIndex == -1) { - enumIndex = children.size(); - - // Create the new enum command - CommandEnumData enumData = new CommandEnumData(paramNode.getName(), new String[] { paramNode.getName() }, false); - children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, enumData, mapCommandType(paramNode.getParser()), null, Collections.emptyList()))); - } else { - // Get the existing enum - ParamInfo enumParamInfo = children.get(enumIndex); + boolean foundCompatible = false; + for (int i = 0; i < children.size(); i++) { + ParamInfo enumParamInfo = children.get(i); + // Check to make sure all descending nodes of this command are compatible - otherwise, create a new overload + if (isCompatible(allNodes, enumParamInfo.getParamNode(), paramNode)) { + foundCompatible = true; + // Extend the current list of enum values + String[] enumOptions = Arrays.copyOf(enumParamInfo.getParamData().getEnumData().getValues(), enumParamInfo.getParamData().getEnumData().getValues().length + 1); + enumOptions[enumOptions.length - 1] = paramNode.getName(); + + // Re-create the command using the updated values + CommandEnumData enumData = new CommandEnumData(enumParamInfo.getParamData().getEnumData().getName(), enumOptions, false); + children.set(i, new ParamInfo(enumParamInfo.getParamNode(), new CommandParamData(enumParamInfo.getParamData().getName(), this.paramNode.isExecutable(), enumData, null, null, Collections.emptyList()))); + break; + } + } - // Extend the current list of enum values - String[] enumOptions = Arrays.copyOf(enumParamInfo.getParamData().getEnumData().getValues(), enumParamInfo.getParamData().getEnumData().getValues().length + 1); - enumOptions[enumOptions.length - 1] = paramNode.getName(); + if (!foundCompatible) { + // Create a new subcommand with this exact type + CommandEnumData enumData = new CommandEnumData(paramNode.getName(), new String[]{paramNode.getName()}, false); - // Re-create the command using the updated values - CommandEnumData enumData = new CommandEnumData(enumParamInfo.getParamData().getEnumData().getName(), enumOptions, false); - children.set(enumIndex, new ParamInfo(enumParamInfo.getParamNode(), new CommandParamData(enumParamInfo.getParamData().getName(), false, enumData, enumParamInfo.getParamData().getType(), null, Collections.emptyList()))); + // On setting optional: + // isExecutable is defined as a node "constitutes a valid command." + // Therefore, any children of the parameter must simply be optional. + children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), this.paramNode.isExecutable(), enumData, null, null, Collections.emptyList()))); } - }else{ + } else { // Put the non-enum param into the list - children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), false, null, mapCommandType(paramNode.getParser()), null, Collections.emptyList()))); + Object mappedType = mapCommandType(paramNode.getParser()); + CommandEnumData enumData = null; + CommandParamType type = null; + if (mappedType instanceof String[]) { + enumData = new CommandEnumData(paramNode.getParser().name().toLowerCase(), (String[]) mappedType, false); + } else { + type = (CommandParamType) mappedType; + } + // IF enumData != null: + // In game, this will show up like + // So if paramNode.getName() == "value" and enumData.getName() == "bool": + children.add(new ParamInfo(paramNode, new CommandParamData(paramNode.getName(), this.paramNode.isExecutable(), enumData, type, null, Collections.emptyList()))); } } @@ -288,6 +343,64 @@ public void buildChildren(CommandNode[] allNodes) { } } + /** + * Comparing CommandNode type a and b, determine if they are in the same overload. + *

+ * Take the gamerule command, and let's present three "subcommands" you can perform: + * + *

    + *
  • gamerule doDaylightCycle true
  • + *
  • gamerule announceAdvancements false
  • + *
  • gamerule randomTickSpeed 3
  • + *
+ * + * While all three of them are indeed part of the same command, the command setting randomTickSpeed parses an int, + * while the others use boolean. In Bedrock, this should be presented as a separate overload to indicate that this + * does something a little different. + *

+ * Therefore, this function will return true if the first two are compared, as they use the same + * parsers. If the third is compared with either of the others, this function will return false. + *

+ * Here's an example of how the above would be presented to Bedrock (as of 1.16.200). Notice how the top two CommandParamData + * classes of each array are identical in type, but the following class is different: + *

+         *     overloads=[
+         *         [
+         *            CommandParamData(name=doDaylightCycle, optional=false, enumData=CommandEnumData(name=announceAdvancements, values=[announceAdvancements, doDaylightCycle], isSoft=false), type=STRING, postfix=null, options=[])
+         *            CommandParamData(name=value, optional=false, enumData=CommandEnumData(name=value, values=[true, false], isSoft=false), type=null, postfix=null, options=[])
+         *         ]
+         *         [
+         *            CommandParamData(name=randomTickSpeed, optional=false, enumData=CommandEnumData(name=randomTickSpeed, values=[randomTickSpeed], isSoft=false), type=STRING, postfix=null, options=[])
+         *            CommandParamData(name=value, optional=false, enumData=null, type=INT, postfix=null, options=[])
+         *         ]
+         *     ]
+         * 
+ * + * @return if these two can be merged into one overload. + */ + private boolean isCompatible(CommandNode[] allNodes, CommandNode a, CommandNode b) { + if (a == b) return true; + if (a.getParser() != b.getParser()) return false; + if (a.getChildIndices().length != b.getChildIndices().length) return false; + + for (int i = 0; i < a.getChildIndices().length; i++) { + boolean hasSimilarity = false; + CommandNode a1 = allNodes[a.getChildIndices()[i]]; + // Search "b" until we find a child that matches this one + for (int j = 0; j < b.getChildIndices().length; j++) { + if (isCompatible(allNodes, a1, allNodes[b.getChildIndices()[j]])) { + hasSimilarity = true; + break; + } + } + + if (!hasSimilarity) { + return false; + } + } + return true; + } + /** * Get the tree of every parameter node (recursive) * @@ -301,13 +414,10 @@ public List getTree() { List childTree = child.getTree(); // Un-pack the tree append the child node to it and push into the list - for (CommandParamData[] subchild : childTree) { - CommandParamData[] tmpTree = new ArrayList() { - { - add(child.getParamData()); - addAll(Arrays.asList(subchild)); - } - }.toArray(new CommandParamData[0]); + for (CommandParamData[] subChild : childTree) { + CommandParamData[] tmpTree = new CommandParamData[subChild.length + 1]; + tmpTree[0] = child.getParamData(); + System.arraycopy(subChild, 0, tmpTree, 1, subChild.length); treeParamData.add(tmpTree); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java index 76e9b0958f0..8506389f3fb 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaKeepAliveTranslator.java @@ -39,6 +39,9 @@ public class JavaKeepAliveTranslator extends PacketTranslator iterator = session.getSkullCache().keySet().iterator(); while (iterator.hasNext()) { Vector3i position = iterator.next(); - if (Math.floor(position.getX() / 16) == packet.getX() && Math.floor(position.getZ() / 16) == packet.getZ()) { + if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) { session.getSkullCache().get(position).despawnEntity(session); iterator.remove(); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java index 4ffb96d0013..f85ac293c3c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java @@ -49,70 +49,72 @@ public class BlockStateValues { private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap(); private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap(); 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 * - * @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 blockData JsonNode of info about the block from blocks.json */ - public static void storeBlockStateValues(Map.Entry entry, int javaBlockState) { - JsonNode bannerColor = entry.getValue().get("banner_color"); + public static void storeBlockStateValues(String javaId, int javaBlockState, JsonNode blockData) { + JsonNode bannerColor = blockData.get("banner_color"); if (bannerColor != null) { BANNER_COLORS.put(javaBlockState, (byte) bannerColor.intValue()); 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) { BED_COLORS.put(javaBlockState, (byte) bedColor.intValue()); return; } - if (entry.getKey().contains("command_block")) { - COMMAND_BLOCK_VALUES.put(javaBlockState, entry.getKey().contains("conditional=true") ? (byte) 1 : (byte) 0); + if (javaId.contains("command_block")) { + COMMAND_BLOCK_VALUES.put(javaBlockState, javaId.contains("conditional=true") ? (byte) 1 : (byte) 0); return; } - if (entry.getValue().get("double_chest_position") != null) { - boolean isX = (entry.getValue().get("x") != null); - boolean isDirectionPositive = ((entry.getValue().get("x") != null && entry.getValue().get("x").asBoolean()) || - (entry.getValue().get("z") != null && entry.getValue().get("z").asBoolean())); - boolean isLeft = (entry.getValue().get("double_chest_position").asText().contains("left")); + if (blockData.get("double_chest_position") != null) { + boolean isX = (blockData.get("x") != null); + boolean isDirectionPositive = ((blockData.get("x") != null && blockData.get("x").asBoolean()) || + (blockData.get("z") != null && blockData.get("z").asBoolean())); + boolean isLeft = (blockData.get("double_chest_position").asText().contains("left")); DOUBLE_CHEST_VALUES.put(javaBlockState, new DoubleChestValue(isX, isDirectionPositive, isLeft)); return; } - if (entry.getKey().contains("potted_") || entry.getKey().contains("flower_pot")) { - FLOWER_POT_VALUES.put(javaBlockState, entry.getKey().replace("potted_", "")); + if (javaId.contains("potted_") || javaId.contains("flower_pot")) { + FLOWER_POT_VALUES.put(javaBlockState, javaId.replace("potted_", "")); return; } - JsonNode notePitch = entry.getValue().get("note_pitch"); + JsonNode notePitch = blockData.get("note_pitch"); if (notePitch != null) { - NOTEBLOCK_PITCHES.put(javaBlockState, entry.getValue().get("note_pitch").intValue()); + NOTEBLOCK_PITCHES.put(javaBlockState, blockData.get("note_pitch").intValue()); return; } - if (entry.getKey().contains("piston")) { + if (javaId.contains("piston")) { // True if extended, false if not - PISTON_VALUES.put(javaBlockState, entry.getKey().contains("extended=true")); - IS_STICKY_PISTON.put(javaBlockState, entry.getKey().contains("sticky")); + PISTON_VALUES.put(javaBlockState, javaId.contains("extended=true")); + IS_STICKY_PISTON.put(javaBlockState, javaId.contains("sticky")); return; } - JsonNode skullVariation = entry.getValue().get("variation"); + JsonNode skullVariation = blockData.get("variation"); if (skullVariation != null) { SKULL_VARIANTS.put(javaBlockState, (byte) skullVariation.intValue()); } - JsonNode skullRotation = entry.getValue().get("skull_rotation"); + JsonNode skullRotation = blockData.get("skull_rotation"); if (skullRotation != null) { SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue()); } - if (entry.getKey().contains("wall_skull") || entry.getKey().contains("wall_head")) { - String direction = entry.getKey().substring(entry.getKey().lastIndexOf("facing=") + 7); + if (javaId.contains("wall_skull") || javaId.contains("wall_head")) { + String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7); int rotation = 0; switch (direction.substring(0, direction.length() - 1)) { case "north": @@ -131,10 +133,16 @@ public static void storeBlockStateValues(Map.Entry entry, int SKULL_WALL_DIRECTIONS.put(javaBlockState, rotation); } - JsonNode shulkerDirection = entry.getValue().get("shulker_direction"); + JsonNode shulkerDirection = blockData.get("shulker_direction"); if (shulkerDirection != null) { 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 static Int2IntMap getSkullWallDirections() { public static byte getShulkerBoxDirection(int state) { 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); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java index d0a56dc6385..0fde056d9ce 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java @@ -182,7 +182,7 @@ public class BlockTranslator { JAVA_ID_BLOCK_MAP.put(javaId, javaRuntimeId); - BlockStateValues.storeBlockStateValues(entry, javaRuntimeId); + BlockStateValues.storeBlockStateValues(entry.getKey(), javaRuntimeId, entry.getValue()); String cleanJavaIdentifier = entry.getKey().split("\\[")[0]; @@ -383,4 +383,11 @@ public static String getPickItem(int javaId) { } return itemIdentifier; } + + /** + * @return a list of all Java block identifiers. For use with command suggestions. + */ + public static String[] getAllBlockIdentifiers() { + return JAVA_ID_TO_JAVA_IDENTIFIER_MAP.values().toArray(new String[0]); + } } diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java index 30c68c875f8..135bd57077f 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java @@ -33,6 +33,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.event.EventManager; import org.geysermc.connector.event.events.geyser.LoadBedrockSkinEvent; @@ -82,7 +83,7 @@ public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, U String capeId, byte[] capeData, SkinProvider.SkinGeometry geometry) { SerializedSkin serializedSkin = SerializedSkin.of( - skinId, geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), + skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), ImageData.of(capeData), geometry.getGeometryData(), "", true, false, !capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId ); @@ -165,7 +166,7 @@ public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSessio geometry = SkinProvider.SkinGeometry.getEars(data.isAlex()); // Store the skin and geometry for the ears - SkinProvider.storeEarSkin(entity.getUuid(), skin); + SkinProvider.storeEarSkin(skin); SkinProvider.storeEarGeometry(entity.getUuid(), data.isAlex()); } } @@ -273,7 +274,10 @@ public static GameProfileData from(GameProfile profile) { return new GameProfileData(skinUrl, capeUrl, isAlex); } catch (Exception exception) { - GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName() + ": " + exception.getMessage()); + GeyserConnector.getInstance().getLogger().debug("Something went wrong while processing skin for " + profile.getName()); + if (GeyserConnector.getInstance().getConfig().isDebugMode()) { + exception.printStackTrace(); + } return loadBedrockOrOfflineSkin(profile); } } @@ -288,7 +292,7 @@ private static GameProfileData loadBedrockOrOfflineSkin(GameProfile profile) { String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl(); String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl(); - if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) { + if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) { GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId()); if (session != null) { diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java index 3f236932a30..c4d4bc486af 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java @@ -79,13 +79,12 @@ public class SkinProvider { .build(); private static final Map> requestedCapes = new ConcurrentHashMap<>(); - public static final SkinGeometry EMPTY_GEOMETRY = SkinProvider.SkinGeometry.getLegacy(false); private static final Map cachedGeometry = new ConcurrentHashMap<>(); public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserConnector.getInstance().getConfig().isAllowThirdPartyEars(); - public static String EARS_GEOMETRY; - public static String EARS_GEOMETRY_SLIM; - public static SkinGeometry SKULL_GEOMETRY; + public static final String EARS_GEOMETRY; + public static final String EARS_GEOMETRY_SLIM; + public static final SkinGeometry SKULL_GEOMETRY; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -229,15 +228,15 @@ public static CompletableFuture requestUnofficialCape(Cape officialCape, U return CompletableFuture.completedFuture(officialCape); } - public static CompletableFuture requestEars(String earsUrl, EarsProvider provider, boolean newThread, Skin skin) { + public static CompletableFuture requestEars(String earsUrl, boolean newThread, Skin skin) { if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin); CompletableFuture future; if (newThread) { - future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl, provider), EXECUTOR_SERVICE) + future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE) .whenCompleteAsync((outSkin, throwable) -> { }); } else { - Skin ears = supplyEars(skin, earsUrl, provider); // blocking + Skin ears = supplyEars(skin, earsUrl); // blocking future = CompletableFuture.completedFuture(ears); } return future; @@ -255,7 +254,7 @@ public static CompletableFuture requestEars(String earsUrl, EarsProvider p public static CompletableFuture requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) { for (EarsProvider provider : EarsProvider.VALUES) { Skin skin1 = getOrDefault( - requestEars(provider.getUrlFor(playerId, username), provider, newThread, officialSkin), + requestEars(provider.getUrlFor(playerId, username), newThread, officialSkin), officialSkin, 4 ); if (skin1.isEars()) { @@ -295,12 +294,11 @@ public static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte } /** - * Stores the ajusted skin with the ear texture to the cache + * Stores the adjusted skin with the ear texture to the cache * - * @param playerID The UUID to cache it against * @param skin The skin to cache */ - public static void storeEarSkin(UUID playerID, Skin skin) { + public static void storeEarSkin(Skin skin) { cachedSkins.put(skin.getTextureUrl(), skin); } @@ -324,7 +322,7 @@ private static Skin supplySkin(UUID uuid, String textureUrl) { } private static Cape supplyCape(String capeUrl, CapeProvider provider) { - byte[] cape = new byte[0]; + byte[] cape = EMPTY_CAPE.getCapeData(); try { cape = requestImage(capeUrl, provider); } catch (Exception ignored) {} // just ignore I guess @@ -334,7 +332,7 @@ private static Cape supplyCape(String capeUrl, CapeProvider provider) { return new Cape( capeUrl, urlSection[urlSection.length - 1], // get the texture id and use it as cape id - cape.length > 0 ? cape : EMPTY_CAPE.getCapeData(), + cape, System.currentTimeMillis(), cape.length == 0 ); @@ -345,10 +343,9 @@ private static Cape supplyCape(String capeUrl, CapeProvider provider) { * * @param existingSkin The players current skin * @param earsUrl The URL to get the ears texture from - * @param provider The ears texture provider * @return The updated skin with ears */ - private static Skin supplyEars(Skin existingSkin, String earsUrl, EarsProvider provider) { + private static Skin supplyEars(Skin existingSkin, String earsUrl) { try { // Get the ears texture BufferedImage ears = ImageIO.read(new URL(earsUrl)); @@ -415,14 +412,15 @@ private static byte[] requestImage(String imageUrl, CapeProvider provider) throw // if the requested image is a cape if (provider != null) { - while(image.getWidth() > 64) { - image = scale(image); + if (image.getWidth() > 64) { + image = scale(image, 64, 32); + } + } else { + // Very rarely, skins can be larger than Minecraft's default. + // Bedrock will not render anything above a width of 128. + if (image.getWidth() > 128) { + image = scale(image, 128, image.getHeight() / (image.getWidth() / 128)); } - BufferedImage newImage = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB); - Graphics g = newImage.createGraphics(); - g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null); - g.dispose(); - image = newImage; } byte[] data = bufferedImageToImageData(image); @@ -506,12 +504,13 @@ private static BufferedImage readFiveZigCape(String url) throws IOException { return null; } - private static BufferedImage scale(BufferedImage bufferedImage) { - BufferedImage resized = new BufferedImage(bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, BufferedImage.TYPE_INT_ARGB); + private static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) { + BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = resized.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(bufferedImage, 0, 0, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2, null); + g2.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null); g2.dispose(); + bufferedImage.flush(); return resized; } @@ -579,17 +578,17 @@ public static T getOrDefault(CompletableFuture future, T defaultValue, in @AllArgsConstructor @Getter public static class SkinAndCape { - private Skin skin; - private Cape cape; + private final Skin skin; + private final Cape cape; } @AllArgsConstructor @Getter public static class Skin { private UUID skinOwner; - private String textureUrl; - private byte[] skinData; - private long requestedOn; + private final String textureUrl; + private final byte[] skinData; + private final long requestedOn; private boolean updated; private boolean ears; @@ -603,19 +602,19 @@ private Skin(long requestedOn, String textureUrl, byte[] skinData) { @AllArgsConstructor @Getter public static class Cape { - private String textureUrl; - private String capeId; - private byte[] capeData; - private long requestedOn; - private boolean failed; + private final String textureUrl; + private final String capeId; + private final byte[] capeData; + private final long requestedOn; + private final boolean failed; } @AllArgsConstructor @Getter public static class SkinGeometry { - private String geometryName; - private String geometryData; - private boolean failed; + private final String geometryName; + private final String geometryData; + private final boolean failed; /** * Generate generic geometry diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java index 644323a427d..7481b70bcdd 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkullSkinManager.java @@ -27,65 +27,42 @@ import com.nukkitx.protocol.bedrock.data.skin.ImageData; import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin; -import com.nukkitx.protocol.bedrock.packet.PlayerListPacket; +import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; import java.util.Collections; -import java.util.UUID; import java.util.function.Consumer; public class SkullSkinManager extends SkinManager { - public static PlayerListPacket.Entry buildSkullEntryManually(UUID uuid, String username, long geyserId, - String skinId, byte[] skinData) { + public static SerializedSkin buildSkullEntryManually(String skinId, byte[] skinData) { // Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png skinId = skinId + "_skull"; - SerializedSkin serializedSkin = SerializedSkin.of( - skinId, SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), + return SerializedSkin.of( + skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(), ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(), "", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId ); - - PlayerListPacket.Entry entry = new PlayerListPacket.Entry(uuid); - entry.setName(username); - entry.setEntityId(geyserId); - entry.setSkin(serializedSkin); - entry.setXuid(""); - entry.setPlatformChatId(""); - entry.setTeacher(false); - entry.setTrustedSkin(true); - return entry; } public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session, Consumer skinConsumer) { GameProfileData data = GameProfileData.from(entity.getProfile()); - SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), false) + SkinProvider.requestSkin(entity.getUuid(), data.getSkinUrl(), true) .whenCompleteAsync((skin, throwable) -> { try { if (session.getUpstream().isInitialized()) { - PlayerListPacket.Entry updatedEntry = buildSkullEntryManually( - entity.getUuid(), - entity.getUsername(), - entity.getGeyserId(), - skin.getTextureUrl(), - skin.getSkinData() - ); - - PlayerListPacket playerAddPacket = new PlayerListPacket(); - playerAddPacket.setAction(PlayerListPacket.Action.ADD); - playerAddPacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerAddPacket); - - // It's a skull. We don't want them in the player list. - PlayerListPacket playerRemovePacket = new PlayerListPacket(); - playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE); - playerRemovePacket.getEntries().add(updatedEntry); - session.sendUpstreamPacket(playerRemovePacket); + PlayerSkinPacket packet = new PlayerSkinPacket(); + packet.setUuid(entity.getUuid()); + packet.setOldSkinName(""); + packet.setNewSkinName(skin.getTextureUrl()); + packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData())); + packet.setTrustedSkin(true); + session.sendUpstreamPacket(packet); } } catch (Exception e) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e); diff --git a/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java b/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java index 5a49fd9beed..816b718aa05 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/CooldownUtils.java @@ -26,7 +26,6 @@ package org.geysermc.connector.utils; import com.nukkitx.protocol.bedrock.packet.SetTitlePacket; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import java.util.concurrent.TimeUnit; @@ -36,11 +35,10 @@ * Much of the work here is from the wonderful folks from ViaRewind: https://github.com/ViaVersion/ViaRewind */ public class CooldownUtils { + private static boolean SHOW_COOLDOWN; - private final static boolean SHOW_COOLDOWN; - - static { - SHOW_COOLDOWN = GeyserConnector.getInstance().getConfig().isShowCooldown(); + public static void setShowCooldown(boolean showCooldown) { + SHOW_COOLDOWN = showCooldown; } /** @@ -116,5 +114,4 @@ private static String getTitle(GeyserSession session) { } return builder.toString(); } - } diff --git a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java index 8349d7c8771..1715eb4eb00 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -92,21 +92,22 @@ public static File fileOrCopiedFromResource(String name, Function format) throws IOException { if (!file.exists()) { + //noinspection ResultOfMethodCallIgnored file.createNewFile(); - FileOutputStream fos = new FileOutputStream(file); - InputStream input = GeyserConnector.class.getResourceAsStream("/" + name); // resources need leading "/" prefix + try (FileOutputStream fos = new FileOutputStream(file)) { + try (InputStream input = GeyserConnector.class.getResourceAsStream("/" + name)) { // resources need leading "/" prefix + byte[] bytes = new byte[input.available()]; - byte[] bytes = new byte[input.available()]; + //noinspection ResultOfMethodCallIgnored + input.read(bytes); - input.read(bytes); + for(char c : format.apply(new String(bytes)).toCharArray()) { + fos.write(c); + } - for(char c : format.apply(new String(bytes)).toCharArray()) { - fos.write(c); + fos.flush(); + } } - - fos.flush(); - input.close(); - fos.close(); } return file; @@ -124,14 +125,13 @@ public static void writeFile(File file, char[] data) throws IOException { file.createNewFile(); } - FileOutputStream fos = new FileOutputStream(file); + try (FileOutputStream fos = new FileOutputStream(file)) { + for (char c : data) { + fos.write(c); + } - for (char c : data) { - fos.write(c); + fos.flush(); } - - fos.flush(); - fos.close(); } /** @@ -237,9 +237,10 @@ public static byte[] readAllBytes(InputStream stream) { try { int size = stream.available(); byte[] bytes = new byte[size]; - BufferedInputStream buf = new BufferedInputStream(stream); - buf.read(bytes, 0, bytes.length); - buf.close(); + try (BufferedInputStream buf = new BufferedInputStream(stream)) { + //noinspection ResultOfMethodCallIgnored + buf.read(bytes, 0, bytes.length); + } return bytes; } catch (IOException e) { throw new RuntimeException("Error while trying to read input stream!"); diff --git a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java index 1a1f758d69d..5284bbcff17 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -67,8 +67,8 @@ public static void loadGeyserLocale(String locale) { // Load the locale if (localeStream != null) { Properties localeProp = new Properties(); - try { - localeProp.load(new InputStreamReader(localeStream, StandardCharsets.UTF_8)); + try (InputStreamReader reader = new InputStreamReader(localeStream, StandardCharsets.UTF_8)) { + localeProp.load(reader); } catch (Exception e) { throw new AssertionError(getLocaleStringLog("geyser.language.load_failed", locale), e); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java index 15a52cf7f9d..db5457244d5 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LocaleUtils.java @@ -147,6 +147,12 @@ private static void downloadLocale(String locale) { } } } catch (IOException ignored) { } + + if (clientJarInfo == null) { + // Likely failed to download + GeyserConnector.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null."); + return; + } targetHash = clientJarInfo.getSha1(); } else { curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile)); @@ -168,9 +174,13 @@ private static void downloadLocale(String locale) { return; } - // Get the hash and download the locale - String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash(); - WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString()); + try { + // Get the hash and download the locale + String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash(); + WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString()); + } catch (Exception e) { + GeyserConnector.getInstance().getLogger().error("Unable to download locale file hash", e); + } } /** @@ -236,23 +246,22 @@ private static void downloadEN_US(File localeFile) { WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString()); // Load in the JAR as a zip and extract the file - ZipFile localeJar = new ZipFile(tmpFilePath.toString()); - InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json")); - FileOutputStream outStream = new FileOutputStream(localeFile); - - // Write the file to the locale dir - byte[] buf = new byte[fileStream.available()]; - int length; - while ((length = fileStream.read(buf)) != -1) { - outStream.write(buf, 0, length); - } - - // Flush all changes to disk and cleanup - outStream.flush(); - outStream.close(); + try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) { + try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) { + try (FileOutputStream outStream = new FileOutputStream(localeFile)) { + + // Write the file to the locale dir + byte[] buf = new byte[fileStream.available()]; + int length; + while ((length = fileStream.read(buf)) != -1) { + outStream.write(buf, 0, length); + } - fileStream.close(); - localeJar.close(); + // Flush all changes to disk and cleanup + outStream.flush(); + } + } + } // Store the latest jar hash FileUtils.writeFile(GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray()); diff --git a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java index bcb1ffd503c..91d1b782e66 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java @@ -30,6 +30,8 @@ import java.io.File; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** @@ -70,10 +72,12 @@ public static void loadPacks() { pack.sha256 = FileUtils.calculateSHA256(file); + Stream stream = null; try { ZipFile zip = new ZipFile(file); - zip.stream().forEach((x) -> { + stream = zip.stream(); + stream.forEach((x) -> { if (x.getName().contains("manifest.json")) { try { ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class); @@ -94,6 +98,10 @@ public static void loadPacks() { } catch (Exception e) { GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.resource_pack.broken", file.getName())); e.printStackTrace(); + } finally { + if (stream != null) { + stream.close(); + } } } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java index 77afda53d1d..c01378d0012 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java @@ -147,7 +147,7 @@ public static boolean handleSettingsForm(GeyserSession session, String response) } if (Boolean.class.equals(gamerule.getType())) { - Boolean value = settingsResponse.getToggleResponses().get(offset).booleanValue(); + boolean value = settingsResponse.getToggleResponses().get(offset); if (value != session.getConnector().getWorldManager().getGameRuleBool(session, gamerule)) { session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); } diff --git a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java index 874fb062021..ba008da4176 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java @@ -88,8 +88,7 @@ public static void downloadFile(String reqURL, String fileLocation) { } public static String post(String reqURL, String postContent) throws IOException { - URL url = null; - url = new URL(reqURL); + URL url = new URL(reqURL); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "text/plain"); @@ -112,7 +111,7 @@ public static String post(String reqURL, String postContent) throws IOException */ private static String connectionToString(HttpURLConnection con) throws IOException { // Send the request (we dont use this but its required for getErrorStream() to work) - int code = con.getResponseCode(); + con.getResponseCode(); // Read the error message if there is one if not just read normally InputStream inputStream = con.getErrorStream(); @@ -120,17 +119,17 @@ private static String connectionToString(HttpURLConnection con) throws IOExcepti inputStream = con.getInputStream(); } - BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); - String inputLine; - StringBuffer content = new StringBuffer(); + StringBuilder content = new StringBuilder(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) { + String inputLine; - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - content.append("\n"); - } + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + content.append("\n"); + } - in.close(); - con.disconnect(); + con.disconnect(); + } return content.toString(); } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 07b73173e5b..ce202a3c1f1 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -18,10 +18,19 @@ bedrock: # This option is for the plugin version only. clone-remote-port: false # The MOTD that will be broadcasted to Minecraft: Bedrock Edition clients. This is irrelevant if "passthrough-motd" is set to true - motd1: "GeyserMC" - motd2: "Another GeyserMC forced host." + # If either of these are empty, the respective string will default to "Geyser" + motd1: "Geyser" + motd2: "Another Geyser server." # The Server Name that will be sent to Minecraft: Bedrock Edition clients. This is visible in both the pause menu and the settings menu. server-name: "Geyser" + # Whether to enable PROXY protocol or not for clients. You DO NOT WANT this feature unless you run UDP reverse proxy + # in front of your Geyser instance. + enable-proxy-protocol: false + # A list of allowed PROXY protocol speaking proxy IP addresses/subnets. Only effective when "enable-proxy-protocol" is enabled, and + # should really only be used when you are not able to use a proper firewall (usually true with shared hosting providers etc.). + # Keeping this list empty means there is no IP address whitelist. + # Both IP addresses and subnets are supported. + #proxy-protocol-whitelisted-ips: [ "127.0.0.1", "172.18.0.0/16" ] remote: # The IP address of the remote (Java Edition) server # If it is "auto", for standalone version the remote address will be set to 127.0.0.1, @@ -81,7 +90,11 @@ legacy-ping-passthrough: false # Increase if you are getting BrokenPipe errors. ping-passthrough-interval: 3 -# Maximum amount of players that can connect +# Whether to forward player ping to the server. While enabling this will allow Bedrock players to have more accurate +# ping, it may also cause players to time out more easily. +forward-player-ping: false + +# Maximum amount of players that can connect. This is only visual at this time and does not actually limit player count. max-players: 100 # If debug messages should be sent through console