From eccb48844eb491ebe382d32ca55daaa69c23b802 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 1 Jan 2021 16:55:04 -0500 Subject: [PATCH 01/23] Allow enderman to make provoked sound when angry (#1763) --- .../entity/living/monster/EndermanEntity.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java index e12b60d61..3151ae474 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EndermanEntity.java @@ -27,8 +27,10 @@ package org.geysermc.connector.entity.living.monster; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; +import com.nukkitx.protocol.bedrock.data.SoundEvent; import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.packet.LevelSoundEvent2Packet; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; @@ -45,11 +47,21 @@ public class EndermanEntity extends MonsterEntity { if (entityMetadata.getId() == 15) { metadata.put(EntityData.CARRIED_BLOCK, BlockTranslator.getBedrockBlockId((int) entityMetadata.getValue())); } - // 'Angry' - mouth open + // "Is screaming" - controls sound if (entityMetadata.getId() == 16) { + if ((boolean) entityMetadata.getValue()) { + LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + packet.setSound(SoundEvent.STARE); + packet.setPosition(this.position); + packet.setExtraData(-1); + packet.setIdentifier("minecraft:enderman"); + session.sendUpstreamPacket(packet); + } + } + // "Is staring/provoked" - controls visuals + if (entityMetadata.getId() == 17) { metadata.getFlags().setFlag(EntityFlag.ANGRY, (boolean) entityMetadata.getValue()); } - // TODO: ID 17 is stared at but I don't believe it's used - maybe only for the sound effect. Check after particle merge super.updateBedrockMetadata(entityMetadata, session); } } From 396d1b6b61b035725ce158f26fbfe9cbbf62ef94 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 1 Jan 2021 18:33:21 -0500 Subject: [PATCH 02/23] Fix items on campfires (#1779) --- .../world/block/entity/CampfireBlockEntityTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java index 5c30782c4..7ae4f315d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/CampfireBlockEntityTranslator.java @@ -47,7 +47,7 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator { protected NbtMap getItem(CompoundTag tag) { ItemEntry entry = ItemRegistry.getItemEntry((String) tag.get("id").getValue()); NbtMapBuilder tagBuilder = NbtMap.builder() - .putShort("id", (short) entry.getBedrockId()) + .putString("Name", entry.getBedrockIdentifier()) .putByte("Count", (byte) tag.get("Count").getValue()) .putShort("Damage", (short) entry.getBedrockData()); tagBuilder.put("tag", NbtMap.builder().build()); From 1a08e1104d357315d1103a7e641fc8053b4c677d Mon Sep 17 00:00:00 2001 From: YHDiamond <47502993+YHDiamond@users.noreply.github.com> Date: Sat, 2 Jan 2021 18:51:41 -0500 Subject: [PATCH 03/23] Fix stopsound bug (#1771) * Fix stopsound not working bug * removed extra imports * Update JavaPlayerStopSoundTranslator.java * Update JavaPlayerStopSoundTranslator.java * Update JavaPlayerStopSoundTranslator.java * Fix packet names and fix specific sounds not stopping Co-authored-by: YHDiamond <47502993+yehudahrrs@users.noreply.github.com> Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- ...ator.java => JavaPlaySoundTranslator.java} | 6 +++--- .../JavaStopSoundTranslator.java} | 21 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) rename connector/src/main/java/org/geysermc/connector/network/translators/java/world/{JavaPlayerPlaySoundTranslator.java => JavaPlaySoundTranslator.java} (93%) rename connector/src/main/java/org/geysermc/connector/network/translators/java/{entity/player/JavaPlayerStopSoundTranslator.java => world/JavaStopSoundTranslator.java} (82%) diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java similarity index 93% rename from connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java index bce96f65d..238e9ba32 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlayerPlaySoundTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaPlaySoundTranslator.java @@ -36,14 +36,14 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.sound.SoundRegistry; @Translator(packet = ServerPlaySoundPacket.class) -public class JavaPlayerPlaySoundTranslator extends PacketTranslator { +public class JavaPlaySoundTranslator extends PacketTranslator { @Override public void translate(ServerPlaySoundPacket packet, GeyserSession session) { String packetSound; - if(packet.getSound() instanceof BuiltinSound) { + if (packet.getSound() instanceof BuiltinSound) { packetSound = ((BuiltinSound) packet.getSound()).getName(); - } else if(packet.getSound() instanceof CustomSound) { + } else if (packet.getSound() instanceof CustomSound) { packetSound = ((CustomSound) packet.getSound()).getName(); } else { session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString()); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java similarity index 82% rename from connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java rename to connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java index 61856755f..d7d0f0738 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerStopSoundTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaStopSoundTranslator.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.connector.network.translators.java.entity.player; +package org.geysermc.connector.network.translators.java.world; import com.github.steveice10.mc.protocol.data.game.world.sound.BuiltinSound; import com.github.steveice10.mc.protocol.data.game.world.sound.CustomSound; @@ -35,26 +35,35 @@ import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.sound.SoundRegistry; @Translator(packet = ServerStopSoundPacket.class) -public class JavaPlayerStopSoundTranslator extends PacketTranslator { +public class JavaStopSoundTranslator extends PacketTranslator { @Override public void translate(ServerStopSoundPacket packet, GeyserSession session) { + // Runs if all sounds are stopped + if (packet.getSound() == null) { + StopSoundPacket stopPacket = new StopSoundPacket(); + stopPacket.setStoppingAllSound(true); + stopPacket.setSoundName(""); + session.sendUpstreamPacket(stopPacket); + return; + } + String packetSound; - if(packet.getSound() instanceof BuiltinSound) { + if (packet.getSound() instanceof BuiltinSound) { packetSound = ((BuiltinSound) packet.getSound()).getName(); - } else if(packet.getSound() instanceof CustomSound) { + } else if (packet.getSound() instanceof CustomSound) { packetSound = ((CustomSound) packet.getSound()).getName(); } else { session.getConnector().getLogger().debug("Unknown sound packet, we were unable to map this. " + packet.toString()); return; } - SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound); + SoundRegistry.SoundMapping soundMapping = SoundRegistry.fromJava(packetSound.replace("minecraft:", "")); session.getConnector().getLogger() .debug("[StopSound] Sound mapping " + packetSound + " -> " + soundMapping + (soundMapping == null ? "[not found]" : "") + " - " + packet.toString()); String playsound; - if(soundMapping == null || soundMapping.getPlaysound() == null) { + if (soundMapping == null || soundMapping.getPlaysound() == null) { // no mapping session.getConnector().getLogger() .debug("[StopSound] Defaulting to sound server gave us."); From 1c7567d79de1f064bd9c4f82a6d0921a39097f65 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 3 Jan 2021 12:53:26 -0500 Subject: [PATCH 04/23] Various resource pack fixes (#1769) - Fixes an instance where an invalid pack_manifest file could be present - Fixes instances where JSON files were not read as UTF-8 --- .../org/geysermc/connector/utils/FileUtils.java | 4 +++- .../geysermc/connector/utils/ResourcePack.java | 15 +++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) 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 fd3e9a8db..862af548d 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/FileUtils.java @@ -36,6 +36,7 @@ import org.reflections.util.ConfigurationBuilder; import java.io.*; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.MessageDigest; import java.util.function.Function; @@ -62,7 +63,8 @@ public class FileUtils { } public static T loadJson(InputStream src, Class valueType) throws IOException { - return GeyserConnector.JSON_MAPPER.readValue(src, valueType); + // Read specifically with UTF-8 to allow any non-UTF-encoded JSON to read + return GeyserConnector.JSON_MAPPER.readValue(new InputStreamReader(src, StandardCharsets.UTF_8), valueType); } /** 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 16a1812ee..bcb1ffd50 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java @@ -63,7 +63,7 @@ public class ResourcePack { // As we just created the directory it will be empty return; } - + for (File file : directory.listFiles()) { if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) { ResourcePack pack = new ResourcePack(); @@ -77,12 +77,15 @@ public class ResourcePack { if (x.getName().contains("manifest.json")) { try { ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class); + // Sometimes a pack_manifest file is present and not in a valid format, + // but a manifest file is, so we null check through that one + if (manifest.getHeader().getUuid() != null) { + pack.file = file; + pack.manifest = manifest; + pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion()); - pack.file = file; - pack.manifest = manifest; - pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion()); - - PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack); + PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack); + } } catch (Exception e) { e.printStackTrace(); } From 3f7d669676b1f805091d14d21b7799fb2c98c692 Mon Sep 17 00:00:00 2001 From: Kooldude183 <33386263+Kooldude183@users.noreply.github.com> Date: Sun, 3 Jan 2021 09:54:07 -0800 Subject: [PATCH 05/23] Update README.md (#1774) * Update README.md Add v1.16.201 in list of supported Bedrock versions * Update README.md Change to `v1.16.100 - v1.16.201` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 816f765d6..1d1657edc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have now joined us here! -### Currently supporting Minecraft Bedrock v1.16.100/v1.16.101/v1.16.200 and Minecraft Java v1.16.4. +### Currently supporting Minecraft Bedrock v1.16.100 - v1.16.201 and Minecraft Java v1.16.4. ## Setting Up Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set up Geyser. From 50b80a64d3e8de39c82fa454453bcfd31bc544a7 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 3 Jan 2021 19:06:20 -0500 Subject: [PATCH 06/23] Dimension switching cleanup (#1694) * Dimension switching cleanup Cleans up dimension switching logic that should no longer be needed. Also fixes above Nether Bedrock building dimension switching. * Clear thunder on dimension switch too * Clarify fake dimension switch function name * Javadoc that --- .../network/session/GeyserSession.java | 21 ++++----------- .../player/BedrockActionTranslator.java | 15 +++++------ .../player/BedrockMovePlayerTranslator.java | 2 +- .../java/JavaJoinGameTranslator.java | 4 +-- .../java/JavaRespawnTranslator.java | 26 +++++++++++-------- .../JavaPlayerPositionRotationTranslator.java | 6 ++--- .../connector/utils/DimensionUtils.java | 21 +++++++++++---- 7 files changed, 47 insertions(+), 48 deletions(-) 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 cb8425868..375c747d4 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 @@ -37,7 +37,6 @@ import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionRotationPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; -import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket; import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; import com.github.steveice10.packetlib.BuiltinFlags; import com.github.steveice10.packetlib.Client; @@ -95,7 +94,6 @@ import java.security.spec.InvalidKeySpecException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.atomic.AtomicInteger; @Getter public class GeyserSession implements CommandSender { @@ -148,7 +146,11 @@ public class GeyserSession implements CommandSender { @Setter private GameMode gameMode = GameMode.SURVIVAL; - private final AtomicInteger pendingDimSwitches = new AtomicInteger(0); + /** + * Keeps track of the world name for respawning. + */ + @Setter + private String worldName = null; private boolean sneaking; @@ -185,9 +187,6 @@ public class GeyserSession implements CommandSender { @Setter private Vector3i lastInteractionPosition = Vector3i.ZERO; - private boolean manyDimPackets = false; - private ServerRespawnPacket lastDimPacket = null; - @Setter private Entity ridingVehicleEntity; @@ -537,16 +536,6 @@ public class GeyserSession implements CommandSender { @Override public void packetReceived(PacketReceivedEvent event) { if (!closed) { - //handle consecutive respawn packets - if (event.getPacket().getClass().equals(ServerRespawnPacket.class)) { - manyDimPackets = lastDimPacket != null; - lastDimPacket = event.getPacket(); - return; - } else if (lastDimPacket != null) { - PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(lastDimPacket.getClass(), lastDimPacket, GeyserSession.this); - lastDimPacket = null; - } - // Required, or else Floodgate players break with Bukkit chunk caching if (event.getPacket() instanceof LoginSuccessPacket) { GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); 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 106a0d8da..66ec0a07b 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 @@ -43,7 +43,6 @@ import org.geysermc.connector.entity.Entity; 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.collision.CollisionManager; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.BlockUtils; @@ -157,14 +156,12 @@ public class BedrockActionTranslator extends PacketTranslator 0) return; + if (!session.isSpawned()) return; if (!session.getUpstream().isInitialized()) { MoveEntityAbsolutePacket moveEntityBack = new MoveEntityAbsolutePacket(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java index 292cfc74d..e9a1901dc 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java @@ -55,12 +55,12 @@ public class JavaJoinGameTranslator extends PacketTranslator @Override public void translate(ServerRespawnPacket packet, GeyserSession session) { Entity entity = session.getPlayerEntity(); - if (entity == null) - return; float maxHealth = entity.getAttributes().containsKey(AttributeType.MAX_HEALTH) ? entity.getAttributes().get(AttributeType.MAX_HEALTH).getValue() : 20f; // Max health must be divisible by two in bedrock @@ -66,18 +64,24 @@ public class JavaRespawnTranslator extends PacketTranslator session.setRaining(false); } + if (session.isThunder()) { + LevelEventPacket stopThunderPacket = new LevelEventPacket(); + stopThunderPacket.setType(LevelEventType.STOP_THUNDERSTORM); + stopThunderPacket.setData(0); + stopThunderPacket.setPosition(Vector3f.ZERO); + session.sendUpstreamPacket(stopThunderPacket); + session.setThunder(false); + } + String newDimension = DimensionUtils.getNewDimension(packet.getDimension()); - if (!session.getDimension().equals(newDimension)) { - DimensionUtils.switchDimension(session, newDimension); - } else { - if (session.isManyDimPackets()) { //reloading world - String fakeDim = session.getDimension().equals(DimensionUtils.OVERWORLD) ? DimensionUtils.NETHER : DimensionUtils.OVERWORLD; + if (!session.getDimension().equals(newDimension) || !packet.getWorldName().equals(session.getWorldName())) { + if (!packet.getWorldName().equals(session.getWorldName()) && session.getDimension().equals(newDimension)) { + // Switching to a new world (based off the world name change); send a fake dimension change + String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension); DimensionUtils.switchDimension(session, fakeDim); - DimensionUtils.switchDimension(session, newDimension); - } else { - // Handled in JavaPlayerPositionRotationTranslator - session.setSpawned(false); } + session.setWorldName(packet.getWorldName()); + DimensionUtils.switchDimension(session, newDimension); } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java index 10d451df5..d5d8e7d3c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java @@ -46,13 +46,11 @@ public class JavaPlayerPositionRotationTranslator extends PacketTranslator 0) { - ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); - } Vector3i pos = Vector3i.from(0, Short.MAX_VALUE, 0); @@ -150,4 +145,20 @@ public class DimensionUtils { // Change dimension ID to the End to allow for building above Bedrock BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? 2 : 1; } + + /** + * Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional + * dimension switch. + * + * @param currentDimension the current dimension of the player + * @param newDimension the new dimension that the player will be transferred to + * @return the fake dimension to transfer to + */ + public static String getTemporaryDimension(String currentDimension, String newDimension) { + if (BEDROCK_NETHER_ID == 2) { + // Prevents rare instances of Bedrock locking up + return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER; + } + return currentDimension.equals(OVERWORLD) ? NETHER : OVERWORLD; + } } From 446f16bc33beabb59ae337c799451c1b9c6d7c23 Mon Sep 17 00:00:00 2001 From: D3ATHBRINGER13 <53559772+D3ATHBRINGER13@users.noreply.github.com> Date: Mon, 4 Jan 2021 19:14:55 +0000 Subject: [PATCH 07/23] Update LICENSE to 2021 (#1799) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index acd4af141..0e368d546 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2020 GeyserMC. http://geysermc.org +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 From 69dc4a964427d25c816b89a7814f24654a57c738 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 4 Jan 2021 15:54:52 -0500 Subject: [PATCH 08/23] [ci-skip] Improve README for better information (#1702) --- .github/ISSUE_TEMPLATE/bug_report.md | 47 +++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 326a1ebd5..17e88f268 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,34 +7,51 @@ assignees: '' --- + + - + **Describe the bug** - + +A clear and concise description of what the bug is. **To Reproduce** - - - - - + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error **Expected behavior** - + +A clear and concise description of what you expected to happen. **Screenshots / Videos** - -**Server Version** - +If applicable, add screenshots to help explain your problem. -**Geyser Version** - +**Server Version and Plugins** + +If you just run Geyser-Spigot, you can leave this area blank as the next section covers this information. + +If you're running a multi-server instance, or using Geyser Standalone: + +- Give us the exact output from `/version` on all servers involved. Saying "latest" does not help us at all. +- Please list all plugins on all servers involved. + +If this bug occurs on a server you do not control, please fill this in to the best of your knowledge. + +**Geyser Dump** + +If Geyser starts correctly, please also include the link to a dump by using `/geyser dump`. If you use the Standalone GUI, the option can be found under `Commands` => `Dump`. This provides us information about your server that we can use to debug your issue. **Minecraft: Bedrock Edition Version** - + +The version of your Minecraft: Bedrock Edition client you tested with, along with your device type (e.g. Windows 10, Switch...). **Additional Context** - + +Add any other context about the problem here. From 29e47cf636fe0520012dfe6754b56f2afb31a735 Mon Sep 17 00:00:00 2001 From: Nickg two Date: Mon, 4 Jan 2021 19:24:07 -0700 Subject: [PATCH 09/23] [ci skip] changed a 2020 to 2021 (#1801) lol imagine being in 2020 --- licenseheader.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/licenseheader.txt b/licenseheader.txt index c22c426c4..8ef205a31 100644 --- a/licenseheader.txt +++ b/licenseheader.txt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * 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 From b6389317f0c1aeed50fdea242ce82ad02140fd99 Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Tue, 5 Jan 2021 09:45:01 -0900 Subject: [PATCH 10/23] Fix minor bug in auth form (#1806) --- .../org/geysermc/connector/utils/LoginEncryptionUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 9e16c428d..5c212ba02 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -198,12 +198,12 @@ public class LoginEncryptionUtils { String password = response.getInputResponses().get(2); session.authenticate(email, password); + + // Clear windows so authentication data isn't accidentally cached + windowCache.getWindows().clear(); } else { showLoginDetailsWindow(session); } - - // Clear windows so authentication data isn't accidentally cached - windowCache.getWindows().clear(); } else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) { SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); if (response != null) { From 0641800be7a460f5bd0d256796e5f1e4c48dc4c2 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 5 Jan 2021 18:41:20 -0500 Subject: [PATCH 11/23] Add Tickable interface (#1790) * Add Tickable interface By having a tickable interface, we're only dedicating one thread to ticking entities and running tasks as opposed to several. This will also help with implementing world border support. * removeEntity already clears tickableEntities for us * Only tick the entity if it's not being ticked --- .../entity/ItemedFireballEntity.java | 6 +- .../connector/entity/ThrowableEntity.java | 38 +++----- .../geysermc/connector/entity/Tickable.java | 35 +++++++ .../living/monster/EnderDragonEntity.java | 21 ++--- .../network/session/GeyserSession.java | 39 +++++++- .../network/session/cache/EntityCache.java | 24 ++++- .../player/BedrockMovePlayerTranslator.java | 94 +------------------ .../collision/CollisionManager.java | 64 +++++++++++++ .../connector/utils/DimensionUtils.java | 4 - 9 files changed, 183 insertions(+), 142 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/entity/Tickable.java 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 488c0e90c..2b411109a 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ItemedFireballEntity.java @@ -38,11 +38,11 @@ public class ItemedFireballEntity extends ThrowableEntity { } @Override - protected void updatePosition(GeyserSession session) { + public void tick(GeyserSession session) { position = position.add(motion); // TODO: While this reduces latency in position updating (needed for better fireball reflecting), - // TODO: movement is incredibly stiff. See if the MoveEntityDeltaPacket in 1.16.100 fixes this, and if not, - // TODO: only use this laggy movement for fireballs that be reflected + // 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); 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 553e558ea..4e0c25ab5 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/ThrowableEntity.java @@ -33,50 +33,35 @@ import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockTranslator; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - /** * Used as a class for any object-like entity that moves as a projectile */ -public class ThrowableEntity extends Entity { +public class ThrowableEntity extends Entity implements Tickable { private Vector3f lastPosition; - /** - * Updates the position for the Bedrock client. - * - * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions - */ - protected ScheduledFuture positionUpdater; public ThrowableEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); this.lastPosition = position; } + /** + * Updates the position for the Bedrock client. + * + * Java clients assume the next positions of moving items. Bedrock needs to be explicitly told positions + */ @Override - public void spawnEntity(GeyserSession session) { - super.spawnEntity(session); - positionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> { - if (session.isClosed()) { - positionUpdater.cancel(true); - return; - } - updatePosition(session); - }, 0, 50, TimeUnit.MILLISECONDS); - } - - protected void moveAbsoluteImmediate(GeyserSession session, Vector3f position, Vector3f rotation, boolean isOnGround, boolean teleported) { - super.moveAbsolute(session, position, rotation, isOnGround, teleported); - } - - protected void updatePosition(GeyserSession session) { + public void tick(GeyserSession session) { super.moveRelative(session, motion.getX(), motion.getY(), motion.getZ(), rotation, onGround); float drag = getDrag(session); float gravity = getGravity(); 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); + } + /** * Get the gravity of this entity type. Used for applying gravity while the entity is in motion. * @@ -140,7 +125,6 @@ public class ThrowableEntity extends Entity { @Override public boolean despawnEntity(GeyserSession session) { - positionUpdater.cancel(true); if (entityType == EntityType.THROWN_ENDERPEARL) { LevelEventPacket particlePacket = new LevelEventPacket(); particlePacket.setType(LevelEventType.PARTICLE_TELEPORT); diff --git a/connector/src/main/java/org/geysermc/connector/entity/Tickable.java b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java new file mode 100644 index 000000000..a7d571ccb --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/entity/Tickable.java @@ -0,0 +1,35 @@ +/* + * 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.entity; + +import org.geysermc.connector.network.session.GeyserSession; + +/** + * Implemented onto anything that should have code ran every Minecraft tick - 50 milliseconds. + */ +public interface Tickable { + void tick(GeyserSession session); +} diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java index 7dbd96a44..df9456826 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java @@ -33,14 +33,12 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.packet.AddEntityPacket; import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; import lombok.Data; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.living.InsentientEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public class EnderDragonEntity extends InsentientEntity { +public class EnderDragonEntity extends InsentientEntity implements Tickable { /** * The Ender Dragon has multiple hit boxes, which * are each its own invisible entity @@ -63,8 +61,6 @@ public class EnderDragonEntity extends InsentientEntity { private boolean hovering; - private ScheduledFuture partPositionUpdater; - public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); @@ -130,24 +126,23 @@ public class EnderDragonEntity extends InsentientEntity { segmentHistory[i].y = position.getY(); } - partPositionUpdater = session.getConnector().getGeneralThreadPool().scheduleAtFixedRate(() -> { - pushSegment(); - updateBoundingBoxes(session); - }, 0, 50, TimeUnit.MILLISECONDS); - session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); } @Override public boolean despawnEntity(GeyserSession session) { - partPositionUpdater.cancel(true); - for (EnderDragonPartEntity part : allParts) { part.despawnEntity(session); } return super.despawnEntity(session); } + @Override + public void tick(GeyserSession session) { + pushSegment(); + updateBoundingBoxes(session); + } + /** * Updates the positions of the Ender Dragon's multiple bounding boxes * 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 375c747d4..5a0d5eeb1 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 @@ -35,6 +35,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.statistic.Statistic; import com.github.steveice10.mc.protocol.data.game.window.VillagerTrade; import com.github.steveice10.mc.protocol.packet.handshake.client.HandshakePacket; +import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerPositionRotationPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.world.ClientTeleportConfirmPacket; import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; @@ -64,6 +65,7 @@ import lombok.Setter; import org.geysermc.common.window.CustomFormWindow; import org.geysermc.common.window.FormWindow; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.Entity; @@ -94,6 +96,7 @@ import java.security.spec.InvalidKeySpecException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; @Getter public class GeyserSession implements CommandSender { @@ -245,10 +248,10 @@ public class GeyserSession implements CommandSender { private ScheduledFuture bucketScheduledFuture; /** - * Sends a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. + * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances. */ @Setter - private ScheduledFuture movementSendIfIdle; + private long lastMovementTimestamp = System.currentTimeMillis(); /** * Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless. @@ -328,6 +331,11 @@ public class GeyserSession implements CommandSender { private List selectedEmotes = new ArrayList<>(); private final Set emotes = new HashSet<>(); + /** + * The thread that will run every 50 milliseconds - one Minecraft tick. + */ + private ScheduledFuture tickThread = null; + private MinecraftProtocol protocol; public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { @@ -458,6 +466,9 @@ public class GeyserSession implements CommandSender { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); } + // Start ticking + tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); + downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); if (connector.getConfig().getRemote().isUseProxyProtocol()) { downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true); @@ -584,6 +595,8 @@ public class GeyserSession implements CommandSender { } } + tickThread.cancel(true); + this.chunkCache = null; this.entityCache = null; this.effectCache = null; @@ -598,6 +611,28 @@ public class GeyserSession implements CommandSender { disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.close", getClientData().getLanguageCode())); } + /** + * Called every 50 milliseconds - one Minecraft tick. + */ + public void tick() { + // Check to see if the player's position needs updating - a position update should be sent once every 3 seconds + if (spawned && (System.currentTimeMillis() - lastMovementTimestamp) > 3000) { + // Recalculate in case something else changed position + Vector3d position = collisionManager.adjustBedrockPosition(playerEntity.getPosition(), playerEntity.isOnGround()); + // A null return value cancels the packet + if (position != null) { + ClientPlayerPositionPacket packet = new ClientPlayerPositionPacket(playerEntity.isOnGround(), + position.getX(), position.getY(), position.getZ()); + sendDownstreamPacket(packet); + } + lastMovementTimestamp = System.currentTimeMillis(); + } + + for (Tickable entity : entityCache.getTickableEntities()) { + entity.tick(this); + } + } + public void setAuthenticationData(AuthData authData) { this.authData = authData; } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index 62b0dbd6b..40000551c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -28,6 +28,7 @@ package org.geysermc.connector.network.session.cache; import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; @@ -40,17 +41,21 @@ import java.util.concurrent.atomic.AtomicLong; * for that player (e.g. seeing vanished players from /vanish) */ public class EntityCache { - private GeyserSession session; + private final GeyserSession session; @Getter private Long2ObjectMap entities = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); + /** + * A list of all entities that must be ticked. + */ + private final List tickableEntities = Collections.synchronizedList(new ArrayList<>()); private Long2LongMap entityIdTranslations = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); private Map playerEntities = Collections.synchronizedMap(new HashMap<>()); private Map bossBars = Collections.synchronizedMap(new HashMap<>()); - private Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); + private final Long2LongMap cachedPlayerEntityLinks = Long2LongMaps.synchronize(new Long2LongOpenHashMap()); @Getter - private AtomicLong nextEntityId = new AtomicLong(2L); + private final AtomicLong nextEntityId = new AtomicLong(2L); public EntityCache(GeyserSession session) { this.session = session; @@ -59,6 +64,11 @@ public class EntityCache { public void spawnEntity(Entity entity) { if (cacheEntity(entity)) { entity.spawnEntity(session); + + if (entity instanceof Tickable) { + // Start ticking it + tickableEntities.add((Tickable) entity); + } } } @@ -76,6 +86,10 @@ public class EntityCache { if (entity != null && entity.isValid() && (force || entity.despawnEntity(session))) { long geyserId = entityIdTranslations.remove(entity.getEntityId()); entities.remove(geyserId); + + if (entity instanceof Tickable) { + tickableEntities.remove(entity); + } return true; } return false; @@ -152,4 +166,8 @@ public class EntityCache { public void addCachedPlayerEntityLink(long playerId, long linkedEntityId) { cachedPlayerEntityLinks.put(playerId, linkedEntityId); } + + public List getTickableEntities() { + return tickableEntities; + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java index 2d6fae3e1..5b4136a6b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -33,16 +33,12 @@ import com.nukkitx.math.vector.Vector3d; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.packet.MoveEntityAbsolutePacket; import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; -import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import org.geysermc.connector.common.ChatColor; 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.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.connector.network.translators.collision.CollisionManager; - -import java.util.concurrent.TimeUnit; @Translator(packet = MovePlayerPacket.class) public class BedrockMovePlayerTranslator extends PacketTranslator { @@ -63,9 +59,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator sendPositionIfIdle(session), - 3, TimeUnit.SECONDS)); } - public boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) { + private boolean isValidMove(GeyserSession session, MovePlayerPacket.Mode mode, Vector3f currentPosition, Vector3f newPosition) { if (mode != MovePlayerPacket.Mode.NORMAL) return true; @@ -171,81 +161,5 @@ public class BedrockMovePlayerTranslator extends PacketTranslator sendPositionIfIdle(session), - 3, TimeUnit.SECONDS)); - } } 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 22e5c95fd..203e4406f 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 @@ -30,8 +30,12 @@ import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; +import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket; +import com.nukkitx.protocol.bedrock.packet.SetEntityDataPacket; import lombok.Getter; import lombok.Setter; +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.translators.BlockCollision; @@ -105,6 +109,7 @@ public class CollisionManager { // According to the Minecraft Wiki, when sneaking: // - In Bedrock Edition, the height becomes 1.65 blocks, allowing movement through spaces as small as 1.75 (2 - 1⁄4) blocks high. // - In Java Edition, the height becomes 1.5 blocks. + // TODO: Have this depend on the player's literal bounding box variable if (session.isSneaking()) { playerBoundingBox.setSizeY(1.5); } else { @@ -113,6 +118,65 @@ public class CollisionManager { } } + /** + * Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between + * the two versions. + * + * @param bedrockPosition the current Bedrock position of the client + * @param onGround whether the Bedrock player is on the ground + * @return the position to send to the Java server, or null to cancel sending the packet + */ + public Vector3d adjustBedrockPosition(Vector3f bedrockPosition, boolean onGround) { + // We need to parse the float as a string since casting a float to a double causes us to + // lose precision and thus, causes players to get stuck when walking near walls + double javaY = bedrockPosition.getY() - EntityType.PLAYER.getOffset(); + + Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY, + Double.parseDouble(Float.toString(bedrockPosition.getZ()))); + + if (session.getConnector().getConfig().isCacheChunks()) { + // With chunk caching, we can do some proper collision checks + updatePlayerBoundingBox(position); + + // Correct player position + if (!correctPlayerPosition()) { + // Cancel the movement if it needs to be cancelled + recalculatePosition(); + return null; + } + + position = Vector3d.from(playerBoundingBox.getMiddleX(), + playerBoundingBox.getMiddleY() - (playerBoundingBox.getSizeY() / 2), + playerBoundingBox.getMiddleZ()); + } else { + // When chunk caching is off, we have to rely on this + // It rounds the Y position up to the nearest 0.5 + // This snaps players to snap to the top of stairs and slabs like on Java Edition + // However, it causes issues such as the player floating on carpets + if (onGround) javaY = Math.ceil(javaY * 2) / 2; + position = position.up(javaY - position.getY()); + } + + return position; + } + + // TODO: This makes the player look upwards for some reason, rotation values must be wrong + public void recalculatePosition() { + PlayerEntity entity = session.getPlayerEntity(); + // Gravity might need to be reset... + SetEntityDataPacket entityDataPacket = new SetEntityDataPacket(); + entityDataPacket.setRuntimeEntityId(entity.getGeyserId()); + entityDataPacket.getMetadata().putAll(entity.getMetadata()); + session.sendUpstreamPacket(entityDataPacket); + + MovePlayerPacket movePlayerPacket = new MovePlayerPacket(); + movePlayerPacket.setRuntimeEntityId(entity.getGeyserId()); + movePlayerPacket.setPosition(entity.getPosition()); + movePlayerPacket.setRotation(entity.getBedrockRotation()); + movePlayerPacket.setMode(MovePlayerPacket.Mode.NORMAL); + session.sendUpstreamPacket(movePlayerPacket); + } + public List getPlayerCollidableBlocks() { List blocks = new ArrayList<>(); diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index f330aed67..f193a61db 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -58,10 +58,6 @@ public class DimensionUtils { int bedrockDimension = javaToBedrock(javaDimension); Entity player = session.getPlayerEntity(); - if (session.getMovementSendIfIdle() != null) { - session.getMovementSendIfIdle().cancel(true); - } - session.getEntityCache().removeAllEntities(); session.getItemFrameCache().clear(); session.getSkullCache().clear(); From 92c86cf15b3f086d0c6468415571a357dd8c1170 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 5 Jan 2021 23:28:31 -0500 Subject: [PATCH 12/23] Null check tick thread (#1812) --- .../org/geysermc/connector/network/session/GeyserSession.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 5a0d5eeb1..1a51bf970 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 @@ -595,7 +595,9 @@ public class GeyserSession implements CommandSender { } } - tickThread.cancel(true); + if (tickThread != null) { + tickThread.cancel(true); + } this.chunkCache = null; this.entityCache = null; From bbbb7034f654ed48cbf0864033a6348d5b92f6ec Mon Sep 17 00:00:00 2001 From: rtm516 Date: Thu, 7 Jan 2021 12:21:35 +0000 Subject: [PATCH 13/23] Trigger builds of Geyser-Fabric and GeyserAndroid after a build success (#1815) * Trigger builds of Geyser-Fabric and GeyserAndroid after a build success * Enable GeyserAndroid and fix paths * Only build for master * Disable propagation of build results * Don't wait for sequential builds --- Jenkinsfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index e7f2ec4e2..d69e851da 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -69,5 +69,13 @@ pipeline { discordSend description: "**Build:** [${currentBuild.id}](${env.BUILD_URL})\n**Status:** [${currentBuild.currentResult}](${env.BUILD_URL})\n${changes}\n\n[**Artifacts on Jenkins**](https://ci.opencollab.dev/job/GeyserMC/job/Geyser)", footer: 'Open Collaboration Jenkins', link: env.BUILD_URL, successful: currentBuild.resultIsBetterOrEqualTo('SUCCESS'), title: "${env.JOB_NAME} #${currentBuild.id}", webhookURL: DISCORD_WEBHOOK } } + success { + script { + if (env.BRANCH_NAME == 'master') { + build propagate: false, wait: false, job: 'GeyserMC/Geyser-Fabric/java-1.16' + build propagate: false, wait: false, job: 'GeyserMC/GeyserAndroid/master' + } + } + } } } From dc46905e503de11aa1a0fb951375b257ba8564be Mon Sep 17 00:00:00 2001 From: SupremeMortal <6178101+SupremeMortal@users.noreply.github.com> Date: Thu, 7 Jan 2021 19:51:40 +0000 Subject: [PATCH 14/23] Use Artifactory Jenkins plugin for deployment (#1818) * Use artifactory jenkins plugin * Bump version to 1.2.0-SNAPSHOT --- Jenkinsfile | 22 +++++++++++++++++++++- pom.xml | 13 ------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d69e851da..501491361 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,7 +26,27 @@ pipeline { } steps { - sh 'mvn javadoc:jar source:jar deploy -DskipTests' + rtMavenDeployer( + id: "maven-deployer", + serverId: "opencollab-artifactory", + releaseRepo: "maven-releases", + snapshotRepo: "maven-snapshots" + ) + rtMavenResolver( + id: "maven-resolver", + serverId: "opencollab-artifactory", + releaseRepo: "release", + snapshotRepo: "snapshot" + ) + rtMavenRun( + pom: 'pom.xml', + goals: 'javadoc:jar source:jar install -DskipTests', + deployerId: "maven-deployer", + resolverId: "maven-resolver" + ) + rtPublishBuildInfo( + serverId: "opencollab-artifactory" + ) } } } diff --git a/pom.xml b/pom.xml index 1b544f9ee..011b320f4 100644 --- a/pom.xml +++ b/pom.xml @@ -71,19 +71,6 @@ - - - releases - opencollab-releases - https://repo.opencollab.dev/maven-releases - - - snapshots - opencollab-snapshots - https://repo.opencollab.dev/maven-snapshots - - - org.projectlombok From 7cefb5713e0aaf366fde30389998baf02854b9b8 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 7 Jan 2021 16:50:57 -0500 Subject: [PATCH 15/23] Clarify that enabling Xbox achievements blocks all commands (#1810) --- connector/src/main/resources/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index ac9ec753d..42e004db1 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -132,8 +132,7 @@ above-bedrock-nether-building: false force-resource-packs: true # Allows Xbox achievements to be unlocked. -# This disables certain commands so the Bedrock client can't to "cheat" to get them. -# Commands such as /gamemode and /give will not work from Bedrock with this enabled +# THIS DISABLES ALL COMMANDS FROM SUCCESSFULLY RUNNING FOR BEDROCK IN-GAME, as otherwise Bedrock thinks you are cheating. xbox-achievements-enabled: false # bStats is a stat tracker that is entirely anonymous and tracks only basic information From fe23c790535d692415e329506d12b8f763fc4844 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 7 Jan 2021 19:40:34 -0500 Subject: [PATCH 16/23] Implement book editing (#1117) * Implement book editing Updates the PR created by @ForceUpdate1 for 1.16 support. Seems to work fine now that hand support is in MCProtocolLib. Co-authored-by: Camotoy <20743703+DoctorMacc@users.noreply.github.com> * Remove debug line * Simplify code Currently still borked for creative mode. * Fix books on creative * Bug fixes * Fix NPE? * Blind fixes * Send Book update before any player actions * Remove debug prints * Fix out of bounds for page replace and add * Fix editing desync and remove empty pages from the end * Send edit packet after signing * Refactor * Clean up and fix creative * Apply suggestions from code review Co-authored-by: rtm516 Co-authored-by: ForceUpdate1 Co-authored-by: Camotoy <20743703+DoctorMacc@users.noreply.github.com> Co-authored-by: David Choo Co-authored-by: rtm516 --- .../network/session/GeyserSession.java | 3 + .../network/session/cache/BookEditCache.java | 75 +++++++++++ .../bedrock/BedrockBookEditTranslator.java | 123 ++++++++++++++++++ ...BedrockInventoryTransactionTranslator.java | 3 + .../BedrockMobEquipmentTranslator.java | 3 + .../player/BedrockActionTranslator.java | 5 + .../player/BedrockMovePlayerTranslator.java | 3 + .../translators/item/ItemRegistry.java | 7 + .../translators/nbt/BookPagesTranslator.java | 3 +- 9 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java create mode 100644 connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java 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 1a51bf970..8a0362663 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 @@ -113,6 +113,7 @@ public class GeyserSession implements CommandSender { private final SessionPlayerEntity playerEntity; private PlayerInventory inventory; + private BookEditCache bookEditCache; private ChunkCache chunkCache; private EntityCache entityCache; private EntityEffectCache effectCache; @@ -342,6 +343,7 @@ public class GeyserSession implements CommandSender { this.connector = connector; this.upstream = new UpstreamSession(bedrockServerSession); + this.bookEditCache = new BookEditCache(this); this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); this.effectCache = new EntityEffectCache(); @@ -599,6 +601,7 @@ public class GeyserSession implements CommandSender { tickThread.cancel(true); } + this.bookEditCache = null; this.chunkCache = null; this.entityCache = null; this.effectCache = null; diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java new file mode 100644 index 000000000..f81a9fdf9 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/BookEditCache.java @@ -0,0 +1,75 @@ +/* + * 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.session.cache; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket; +import lombok.Setter; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.item.ItemRegistry; + +/** + * Manages updating the current writable book. + * + * Java sends book updates less frequently than Bedrock, and this can cause issues with servers that rate limit + * book packets. Because of this, we need to ensure packets are only send every second or so at maximum. + */ +public class BookEditCache { + private final GeyserSession session; + @Setter + private ClientEditBookPacket packet; + /** + * Stores the last time a book update packet was sent to the server. + */ + private long lastBookUpdate; + + public BookEditCache(GeyserSession session) { + this.session = session; + } + + /** + * Check to see if there is a book edit update to send, and if so, send it. + */ + public void checkForSend() { + if (packet == null) { + // No new packet has to be sent + return; + } + // Prevent kicks due to rate limiting - specifically on Spigot servers + if ((System.currentTimeMillis() - lastBookUpdate) < 1000) { + return; + } + // Don't send the update if the player isn't not holding a book, shouldn't happen if we catch all interactions + ItemStack itemStack = session.getInventory().getItemInHand(); + if (itemStack == null || itemStack.getId() != ItemRegistry.WRITABLE_BOOK.getJavaId()) { + packet = null; + return; + } + session.getDownstream().getSession().send(packet); + packet = null; + lastBookUpdate = System.currentTimeMillis(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java new file mode 100644 index 000000000..dd5d08a2c --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockBookEditTranslator.java @@ -0,0 +1,123 @@ +/* + * 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.translators.bedrock; + +import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientEditBookPacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.nukkitx.protocol.bedrock.packet.BookEditPacket; +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.inventory.InventoryTranslator; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +@Translator(packet = BookEditPacket.class) +public class BedrockBookEditTranslator extends PacketTranslator { + + @Override + public void translate(BookEditPacket packet, GeyserSession session) { + ItemStack itemStack = session.getInventory().getItemInHand(); + if (itemStack != null) { + CompoundTag tag = itemStack.getNbt() != null ? itemStack.getNbt() : new CompoundTag(""); + ItemStack bookItem = new ItemStack(itemStack.getId(), itemStack.getAmount(), tag); + List pages = tag.contains("pages") ? new LinkedList<>(((ListTag) tag.get("pages")).getValue()) : new LinkedList<>(); + + int page = packet.getPageNumber(); + // Creative edits the NBT for us + if (session.getGameMode() != GameMode.CREATIVE) { + switch (packet.getAction()) { + case ADD_PAGE: { + // Add empty pages in between + for (int i = pages.size(); i < page; i++) { + pages.add(i, new StringTag("", "")); + } + pages.add(page, new StringTag("", packet.getText())); + break; + } + // Called whenever a page is modified + case REPLACE_PAGE: { + if (page < pages.size()) { + pages.set(page, new StringTag("", packet.getText())); + } else { + // Add empty pages in between + for (int i = pages.size(); i < page; i++) { + pages.add(i, new StringTag("", "")); + } + pages.add(page, new StringTag("", packet.getText())); + } + break; + } + case DELETE_PAGE: { + if (page < pages.size()) { + pages.remove(page); + } + break; + } + case SWAP_PAGES: { + int page2 = packet.getSecondaryPageNumber(); + if (page < pages.size() && page2 < pages.size()) { + Collections.swap(pages, page, page2); + } + break; + } + case SIGN_BOOK: { + tag.put(new StringTag("author", packet.getAuthor())); + tag.put(new StringTag("title", packet.getTitle())); + break; + } + default: + return; + } + } + // Remove empty pages at the end + while (pages.size() > 0) { + StringTag currentPage = (StringTag) pages.get(pages.size() - 1); + if (currentPage.getValue() == null || currentPage.getValue().isEmpty()) { + pages.remove(pages.size() - 1); + } else { + break; + } + } + tag.put(new ListTag("pages", pages)); + session.getInventory().setItem(36 + session.getInventory().getHeldItemSlot(), bookItem); + InventoryTranslator.INVENTORY_TRANSLATORS.get(null).updateInventory(session, session.getInventory()); + + session.getBookEditCache().setPacket(new ClientEditBookPacket(bookItem, packet.getAction() == BookEditPacket.Action.SIGN_BOOK, session.getInventory().getHeldItemSlot())); + // There won't be any more book updates after this, so we can try sending the edit packet immediately + if (packet.getAction() == BookEditPacket.Action.SIGN_BOOK) { + session.getBookEditCache().checkForSend(); + } + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index b756451bf..228b2812f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -69,6 +69,9 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator Date: Thu, 7 Jan 2021 22:43:36 -0500 Subject: [PATCH 17/23] Block breaking refactors (#1336) Client-side block animations and reach checks are now added. This commit also includes cleanup in BlockChangeTranslator as well as proper Netherite tool support for calculating block breaking. Co-authored-by: rtm516 --- ...BedrockInventoryTransactionTranslator.java | 120 +++++++++++++++--- .../player/BedrockActionTranslator.java | 50 +++++++- .../entity/JavaEntityStatusTranslator.java | 15 ++- .../player/JavaPlayerActionAckTranslator.java | 99 ++++++++------- .../java/world/JavaBlockChangeTranslator.java | 11 +- .../world/block/BlockTranslator.java | 4 +- .../geysermc/connector/utils/BlockUtils.java | 3 + 7 files changed, 224 insertions(+), 78 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index 228b2812f..8263507b2 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -39,12 +39,11 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; +import com.nukkitx.protocol.bedrock.data.entity.EntityFlags; import com.nukkitx.protocol.bedrock.data.inventory.ContainerId; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; -import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; -import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; -import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; -import com.nukkitx.protocol.bedrock.packet.LevelEventPacket; +import com.nukkitx.protocol.bedrock.packet.*; import org.geysermc.connector.entity.CommandBlockMinecartEntity; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.ItemFrameEntity; @@ -64,9 +63,18 @@ import org.geysermc.connector.utils.InventoryUtils; import java.util.concurrent.TimeUnit; +/** + * BedrockInventoryTransactionTranslator handles most interactions between the client and the world, + * or the client and their inventory. + */ @Translator(packet = InventoryTransactionPacket.class) public class BedrockInventoryTransactionTranslator extends PacketTranslator { + private static final float MAXIMUM_BLOCK_PLACING_DISTANCE = 64f; + private static final int CREATIVE_EYE_HEIGHT_PLACE_DISTANCE = 49; + private static final int SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE = 36; + private static final float MAXIMUM_BLOCK_DESTROYING_DISTANCE = 36f; + @Override public void translate(InventoryTransactionPacket packet, GeyserSession session) { // Send book updates before opening inventories @@ -112,6 +120,46 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator + (session.getGameMode().equals(GameMode.CREATIVE) ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) { + restoreCorrectBlock(session, blockPos, packet); + return; + } + + // Vanilla check + if (!(session.getPlayerEntity().getPosition().sub(0, EntityType.PLAYER.getOffset(), 0) + .distanceSquared(packet.getBlockPosition().toFloat().add(0.5f, 0.5f, 0.5f)) < MAXIMUM_BLOCK_PLACING_DISTANCE)) { + // The client thinks that its blocks have been successfully placed. Restore the server's blocks instead. + restoreCorrectBlock(session, blockPos, packet); + return; + } + /* + Block place checks end - client is good to go + */ + ClientPlayerPlaceBlockPacket blockPacket = new ClientPlayerPlaceBlockPacket( new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()), BlockFace.values()[packet.getBlockFace()], @@ -159,7 +207,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator MAXIMUM_BLOCK_DESTROYING_DISTANCE) { + restoreCorrectBlock(session, packet.getBlockPosition(), packet); + return; } + LevelEventPacket blockBreakPacket = new LevelEventPacket(); + blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK); + blockBreakPacket.setPosition(packet.getBlockPosition().toFloat()); + blockBreakPacket.setData(BlockTranslator.getBedrockBlockId(blockState)); + session.sendUpstreamPacket(blockBreakPacket); + session.setBreakingBlock(BlockTranslator.JAVA_AIR_ID); + long frameEntityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()); if (frameEntityId != -1 && session.getEntityCache().getEntityByJavaId(frameEntityId) != null) { ClientPlayerInteractEntityPacket attackPacket = new ClientPlayerInteractEntityPacket((int) frameEntityId, InteractAction.ATTACK, session.isSneaking()); @@ -270,4 +330,34 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator { @Override public void translate(ServerBlockChangePacket packet, GeyserSession session) { Position pos = packet.getRecord().getPosition(); - boolean updatePlacement = !(session.getConnector().getConfig().isCacheChunks() && session.getConnector().getWorldManager().getBlockAt(session, pos.getX(), pos.getY(), pos.getZ()) == packet.getRecord().getBlock()); - ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), packet.getRecord().getPosition()); - if (updatePlacement && session.getConnector().getPlatformType() != PlatformType.SPIGOT) { + boolean updatePlacement = session.getConnector().getPlatformType() != PlatformType.SPIGOT && // Spigot simply listens for the block place event + !(session.getConnector().getConfig().isCacheChunks() && + session.getConnector().getWorldManager().getBlockAt(session, pos) == packet.getRecord().getBlock()); + ChunkUtils.updateBlock(session, packet.getRecord().getBlock(), pos); + if (updatePlacement) { this.checkPlace(session, packet); } this.checkInteract(session, packet); 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 db6f43fea..b047999e7 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 @@ -87,7 +87,9 @@ public class BlockTranslator { */ public static final int BEDROCK_RUNTIME_COMMAND_BLOCK_ID; - // For block breaking animation math + /** + * A list of all Java runtime wool IDs, for use with block breaking math and shears + */ public static final IntSet JAVA_RUNTIME_WOOL_IDS = new IntOpenHashSet(); public static final int JAVA_RUNTIME_COBWEB_ID; diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java index 93909b20f..c859b9f65 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java @@ -50,6 +50,7 @@ public class BlockUtils { if (toolType.equals("shears")) return isWoolBlock ? 5.0 : 15.0; if (toolType.equals("")) return 1.0; switch (toolTier) { + // https://minecraft.gamepedia.com/Breaking#Speed case "wooden": return 2.0; case "stone": @@ -58,6 +59,8 @@ public class BlockUtils { return 6.0; case "diamond": return 8.0; + case "netherite": + return 9.0; case "golden": return 12.0; default: From 9232c5565fb1d9ed6ebbb4cfddd0ed7562fdc7ba Mon Sep 17 00:00:00 2001 From: David Choo Date: Sat, 9 Jan 2021 20:43:03 -0500 Subject: [PATCH 18/23] Add Ender Dragon effects and sounds (#1781) * Add Ender Dragon effects and sounds * Add proper death effect and clean up * Add Ender Dragon respawn sound * Possibly fix dragon breath direction? * Update mappings * Fix death animation triggering at low health * Trigger death event when health is 0 and add explosions back * Add comment --- .../living/monster/EnderDragonEntity.java | 217 +++++++++++++++--- .../living/monster/EnderDragonPartEntity.java | 5 +- connector/src/main/resources/mappings | 2 +- 3 files changed, 183 insertions(+), 41 deletions(-) diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java index df9456826..621679798 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonEntity.java @@ -28,15 +28,26 @@ package org.geysermc.connector.entity.living.monster; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.protocol.bedrock.data.AttributeData; +import com.nukkitx.protocol.bedrock.data.LevelEventType; +import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityEventType; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; -import com.nukkitx.protocol.bedrock.packet.AddEntityPacket; -import com.nukkitx.protocol.bedrock.packet.EntityEventPacket; +import com.nukkitx.protocol.bedrock.packet.*; import lombok.Data; import org.geysermc.connector.entity.Tickable; +import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.entity.living.InsentientEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.utils.AttributeUtils; +import org.geysermc.connector.utils.DimensionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; public class EnderDragonEntity extends InsentientEntity implements Tickable { /** @@ -59,7 +70,19 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable { private final Segment[] segmentHistory = new Segment[19]; private int latestSegment = -1; - private boolean hovering; + private int phase; + /** + * The number of ticks since the beginning of the phase + */ + private int phaseTicks; + + private int ticksTillNextGrowl = 100; + + /** + * Used to determine when the wing flap sound should be played + */ + private float wingPosition; + private float lastWingPosition; public EnderDragonEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) { super(entityId, geyserId, entityType, position, motion, rotation); @@ -69,49 +92,67 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable { @Override public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { - // Phase - if (entityMetadata.getId() == 15) { - int value = (int) entityMetadata.getValue(); - if (value == 5) { - // Performing breath attack + if (entityMetadata.getId() == 15) { // Phase + phase = (int) entityMetadata.getValue(); + phaseTicks = 0; + metadata.getFlags().setFlag(EntityFlag.SITTING, isSitting()); + } + + super.updateBedrockMetadata(entityMetadata, session); + + if (entityMetadata.getId() == 8) { // Health + // Update the health attribute, so that the death animation gets played + // Round health up, so that Bedrock doesn't consider the dragon to be dead when health is between 0 and 1 + float health = (float) Math.ceil(metadata.getFloat(EntityData.HEALTH)); + if (phase == 9 && health <= 0) { // Dying phase EntityEventPacket entityEventPacket = new EntityEventPacket(); - entityEventPacket.setType(EntityEventType.DRAGON_FLAMING); + entityEventPacket.setType(EntityEventType.ENDER_DRAGON_DEATH); entityEventPacket.setRuntimeEntityId(geyserId); entityEventPacket.setData(0); session.sendUpstreamPacket(entityEventPacket); } - metadata.getFlags().setFlag(EntityFlag.SITTING, value == 5 || value == 6 || value == 7); - hovering = value == 10; + attributes.put(AttributeType.HEALTH, AttributeType.HEALTH.getAttribute(health, 200)); + updateBedrockAttributes(session); } - super.updateBedrockMetadata(entityMetadata, session); + } + + /** + * Send an updated list of attributes to the Bedrock client. + * This is overwritten to allow the health attribute to differ from + * the health specified in the metadata. + * + * @param session GeyserSession + */ + @Override + public void updateBedrockAttributes(GeyserSession session) { + if (!valid) return; + + List attributes = new ArrayList<>(); + for (Map.Entry entry : this.attributes.entrySet()) { + if (!entry.getValue().getType().isBedrockAttribute()) + continue; + attributes.add(AttributeUtils.getBedrockAttribute(entry.getValue())); + } + + UpdateAttributesPacket updateAttributesPacket = new UpdateAttributesPacket(); + updateAttributesPacket.setRuntimeEntityId(geyserId); + updateAttributesPacket.setAttributes(attributes); + session.sendUpstreamPacket(updateAttributesPacket); } @Override public void spawnEntity(GeyserSession session) { - AddEntityPacket addEntityPacket = new AddEntityPacket(); - addEntityPacket.setIdentifier("minecraft:" + entityType.name().toLowerCase()); - addEntityPacket.setRuntimeEntityId(geyserId); - addEntityPacket.setUniqueEntityId(geyserId); - addEntityPacket.setPosition(position); - addEntityPacket.setMotion(motion); - addEntityPacket.setRotation(getBedrockRotation()); - addEntityPacket.setEntityType(entityType.getType()); - addEntityPacket.getMetadata().putAll(metadata); + super.spawnEntity(session); - // Otherwise dragon is always 'dying' - addEntityPacket.getAttributes().add(new AttributeData("minecraft:health", 0.0f, 200f, 200f, 200f)); - - valid = true; - session.sendUpstreamPacket(addEntityPacket); - - head = new EnderDragonPartEntity(entityId + 1, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 1, 1); - neck = new EnderDragonPartEntity(entityId + 2, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 3, 3); - body = new EnderDragonPartEntity(entityId + 3, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 5, 3); - leftWing = new EnderDragonPartEntity(entityId + 4, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2); - rightWing = new EnderDragonPartEntity(entityId + 5, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 4, 2); + AtomicLong nextEntityId = session.getEntityCache().getNextEntityId(); + head = new EnderDragonPartEntity(entityId + 1, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 1, 1); + neck = new EnderDragonPartEntity(entityId + 2, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 3, 3); + body = new EnderDragonPartEntity(entityId + 3, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 5, 3); + leftWing = new EnderDragonPartEntity(entityId + 4, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2); + rightWing = new EnderDragonPartEntity(entityId + 5, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 4, 2); tail = new EnderDragonPartEntity[3]; for (int i = 0; i < 3; i++) { - tail[i] = new EnderDragonPartEntity(entityId + 6 + i, session.getEntityCache().getNextEntityId().incrementAndGet(), EntityType.ENDER_DRAGON_PART, position, motion, rotation, 2, 2); + tail[i] = new EnderDragonPartEntity(entityId + 6 + i, nextEntityId.incrementAndGet(), EntityType.ENDER_DRAGON_PART, 2, 2); } allParts = new EnderDragonPartEntity[]{head, neck, body, leftWing, rightWing, tail[0], tail[1], tail[2]}; @@ -125,8 +166,6 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable { segmentHistory[i].yaw = rotation.getZ(); segmentHistory[i].y = position.getY(); } - - session.getConnector().getLogger().debug("Spawned entity " + entityType + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); } @Override @@ -139,8 +178,11 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable { @Override public void tick(GeyserSession session) { - pushSegment(); - updateBoundingBoxes(session); + effectTick(session); + if (!metadata.getFlags().getFlag(EntityFlag.NO_AI) && isAlive()) { + pushSegment(); + updateBoundingBoxes(session); + } } /** @@ -158,7 +200,7 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable { // Lowers the head when the dragon sits/hovers float headDuck; - if (hovering || metadata.getFlags().getFlag(EntityFlag.SITTING)) { + if (isHovering() || isSitting()) { headDuck = -1f; } else { headDuck = baseSegment.y - getSegment(0).y; @@ -188,6 +230,105 @@ public class EnderDragonEntity extends InsentientEntity implements Tickable { } } + /** + * Handles the particles and sounds of the Ender Dragon + * @param session GeyserSession. + */ + private void effectTick(GeyserSession session) { + Random random = ThreadLocalRandom.current(); + if (!metadata.getFlags().getFlag(EntityFlag.SILENT)) { + if (Math.cos(wingPosition * 2f * Math.PI) <= -0.3f && Math.cos(lastWingPosition * 2f * Math.PI) >= -0.3f) { + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("mob.enderdragon.flap"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(5f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + + if (!isSitting() && !isHovering() && ticksTillNextGrowl-- == 0) { + playGrowlSound(session); + ticksTillNextGrowl = 200 + random.nextInt(200); + } + + lastWingPosition = wingPosition; + } + if (isAlive()) { + if (metadata.getFlags().getFlag(EntityFlag.NO_AI)) { + wingPosition = 0.5f; + } else if (isHovering() || isSitting()) { + wingPosition += 0.1f; + } else { + double speed = motion.length(); + wingPosition += 0.2f / (speed * 10f + 1) * Math.pow(2, motion.getY()); + } + + phaseTicks++; + if (phase == 3) { // Landing Phase + float headHeight = head.getMetadata().getFloat(EntityData.BOUNDING_BOX_HEIGHT); + Vector3f headCenter = head.getPosition().up(headHeight * 0.5f); + + for (int i = 0; i < 8; i++) { + Vector3f particlePos = headCenter.add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f); + // This is missing velocity information + LevelEventPacket particlePacket = new LevelEventPacket(); + particlePacket.setType(LevelEventType.PARTICLE_DRAGONS_BREATH); + particlePacket.setPosition(particlePos); + session.sendUpstreamPacket(particlePacket); + } + } else if (phase == 5) { // Sitting Flaming Phase + if (phaseTicks % 2 == 0 && phaseTicks < 10) { + // Performing breath attack + // Entity event DRAGON_FLAMING seems to create particles from the origin of the dragon, + // so we need to manually spawn particles + for (int i = 0; i < 8; i++) { + SpawnParticleEffectPacket spawnParticleEffectPacket = new SpawnParticleEffectPacket(); + spawnParticleEffectPacket.setDimensionId(DimensionUtils.javaToBedrock(session.getDimension())); + spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f)); + spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire"); + session.sendUpstreamPacket(spawnParticleEffectPacket); + } + } + } else if (phase == 7) { // Sitting Attacking Phase + playGrowlSound(session); + } else if (phase == 9) { // Dying Phase + // Send explosion particles as the dragon move towards the end portal + if (phaseTicks % 10 == 0) { + float xOffset = 8f * (random.nextFloat() - 0.5f); + float yOffset = 4f * (random.nextFloat() - 0.5f) + 2f; + float zOffset = 8f * (random.nextFloat() - 0.5f); + Vector3f particlePos = position.add(xOffset, yOffset, zOffset); + LevelEventPacket particlePacket = new LevelEventPacket(); + particlePacket.setType(LevelEventType.PARTICLE_EXPLOSION); + particlePacket.setPosition(particlePos); + session.sendUpstreamPacket(particlePacket); + } + } + } + } + + private void playGrowlSound(GeyserSession session) { + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("mob.enderdragon.growl"); + playSoundPacket.setPosition(position); + playSoundPacket.setVolume(2.5f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + + private boolean isAlive() { + return metadata.getFloat(EntityData.HEALTH) > 0; + } + + private boolean isHovering() { + return phase == 10; + } + + private boolean isSitting() { + return phase == 5 || phase == 6 || phase == 7; + } + /** * Store the current yaw and y into the circular buffer */ diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java index 095d12b2c..288a3e423 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/living/monster/EnderDragonPartEntity.java @@ -32,11 +32,12 @@ import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.type.EntityType; public class EnderDragonPartEntity extends Entity { - public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation, float width, float height) { - super(entityId, geyserId, entityType, position, motion, rotation); + public EnderDragonPartEntity(long entityId, long geyserId, EntityType entityType, float width, float height) { + super(entityId, geyserId, entityType, Vector3f.ZERO, Vector3f.ZERO, Vector3f.ZERO); metadata.put(EntityData.BOUNDING_BOX_WIDTH, width); metadata.put(EntityData.BOUNDING_BOX_HEIGHT, height); metadata.getFlags().setFlag(EntityFlag.INVISIBLE, true); + metadata.getFlags().setFlag(EntityFlag.FIRE_IMMUNE, true); } } diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index 143285afb..dd0347bd5 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 143285afb4bdf4d5ef40ef7a7959477dabf4d34c +Subproject commit dd0347bd51e00e42ea58faaf68b562526c4d2817 From d6461e71fb1e6e804b3e674f7e6f741810ad3864 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 10 Jan 2021 20:00:48 -0500 Subject: [PATCH 19/23] Add message if Geyser thinks the English locale is missing (#1825) --- .../java/org/geysermc/connector/utils/LanguageUtils.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 c6ab715d4..1a1f758d6 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -187,7 +187,11 @@ public class LanguageUtils { if (FileUtils.class.getResource("/languages/texts/" + locale + ".properties") == null) { result = false; if (GeyserConnector.getInstance() != null && GeyserConnector.getInstance().getLogger() != null) { // Could be too early for these to be initialized - GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language + if (locale.equals("en_US")) { + GeyserConnector.getInstance().getLogger().error("English locale not found in Geyser. Did you clone the submodules? (git submodule update --init)"); + } else { + GeyserConnector.getInstance().getLogger().warning(locale + " is not a valid Bedrock language."); // We can't translate this since we just loaded an invalid language + } } } else { if (!LOCALE_MAPPINGS.containsKey(locale)) { From 999fa7298a0e67bbe4b32297f06246485f025191 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 10 Jan 2021 20:01:04 -0500 Subject: [PATCH 20/23] Clarify proxy protocol config message (#1824) --- connector/src/main/resources/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 42e004db1..228852785 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -35,7 +35,8 @@ remote: # Whether to enable PROXY protocol or not while connecting to the server. # This is useful only when: # 1) Your server supports PROXY protocol (it probably doesn't) - # 2) You run Velocity or BungeeCord with respective option enabled. + # 2) You run Velocity or BungeeCord with the option enabled in the proxy's main config. + # IF YOU DON'T KNOW WHAT THIS IS, DON'T TOUCH IT! use-proxy-protocol: false # Floodgate uses encryption to ensure use from authorised sources. From 2cc8726c1256ad33d6f17569ba067e49314380e3 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 11 Jan 2021 14:11:21 -0500 Subject: [PATCH 21/23] Shade in the entire net.kyori package (#1826) --- bootstrap/bungeecord/pom.xml | 4 ++-- bootstrap/spigot/pom.xml | 4 ++-- bootstrap/sponge/pom.xml | 4 ++-- bootstrap/velocity/pom.xml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 124967b0a..54e0d56ef 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -86,8 +86,8 @@ org.geysermc.platform.bungeecord.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.bungeecord.shaded.adventure + net.kyori + org.geysermc.platform.bungeecord.shaded.kyori diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index adaa7557f..93eebc3d2 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -97,8 +97,8 @@ org.geysermc.platform.spigot.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.spigot.shaded.adventure + net.kyori + org.geysermc.platform.spigot.shaded.kyori diff --git a/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index e6ce8f851..97c4ac8a4 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -86,8 +86,8 @@ org.geysermc.platform.sponge.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.sponge.shaded.adventure + net.kyori + org.geysermc.platform.sponge.shaded.kyori diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index 2fedca71a..5c0824def 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -82,8 +82,8 @@ org.geysermc.platform.velocity.shaded.dom4j - net.kyori.adventure - org.geysermc.platform.velocity.shaded.adventure + net.kyori + org.geysermc.platform.velocity.shaded.kyori From 6aa74a2322f0555007585191c2a085b19e3b1efb Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 11 Jan 2021 15:37:37 -0500 Subject: [PATCH 22/23] Allow server pong to appear if MOTDs are too long (#1445) * Prevent server pong from appearing if MOTDs are too long If the server MOTDs are 340 characters or longer, they will not appear. If this is the case, we trim each. * Implement a more exact fix --- .../network/ConnectorServerEventHandler.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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 79c04f674..87883087d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/ConnectorServerEventHandler.java @@ -30,15 +30,16 @@ import com.nukkitx.protocol.bedrock.BedrockServerEventHandler; import com.nukkitx.protocol.bedrock.BedrockServerSession; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.socket.DatagramPacket; -import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.common.ping.GeyserPingInfo; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.network.translators.chat.MessageTranslator; +import org.geysermc.connector.ping.IGeyserPingPassthrough; import org.geysermc.connector.utils.LanguageUtils; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; public class ConnectorServerEventHandler implements BedrockServerEventHandler { @@ -94,6 +95,20 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler { pong.setMaximumPlayerCount(config.getMaxPlayers()); } + // 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) { + // If the top MOTD is still too long, we chop it down + byte[] newMotdArray = new byte[339]; + System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); + pong.setMotd(new String(newMotdArray, StandardCharsets.UTF_8)); + } + } + //Bedrock will not even attempt a connection if the client thinks the server is full //so we have to fake it not being full if (pong.getPlayerCount() >= pong.getMaximumPlayerCount()) { From 1c0cc4622a04df9f3c3bdccac4c3a600cda0b94e Mon Sep 17 00:00:00 2001 From: rtm516 Date: Mon, 11 Jan 2021 20:52:02 +0000 Subject: [PATCH 23/23] Microsoft account authentication (#1808) Microsoft accounts can now use Geyser, while maintaining full backwards compatibility with Mojang accounts. Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- connector/pom.xml | 9 + .../geysermc/connector/GeyserConnector.java | 9 +- .../configuration/GeyserConfiguration.java | 8 + .../GeyserJacksonConfiguration.java | 7 + .../network/UpstreamPacketHandler.java | 1 + .../network/session/GeyserSession.java | 355 +++++++++++------- .../JavaPlayerPositionRotationTranslator.java | 2 +- .../java/world/JavaUpdateTimeTranslator.java | 11 +- .../connector/utils/LoginEncryptionUtils.java | 82 +++- connector/src/main/resources/config.yml | 5 + connector/src/main/resources/languages | 2 +- 11 files changed, 338 insertions(+), 153 deletions(-) diff --git a/connector/pom.xml b/connector/pom.xml index de095a33b..a7001be00 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -132,6 +132,10 @@ com.github.steveice10 packetlib + + com.github.steveice10 + mcauthlib + @@ -198,6 +202,11 @@ 4.13.1 test + + com.github.GeyserMC + MCAuthLib + 0e48a094f2 + diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 5dd00dabe..d61500e04 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -86,6 +86,11 @@ public class GeyserConnector { public static final String GIT_VERSION = "DEV"; // A fallback for running in IDEs public static final String VERSION = "DEV"; // A fallback for running in IDEs + /** + * Oauth client ID for Microsoft authentication + */ + public static final String OAUTH_CLIENT_ID = "204cefd1-4818-4de1-b98d-513fae875d88"; + private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"; private final List players = new ArrayList<>(); @@ -101,8 +106,8 @@ public class GeyserConnector { private final ScheduledExecutorService generalThreadPool; private BedrockServer bedrockServer; - private PlatformType platformType; - private GeyserBootstrap bootstrap; + private final PlatformType platformType; + private final GeyserBootstrap bootstrap; private Metrics metrics; 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 d21893f89..e21aa6bb8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserConfiguration.java @@ -118,6 +118,8 @@ public interface GeyserConfiguration { String getAuthType(); + boolean isPasswordAuthentication(); + boolean isUseProxyProtocol(); } @@ -125,6 +127,12 @@ public interface GeyserConfiguration { String getEmail(); String getPassword(); + + /** + * Will be removed after Microsoft accounts are fully migrated + */ + @Deprecated + boolean isMicrosoftAccount(); } interface IMetricsInfo { 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 3a8946e00..7c9532ff8 100644 --- a/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java +++ b/connector/src/main/java/org/geysermc/connector/configuration/GeyserJacksonConfiguration.java @@ -149,17 +149,24 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration @JsonProperty("auth-type") private String authType = "online"; + @JsonProperty("allow-password-authentication") + private boolean passwordAuthentication = true; + @JsonProperty("use-proxy-protocol") private boolean useProxyProtocol = false; } @Getter + @JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load public static class UserAuthenticationInfo implements IUserAuthenticationInfo { @AsteriskSerializer.Asterisk() private String email; @AsteriskSerializer.Asterisk() private String password; + + @JsonProperty("microsoft-account") + private boolean microsoftAccount = false; } @Getter diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index a6a369e45..3922a95ff 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -161,6 +161,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { if (info != null) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().getName())); + session.setMicrosoftAccount(info.isMicrosoftAccount()); session.authenticate(info.getEmail(), info.getPassword()); // TODO send a message to bedrock user telling them they are connected (if nothing like a motd 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 8a0362663..a82ea061e 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 @@ -26,8 +26,12 @@ package org.geysermc.connector.network.session; import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.mc.auth.exception.request.AuthPendingException; import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException; import com.github.steveice10.mc.auth.exception.request.RequestException; +import com.github.steveice10.mc.auth.service.AuthenticationService; +import com.github.steveice10.mc.auth.service.MojangAuthenticationService; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.github.steveice10.mc.protocol.MinecraftConstants; import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.data.SubProtocol; @@ -110,6 +114,10 @@ public class GeyserSession implements CommandSender { @Setter private BedrockClientData clientData; + @Deprecated + @Setter + private boolean microsoftAccount; + private final SessionPlayerEntity playerEntity; private PlayerInventory inventory; @@ -257,7 +265,6 @@ public class GeyserSession implements CommandSender { /** * Controls whether the daylight cycle gamerule has been sent to the client, so the sun/moon remain motionless. */ - @Setter private boolean daylightCycle = true; private boolean reducedDebugInfo = false; @@ -443,139 +450,22 @@ public class GeyserSession implements CommandSender { new Thread(() -> { try { if (password != null && !password.isEmpty()) { - protocol = new MinecraftProtocol(username, password); + AuthenticationService authenticationService; + if (microsoftAccount) { + authenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + } else { + authenticationService = new MojangAuthenticationService(); + } + authenticationService.setUsername(username); + authenticationService.setPassword(password); + authenticationService.login(); + + protocol = new MinecraftProtocol(authenticationService); } else { protocol = new MinecraftProtocol(username); } - boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; - final PublicKey publicKey; - - if (floodgate) { - PublicKey key = null; - try { - key = EncryptionUtil.getKeyFromFile( - connector.getConfig().getFloodgateKeyPath(), - PublicKey.class - ); - } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); - } - publicKey = key; - } else publicKey = null; - - if (publicKey != null) { - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); - } - - // Start ticking - tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); - - downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); - if (connector.getConfig().getRemote().isUseProxyProtocol()) { - 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); - downstream.getSession().addListener(new SessionAdapter() { - @Override - public void packetSending(PacketSendingEvent event) { - //todo move this somewhere else - if (event.getPacket() instanceof HandshakePacket && floodgate) { - String encrypted = ""; - try { - encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( - clientData.getGameVersion(), - authData.getName(), - authData.getXboxUUID(), - clientData.getDeviceOS().ordinal(), - clientData.getLanguageCode(), - clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() - )); - } catch (Exception e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); - } - - HandshakePacket handshakePacket = event.getPacket(); - event.setPacket(new HandshakePacket( - handshakePacket.getProtocolVersion(), - handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted, - handshakePacket.getPort(), - handshakePacket.getIntent() - )); - } - } - - @Override - public void connected(ConnectedEvent event) { - loggingIn = false; - loggedIn = true; - if (protocol.getProfile() == null) { - // Java account is offline - disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return; - } - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); - playerEntity.setUuid(protocol.getProfile().getId()); - playerEntity.setUsername(protocol.getProfile().getName()); - - String locale = clientData.getLanguageCode(); - - // Let the user know there locale may take some time to download - // as it has to be extracted from a JAR - if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { - // This should probably be left hardcoded as it will only show for en_us clients - sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); - } - - // Download and load the language for the player - LocaleUtils.downloadAndLoadLocale(locale); - } - - @Override - public void disconnected(DisconnectedEvent event) { - loggingIn = false; - loggedIn = false; - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); - if (event.getCause() != null) { - event.getCause().printStackTrace(); - } - - upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); - } - - @Override - public void packetReceived(PacketReceivedEvent event) { - if (!closed) { - // Required, or else Floodgate players break with Bukkit chunk caching - if (event.getPacket() instanceof LoginSuccessPacket) { - GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); - playerEntity.setUsername(profile.getName()); - playerEntity.setUuid(profile.getId()); - - // Check if they are not using a linked account - if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { - SkinManager.handleBedrockSkin(playerEntity, clientData); - } - } - - PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); - } - } - - @Override - public void packetError(PacketErrorEvent event) { - connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); - if (connector.getConfig().isDebugMode()) - event.getCause().printStackTrace(); - event.setSuppress(true); - } - }); - - downstream.getSession().connect(); - connector.addPlayer(this); + connectDownstream(); } catch (InvalidCredentialsException | IllegalArgumentException e) { connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", username)); disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); @@ -585,6 +475,199 @@ public class GeyserSession implements CommandSender { }).start(); } + /** + * Present a form window to the user asking to log in with another web browser + */ + public void authenticateWithMicrosoftCode() { + if (loggedIn) { + connector.getLogger().severe(LanguageUtils.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().getName())); + return; + } + + loggingIn = true; + // new thread so clients don't timeout + new Thread(() -> { + try { + MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); + + MsaAuthenticationService.MsCodeResponse response = msaAuthenticationService.getAuthCode(); + LoginEncryptionUtils.showMicrosoftCodeWindow(this, response); + + // This just looks cool + SetTimePacket packet = new SetTimePacket(); + packet.setTime(16000); + sendUpstreamPacket(packet); + + // Wait for the code to validate + attemptCodeAuthentication(msaAuthenticationService); + } catch (InvalidCredentialsException | IllegalArgumentException e) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.invalid", getAuthData().getName())); + disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode())); + } catch (RequestException ex) { + ex.printStackTrace(); + } + }).start(); + } + + /** + * Poll every second to see if the user has successfully signed in + */ + private void attemptCodeAuthentication(MsaAuthenticationService msaAuthenticationService) { + if (loggedIn || closed) { + return; + } + try { + msaAuthenticationService.login(); + protocol = new MinecraftProtocol(msaAuthenticationService); + + connectDownstream(); + } catch (RequestException e) { + if (!(e instanceof AuthPendingException)) { + e.printStackTrace(); + } else { + // Wait one second before trying again + connector.getGeneralThreadPool().schedule(() -> attemptCodeAuthentication(msaAuthenticationService), 1, TimeUnit.SECONDS); + } + } + } + + /** + * After getting whatever credentials needed, we attempt to join the Java server. + */ + private void connectDownstream() { + boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE; + final PublicKey publicKey; + + if (floodgate) { + PublicKey key = null; + try { + key = EncryptionUtil.getKeyFromFile( + connector.getConfig().getFloodgateKeyPath(), + PublicKey.class + ); + } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); + } + publicKey = key; + } else publicKey = null; + + if (publicKey != null) { + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); + } + + // Start ticking + tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); + + downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory()); + if (connector.getConfig().getRemote().isUseProxyProtocol()) { + 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); + downstream.getSession().addListener(new SessionAdapter() { + @Override + public void packetSending(PacketSendingEvent event) { + //todo move this somewhere else + if (event.getPacket() instanceof HandshakePacket && floodgate) { + String encrypted = ""; + try { + encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( + clientData.getGameVersion(), + authData.getName(), + authData.getXboxUUID(), + clientData.getDeviceOS().ordinal(), + clientData.getLanguageCode(), + clientData.getCurrentInputMode().ordinal(), + upstream.getSession().getAddress().getAddress().getHostAddress() + )); + } catch (Exception e) { + connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); + } + + HandshakePacket handshakePacket = event.getPacket(); + event.setPacket(new HandshakePacket( + handshakePacket.getProtocolVersion(), + handshakePacket.getHostname() + '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted, + handshakePacket.getPort(), + handshakePacket.getIntent() + )); + } + } + + @Override + public void connected(ConnectedEvent event) { + loggingIn = false; + loggedIn = true; + if (protocol.getProfile() == null) { + // Java account is offline + disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); + return; + } + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress())); + playerEntity.setUuid(protocol.getProfile().getId()); + playerEntity.setUsername(protocol.getProfile().getName()); + + String locale = clientData.getLanguageCode(); + + // Let the user know there locale may take some time to download + // as it has to be extracted from a JAR + if (locale.toLowerCase().equals("en_us") && !LocaleUtils.LOCALE_MAPPINGS.containsKey("en_us")) { + // This should probably be left hardcoded as it will only show for en_us clients + sendMessage("Loading your locale (en_us); if this isn't already downloaded, this may take some time"); + } + + // Download and load the language for the player + LocaleUtils.downloadAndLoadLocale(locale); + } + + @Override + public void disconnected(DisconnectedEvent event) { + loggingIn = false; + loggedIn = false; + connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason())); + if (event.getCause() != null) { + event.getCause().printStackTrace(); + } + + upstream.disconnect(MessageTranslator.convertMessageLenient(event.getReason())); + } + + @Override + public void packetReceived(PacketReceivedEvent event) { + if (!closed) { + // Required, or else Floodgate players break with Bukkit chunk caching + if (event.getPacket() instanceof LoginSuccessPacket) { + GameProfile profile = ((LoginSuccessPacket) event.getPacket()).getProfile(); + playerEntity.setUsername(profile.getName()); + playerEntity.setUuid(profile.getId()); + + // Check if they are not using a linked account + if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { + SkinManager.handleBedrockSkin(playerEntity, clientData); + } + } + + PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); + } + } + + @Override + public void packetError(PacketErrorEvent event) { + connector.getLogger().warning(LanguageUtils.getLocaleStringLog("geyser.network.downstream_error", event.getCause().getMessage())); + if (connector.getConfig().isDebugMode()) + event.getCause().printStackTrace(); + event.setSuppress(true); + } + }); + + if (!daylightCycle) { + setDaylightCycle(true); + } + downstream.getSession().connect(); + connector.addPlayer(this); + } + public void disconnect(String reason) { if (!closed) { loggedIn = false; @@ -872,6 +955,18 @@ public class GeyserSession implements CommandSender { reducedDebugInfo = value; } + /** + * Changes the daylight cycle gamerule on the client + * This is used in the login screen along-side normal usage + * + * @param doCycle If the cycle should continue + */ + public void setDaylightCycle(boolean doCycle) { + sendGameRule("dodaylightcycle", doCycle); + // Save the value so we don't have to constantly send a daylight cycle gamerule update + this.daylightCycle = doCycle; + } + /** * Send a gamerule value to the client * diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java index d5d8e7d3c..b379443e3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerPositionRotationTranslator.java @@ -81,7 +81,7 @@ public class JavaPlayerPositionRotationTranslator extends PacketTranslator= 0) { // Client thinks there is no daylight cycle but there is - setDoDaylightCycleGamerule(session, true); + session.setDaylightCycle(true); } else if (session.isDaylightCycle() && time < 0) { // Client thinks there is daylight cycle but there isn't - setDoDaylightCycleGamerule(session, false); + session.setDaylightCycle(false); } } - - private void setDoDaylightCycleGamerule(GeyserSession session, boolean doCycle) { - session.sendGameRule("dodaylightcycle", doCycle); - // Save the value so we don't have to constantly send a daylight cycle gamerule update - session.setDaylightCycle(doCycle); - } - } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 5c212ba02..fd7ef4e64 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -29,19 +29,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.nimbusds.jose.JWSObject; import com.nukkitx.network.util.Preconditions; import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; -import org.geysermc.common.window.CustomFormBuilder; -import org.geysermc.common.window.CustomFormWindow; -import org.geysermc.common.window.FormWindow; -import org.geysermc.common.window.SimpleFormWindow; +import org.geysermc.common.window.*; import org.geysermc.common.window.button.FormButton; import org.geysermc.common.window.component.InputComponent; import org.geysermc.common.window.component.LabelComponent; import org.geysermc.common.window.response.CustomFormResponse; +import org.geysermc.common.window.response.ModalFormResponse; import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; @@ -156,13 +155,21 @@ public class LoginEncryptionUtils { session.sendUpstreamPacketImmediately(packet); } - private static int AUTH_FORM_ID = 1336; - private static int AUTH_DETAILS_FORM_ID = 1337; + private static final int AUTH_MSA_DETAILS_FORM_ID = 1334; + private static final int AUTH_MSA_CODE_FORM_ID = 1335; + private static final int AUTH_FORM_ID = 1336; + private static final int AUTH_DETAILS_FORM_ID = 1337; public static void showLoginWindow(GeyserSession session) { + // Set DoDaylightCycle to false so the time doesn't accelerate while we're here + session.setDaylightCycle(false); + String userLanguage = session.getLocale(); SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.title", userLanguage), LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.desc", userLanguage)); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login", userLanguage))); + if (session.getConnector().getConfig().getRemote().isPasswordAuthentication()) { + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.mojang", userLanguage))); + } + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage))); window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); session.sendForm(window, AUTH_FORM_ID); @@ -179,12 +186,33 @@ public class LoginEncryptionUtils { session.sendForm(window, AUTH_DETAILS_FORM_ID); } + /** + * Prompts the user between either OAuth code login or manual password authentication + */ + public static void showMicrosoftAuthenticationWindow(GeyserSession session) { + String userLanguage = session.getLocale(); + SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage), ""); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.browser", userLanguage))); + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.password", userLanguage))); // This form won't show if password authentication is disabled + window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); + session.sendForm(window, AUTH_MSA_DETAILS_FORM_ID); + } + + /** + * Shows the code that a user must input into their browser + */ + public static void showMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse response) { + ModalFormWindow msaCodeWindow = new ModalFormWindow("%xbox.signin", "%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + + response.user_code, "Done", "%menu.disconnect"); + session.sendForm(msaCodeWindow, LoginEncryptionUtils.AUTH_MSA_CODE_FORM_ID); + } + public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) { WindowCache windowCache = session.getWindowCache(); if (!windowCache.getWindows().containsKey(formId)) return false; - if(formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID) { + if (formId == AUTH_MSA_DETAILS_FORM_ID || formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID || formId == AUTH_MSA_CODE_FORM_ID) { FormWindow window = windowCache.getWindows().remove(formId); window.setResponse(formData.trim()); @@ -205,16 +233,50 @@ public class LoginEncryptionUtils { showLoginDetailsWindow(session); } } else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) { + boolean isPasswordAuthentication = session.getConnector().getConfig().getRemote().isPasswordAuthentication(); + int microsoftButton = isPasswordAuthentication ? 1 : 0; + int disconnectButton = isPasswordAuthentication ? 2 : 1; SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); if (response != null) { - if (response.getClickedButtonId() == 0) { + if (isPasswordAuthentication && response.getClickedButtonId() == 0) { + session.setMicrosoftAccount(false); showLoginDetailsWindow(session); - } else if(response.getClickedButtonId() == 1) { + } else if (response.getClickedButtonId() == microsoftButton) { + session.setMicrosoftAccount(true); + if (isPasswordAuthentication) { + showMicrosoftAuthenticationWindow(session); + } else { + // Just show the OAuth code + session.authenticateWithMicrosoftCode(); + } + } else if (response.getClickedButtonId() == disconnectButton) { session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); } } else { showLoginWindow(session); } + } else if (formId == AUTH_MSA_DETAILS_FORM_ID && window instanceof SimpleFormWindow) { + SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); + if (response != null) { + if (response.getClickedButtonId() == 0) { + session.authenticateWithMicrosoftCode(); + } else if (response.getClickedButtonId() == 1) { + showLoginDetailsWindow(session); + } else if (response.getClickedButtonId() == 2) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + } else { + showLoginWindow(session); + } + } else if (formId == AUTH_MSA_CODE_FORM_ID && window instanceof ModalFormWindow) { + ModalFormResponse response = (ModalFormResponse) window.getResponse(); + if (response != null) { + if (response.getClickedButtonId() == 1) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + } else { + showMicrosoftAuthenticationWindow(session); + } } } } diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 228852785..07b73173e 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -32,6 +32,9 @@ remote: port: 25565 # Authentication type. Can be offline, online, or floodgate (see https://github.com/GeyserMC/Geyser/wiki/Floodgate). auth-type: online + # Allow for password-based authentication methods through Geyser. Only useful in online mode. + # If this is false, users must authenticate to Microsoft using a code provided by Geyser on their desktop. + allow-password-authentication: true # Whether to enable PROXY protocol or not while connecting to the server. # This is useful only when: # 1) Your server supports PROXY protocol (it probably doesn't) @@ -52,10 +55,12 @@ floodgate-key-file: public-key.pem # BedrockAccountUsername: # Your Minecraft: Bedrock Edition username # email: javaccountemail@example.com # Your Minecraft: Java Edition email # password: javaccountpassword123 # Your Minecraft: Java Edition password +# microsoft-account: true # Whether the account is a Mojang or Microsoft account. # # bluerkelp2: # email: not_really_my_email_address_mr_minecrafter53267@gmail.com # password: "this isn't really my password" +# microsoft-account: false # Bedrock clients can freeze when opening up the command prompt for the first time if given a lot of commands. # Disabling this will prevent command suggestions from being sent and solve freezing for Bedrock clients. diff --git a/connector/src/main/resources/languages b/connector/src/main/resources/languages index 1a0076684..6f246c24d 160000 --- a/connector/src/main/resources/languages +++ b/connector/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 1a00766840baf1f512d98f5a75c177c8bcfba6f3 +Subproject commit 6f246c24ddbd543a359d651e706da470fe53ceeb