From 835ba9e24f5904ab5ee84f574a94e6a5a637a3df Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:25:12 -0400 Subject: [PATCH 01/84] Remove casting from block mappings --- .../java/org/geysermc/geyser/registry/type/BlockMappings.java | 4 ++-- .../main/java/org/geysermc/geyser/session/GeyserSession.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java index 5c4e835e4..be96fec0e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/BlockMappings.java @@ -44,7 +44,7 @@ import java.util.Set; @Builder @Value -public class BlockMappings implements DefinitionRegistry { +public class BlockMappings implements DefinitionRegistry { GeyserBedrockBlock bedrockAir; BlockDefinition bedrockWater; BlockDefinition bedrockMovingBlock; @@ -134,7 +134,7 @@ public class BlockMappings implements DefinitionRegistry { } @Override - public boolean isRegistered(GeyserBedrockBlock bedrockBlock) { + public boolean isRegistered(BlockDefinition bedrockBlock) { return getDefinition(bedrockBlock.getRuntimeId()) == bedrockBlock; } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 7f015a05e..25dd21662 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -57,7 +57,6 @@ import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.*; -import org.cloudburstmc.protocol.common.DefinitionRegistry; import org.cloudburstmc.protocol.common.util.OptionalBoolean; import org.geysermc.api.util.BedrockPlatform; import org.geysermc.api.util.InputMode; @@ -1466,7 +1465,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private void startGame() { this.upstream.getCodecHelper().setItemDefinitions(this.itemMappings); - this.upstream.getCodecHelper().setBlockDefinitions((DefinitionRegistry) this.blockMappings); //FIXME + this.upstream.getCodecHelper().setBlockDefinitions(this.blockMappings); this.upstream.getCodecHelper().setCameraPresetDefinitions(CameraDefinitions.CAMERA_DEFINITIONS); StartGamePacket startGamePacket = new StartGamePacket(); From 130b27203f8b7214c986f13b103e9398f2f7b9ad Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Fri, 28 Jun 2024 22:55:36 -0400 Subject: [PATCH 02/84] Limit particle amount in LevelParticlesPacket (#4802) --- .../protocol/java/level/JavaLevelParticlesTranslator.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelParticlesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelParticlesTranslator.java index 83b5da219..9ec6ee51e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelParticlesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelParticlesTranslator.java @@ -60,6 +60,7 @@ import java.util.function.Function; @Translator(packet = ClientboundLevelParticlesPacket.class) public class JavaLevelParticlesTranslator extends PacketTranslator { + private static final int MAX_PARTICLES = 100; @Override public void translate(GeyserSession session, ClientboundLevelParticlesPacket packet) { @@ -71,7 +72,8 @@ public class JavaLevelParticlesTranslator extends PacketTranslator Date: Sat, 29 Jun 2024 13:32:00 -0400 Subject: [PATCH 03/84] Update license to 2024 --- .github/workflows/build.yml | 1 - LICENSE | 2 +- licenseheader.txt | 24 ------------------------ 3 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 licenseheader.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bcf25e8e..59aa89086 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ on: - 'LICENSE' - 'Jenkinsfile ' - 'README.md' - - 'licenseheader.txt' jobs: build: diff --git a/LICENSE b/LICENSE index bde252698..ba3a723ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2023 GeyserMC. http://geysermc.org +Copyright (c) 2019-2024 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 diff --git a/licenseheader.txt b/licenseheader.txt deleted file mode 100644 index 9bfe117f9..000000000 --- a/licenseheader.txt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2019-2022 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 - */ \ No newline at end of file From 76126869721457b921ed25c2acf3523a543a6173 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:03:30 -0400 Subject: [PATCH 04/84] Don't show git version string twice --- core/build.gradle.kts | 4 ++-- core/src/main/java/org/geysermc/geyser/GeyserImpl.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index fe2576462..1d1794cf7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -89,7 +89,7 @@ sourceSets { blossom { val info = GitInfo() javaSources { - property("version", "${info.version} (${info.gitVersion})") + property("version", info.version) property("gitVersion", info.gitVersion) property("buildNumber", info.buildNumber.toString()) property("branch", info.branch) @@ -156,4 +156,4 @@ tasks.register("downloadBedrockData") { suffixedFiles = listOf("block_palette.nbt", "creative_items.json", "runtime_item_states.json") destinationDir = "$projectDir/src/main/resources/bedrock" -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index a3eeca6a5..88cc74691 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -694,7 +694,7 @@ public class GeyserImpl implements GeyserApi { @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isProductionEnvironment() { // First is if Blossom runs, second is if Blossom doesn't run - //noinspection ConstantConditions,MismatchedStringCase - changes in production + //noinspection ConstantConditions - changes in production return !("git-local/dev-0000000".equals(GeyserImpl.GIT_VERSION) || "${gitVersion}".equals(GeyserImpl.GIT_VERSION)); } From 48ea81eb9845b621e2bb1434669a2e2a51eded43 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 1 Jul 2024 17:17:52 +0200 Subject: [PATCH 05/84] Update mappings (#4812) Fixes short grass mapping --- core/src/main/resources/mappings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 23cb22f9c..69e302bf3 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 23cb22f9ceeb7f24b896a69a711944d7f3e756ed +Subproject commit 69e302bf3a118e5274f86b23a1818c61dba84c64 From ff1e1dd7e38f617454b1b439d3056361072b795b Mon Sep 17 00:00:00 2001 From: Eclipse <116838833+eclipseisoffline@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:17:47 +0000 Subject: [PATCH 06/84] Fix attribute modifier tooltips (#4816) --- .../geysermc/geyser/translator/item/ItemTranslator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index 85b4c6264..abe39f177 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -52,6 +52,7 @@ import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.*; @@ -209,7 +210,7 @@ public final class ItemTranslator { Map> slotsToModifiers = new HashMap<>(); for (ItemAttributeModifiers.Entry entry : modifiers.getModifiers()) { // convert the modifier tag to a lore entry - String loreEntry = attributeToLore(entry.getModifier(), language); + String loreEntry = attributeToLore(entry.getAttribute(), entry.getModifier(), language); if (loreEntry == null) { continue; // invalid or failed } @@ -254,13 +255,13 @@ public final class ItemTranslator { } @Nullable - private static String attributeToLore(ItemAttributeModifiers.AttributeModifier modifier, String language) { + private static String attributeToLore(int attribute, ItemAttributeModifiers.AttributeModifier modifier, String language) { double amount = modifier.getAmount(); if (amount == 0) { return null; } - String name = modifier.getId().asMinimalString(); + String name = AttributeType.Builtin.from(attribute).getIdentifier().asMinimalString(); // the namespace does not need to be present, but if it is, the java client ignores it as of pre-1.20.5 ModifierOperation operation = modifier.getOperation(); From 9f19c0a9f614d9e40141fb490163aa3ca2d66cd2 Mon Sep 17 00:00:00 2001 From: Ethan <68365423+letsgoawaydev@users.noreply.github.com> Date: Sat, 6 Jul 2024 17:58:54 +0800 Subject: [PATCH 07/84] Fix: Trial Spawner Level Events (#4821) * commit every thing i could do, couldnt get it to work with ominous stuff * Fix trial spawner events to work correctly. * Update mappings to latest * fix comment * Workaround for buggy trial chamber particles * Fix positioning and also fix become ominous event * Fix sounds and positioning again * Fix random new line * Update mappings * update cloudburst library to 20240704.153116-14 * Fix accidental random line --- .../java/level/JavaLevelEventTranslator.java | 134 +++++++++++++++--- core/src/main/resources/mappings | 2 +- gradle/libs.versions.toml | 2 +- 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java index d4e317af4..542af1598 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java @@ -28,9 +28,16 @@ package org.geysermc.geyser.translator.protocol.java.level; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; +import org.cloudburstmc.protocol.bedrock.packet.SpawnParticleEffectPacket; +import org.cloudburstmc.protocol.bedrock.packet.StopSoundPacket; +import org.cloudburstmc.protocol.bedrock.packet.TextPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.level.JukeboxSong; import org.geysermc.geyser.registry.Registries; @@ -40,13 +47,27 @@ import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.level.event.LevelEventTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.SoundUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; -import org.geysermc.mcprotocollib.protocol.data.game.level.event.*; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.BonemealGrowEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.BreakBlockEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.BreakPotionEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.ComposterEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.DragonFireballEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEventType; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.RecordEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.SculkBlockChargeEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.SmokeEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.TrialSpawnerDetectEventData; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.UnknownLevelEventData; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelEventPacket; import java.util.Collections; +import java.util.Optional; +import java.util.Random; import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; @Translator(packet = ClientboundLevelEventPacket.class) public class JavaLevelEventTranslator extends PacketTranslator { @@ -291,6 +312,20 @@ public class JavaLevelEventTranslator extends PacketTranslator effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_SPAWN_COBWEB); + case ANIMATION_VAULT_ACTIVATE -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_VAULT_ACTIVATE); + case ANIMATION_VAULT_DEACTIVATE -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_VAULT_DEACTIVATE); + case ANIMATION_VAULT_EJECT_ITEM -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.ANIMATION_VAULT_EJECT_ITEM); + case ANIMATION_TRIAL_SPAWNER_EJECT_ITEM -> { + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("trial_spawner.eject_item"); + playSoundPacket.setPosition(pos); + playSoundPacket.setVolume(1.0f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + return; + } case DRIPSTONE_DRIP -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_DRIPSTONE_DRIP); case PARTICLES_ELECTRIC_SPARK -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_ELECTRIC_SPARK); // Matches with a Bedrock server but doesn't seem to match up with Java case PARTICLES_AND_SOUND_WAX_ON -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_WAX_ON); @@ -303,22 +338,22 @@ public class JavaLevelEventTranslator extends PacketTranslator 0) { levelEventPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.SCULK_CHARGE); levelEventPacket.setTag( - NbtMap.builder() - .putInt("x", packet.getPosition().getX()) - .putInt("y", packet.getPosition().getY()) - .putInt("z", packet.getPosition().getZ()) - .putShort("charge", (short) eventData.getCharge()) - .putShort("facing", encodeFacing(eventData.getBlockFaces())) // TODO check if this is actually correct - .build() + NbtMap.builder() + .putInt("x", packet.getPosition().getX()) + .putInt("y", packet.getPosition().getY()) + .putInt("z", packet.getPosition().getZ()) + .putShort("charge", (short) eventData.getCharge()) + .putShort("facing", encodeFacing(eventData.getBlockFaces())) // TODO check if this is actually correct + .build() ); } else { levelEventPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.SCULK_CHARGE_POP); levelEventPacket.setTag( - NbtMap.builder() - .putInt("x", packet.getPosition().getX()) - .putInt("y", packet.getPosition().getY()) - .putInt("z", packet.getPosition().getZ()) - .build() + NbtMap.builder() + .putInt("x", packet.getPosition().getX()) + .putInt("y", packet.getPosition().getY()) + .putInt("z", packet.getPosition().getZ()) + .build() ); } session.sendUpstreamPacket(levelEventPacket); @@ -328,11 +363,11 @@ public class JavaLevelEventTranslator extends PacketTranslator { + // Particles spawn here + TrialSpawnerDetectEventData eventData = (TrialSpawnerDetectEventData) packet.getData(); + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_DETECTION); + // 0.75 is used here for Y instead of 0.5 to match Java Positioning. + // 0.5 is what the BDS uses for positioning. + effectPacket.setPosition(pos.sub(0.5f, 0.75f, 0.5f)); + effectPacket.setData(eventData.getDetectedPlayers()); + } + case PARTICLES_TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS -> { + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_DETECTION_CHARGED); + effectPacket.setPosition(pos.sub(0.5f, 0.75f, 0.5f)); + /* + Particles don't spawn here for some reason, only sound plays + This seems to be a bug in v1.21.0 and v1.21.1: see https://bugs.mojang.com/browse/MCPE-181465 + If this gets fixed, the spawnOminousTrialSpawnerParticles function can be removed. + The positioning should be the same as normal activation. + */ + spawnOminousTrialSpawnerParticles(session, pos); + } + case PARTICLES_TRIAL_SPAWNER_BECOME_OMINOUS -> { + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_BECOME_CHARGED); + effectPacket.setPosition(pos.sub(0.5f, 0.5f, 0.5f)); + // Same issue as above here + spawnOminousTrialSpawnerParticles(session, pos); + } + case PARTICLES_TRIAL_SPAWNER_SPAWN_MOB_AT -> { + // This should be its own class in MCProtocolLib. + // if 0, use Orange Flames, + // if 1, use Blue Flames for ominous spawners + UnknownLevelEventData eventData = (UnknownLevelEventData) packet.getData(); + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_SPAWNING); + effectPacket.setData(eventData.getData()); + } + case PARTICLES_TRIAL_SPAWNER_SPAWN -> { + UnknownLevelEventData eventData = (UnknownLevelEventData) packet.getData(); + effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_SPAWNING); + effectPacket.setData(eventData.getData()); + + Random random = ThreadLocalRandom.current(); + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setSound("trial_spawner.spawn_mob"); + playSoundPacket.setPosition(pos); + playSoundPacket.setVolume(1.0f); + playSoundPacket.setPitch(0.8f + random.nextFloat() * 0.3f); + session.sendUpstreamPacket(playSoundPacket); + } + case PARTICLES_TRIAL_SPAWNER_SPAWN_ITEM -> effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_TRIAL_SPAWNER_EJECTING); case SOUND_STOP_JUKEBOX_SONG -> { String bedrockSound = session.getWorldCache().removeActiveRecord(origin); if (bedrockSound == null) { @@ -396,4 +479,15 @@ public class JavaLevelEventTranslator extends PacketTranslator Date: Sat, 6 Jul 2024 21:50:19 +0800 Subject: [PATCH 08/84] Fix toggleable block opening sounds (doors, trapdoors, fence gates) (#4815) * Initial Commit * Fix minor copy and paste comment mistake * Remove debug log Co-authored-by: chris * Remove iron check on fence gate sound translator iron fence gates dont exist lol * Remove iron check from fence gate sound translator. Iron fence gates dont exist (yet) * Remove unnecessary curly braces * Rewrite the code, now functioning correctly with the runtime id * Update mappings * Better formatting Co-authored-by: chris * fix comment because it is referring to code that has been changed * Hopefully fix double closing * Seems like the double closing issue isnt from my code and from somewhere else * Fix issue where doors would update twice when opening/closing them using the upper half * change weird variable name --------- Co-authored-by: chris --- .../geyser/level/block/type/DoorBlock.java | 15 +++++++- .../java/level/JavaBlockUpdateTranslator.java | 4 +-- .../BlockSoundInteractionTranslator.java | 12 +++---- .../BucketSoundInteractionTranslator.java | 4 ++- .../ComparatorSoundInteractionTranslator.java | 4 ++- .../FlintAndSteelInteractionTranslator.java | 3 +- .../block/GrassPathInteractionTranslator.java | 4 ++- .../sound/block/HoeInteractionTranslator.java | 4 ++- .../LeverSoundInteractionTranslator.java | 4 ++- ...> OpenableSoundInteractionTranslator.java} | 35 +++++++++++++++---- .../org/geysermc/geyser/util/SoundUtils.java | 4 +-- 11 files changed, 69 insertions(+), 24 deletions(-) rename core/src/main/java/org/geysermc/geyser/translator/sound/block/{DoorSoundInteractionTranslator.java => OpenableSoundInteractionTranslator.java} (55%) diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java index bfde51a79..2efbdb523 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java @@ -37,9 +37,22 @@ public class DoorBlock extends Block { @Override public void updateBlock(GeyserSession session, BlockState state, Vector3i position) { + // Needed to check whether we must force the client to update the door state. + String doubleBlockHalf = state.getValue(Properties.DOUBLE_BLOCK_HALF); + + if (doubleBlockHalf.equals("lower")) { + BlockState oldBlockState = session.getGeyser().getWorldManager().blockAt(session, position); + // If these are the same, it means that we already updated the lower door block (manually in the workaround below), + // and we do not need to update the block in the cache/on the client side using the super.updateBlock() method again. + // Otherwise, we send the door updates twice which will cause visual glitches on the client side + if (oldBlockState == state) { + return; + } + } + super.updateBlock(session, state, position); - if (state.getValue(Properties.DOUBLE_BLOCK_HALF).equals("upper")) { + if (doubleBlockHalf.equals("upper")) { // Update the lower door block as Bedrock client doesn't like door to be closed from the top // See https://github.com/GeyserMC/Geyser/issues/4358 Vector3i belowDoorPosition = position.sub(0, 1, 0); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java index 74b961996..d89775662 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java @@ -100,8 +100,8 @@ public class JavaBlockUpdateTranslator extends PacketTranslator { - +public interface BlockSoundInteractionTranslator extends SoundInteractionTranslator { /** * Handles the block interaction when a player * right-clicks a block. * * @param session the session interacting with the block * @param position the position of the block - * @param identifier the identifier of the block + * @param state the state of the block */ - static void handleBlockInteraction(GeyserSession session, Vector3f position, String identifier) { + static void handleBlockInteraction(GeyserSession session, Vector3f position, BlockState state) { // If we need to get the hand identifier, only get it once and save it to a variable String handIdentifier = null; @@ -58,7 +58,7 @@ public interface BlockSoundInteractionTranslator extends SoundInteractionTransla if (interactionEntry.getKey().blocks().length != 0) { boolean contains = false; for (String blockIdentifier : interactionEntry.getKey().blocks()) { - if (identifier.contains(blockIdentifier)) { + if (state.toString().contains(blockIdentifier)) { contains = true; break; } @@ -87,7 +87,7 @@ public interface BlockSoundInteractionTranslator extends SoundInteractionTransla continue; } } - ((BlockSoundInteractionTranslator) interactionEntry.getValue()).translate(session, position, identifier); + ((BlockSoundInteractionTranslator) interactionEntry.getValue()).translate(session, position, state); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java index 7a5e86af7..45800e1ab 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java @@ -29,6 +29,7 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -37,7 +38,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class BucketSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); if (!session.isPlacedBucket()) { return; // No bucket was really interacted with } diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java index e77539e6e..9d5d3cd59 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/ComparatorSoundInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -36,7 +37,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class ComparatorSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); boolean powered = identifier.contains("mode=compare"); LevelEventPacket levelEventPacket = new LevelEventPacket(); levelEventPacket.setPosition(position); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java index a822f3520..e8225a336 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/FlintAndSteelInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -36,7 +37,7 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class FlintAndSteelInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); levelSoundEventPacket.setPosition(position); levelSoundEventPacket.setBabySound(false); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java index 0a91f8896..a25beaa50 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/GrassPathInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; @@ -37,7 +38,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class GrassPathInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); levelSoundEventPacket.setPosition(position); levelSoundEventPacket.setBabySound(false); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java index 410422b4a..1a583ab8a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/HoeInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; @@ -37,7 +38,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class HoeInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); levelSoundEventPacket.setPosition(position); levelSoundEventPacket.setBabySound(false); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java index 17b8768bc..fd045739c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/LeverSoundInteractionTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @@ -36,7 +37,8 @@ import org.geysermc.geyser.translator.sound.SoundTranslator; public class LeverSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); boolean powered = identifier.contains("powered=true"); LevelEventPacket levelEventPacket = new LevelEventPacket(); levelEventPacket.setPosition(position); diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/DoorSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/OpenableSoundInteractionTranslator.java similarity index 55% rename from core/src/main/java/org/geysermc/geyser/translator/sound/block/DoorSoundInteractionTranslator.java rename to core/src/main/java/org/geysermc/geyser/translator/sound/block/OpenableSoundInteractionTranslator.java index 8f8ab8bf6..93d55ca33 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/DoorSoundInteractionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/OpenableSoundInteractionTranslator.java @@ -27,21 +27,42 @@ package org.geysermc.geyser.translator.sound.block; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; +import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.sound.BlockSoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; @SoundTranslator(blocks = {"door", "fence_gate"}) -public class DoorSoundInteractionTranslator implements BlockSoundInteractionTranslator { +public class OpenableSoundInteractionTranslator implements BlockSoundInteractionTranslator { @Override - public void translate(GeyserSession session, Vector3f position, String identifier) { + public void translate(GeyserSession session, Vector3f position, BlockState state) { + String identifier = state.toString(); if (identifier.contains("iron")) return; - LevelEventPacket levelEventPacket = new LevelEventPacket(); - levelEventPacket.setType(LevelEvent.SOUND_DOOR_OPEN); - levelEventPacket.setPosition(position); - levelEventPacket.setData(0); - session.sendUpstreamPacket(levelEventPacket); + SoundEvent event = getSound(identifier.contains("open=true"), identifier); + LevelSoundEventPacket levelSoundEventPacket = new LevelSoundEventPacket(); + levelSoundEventPacket.setPosition(position.add(0.5, 0.5, 0.5)); + levelSoundEventPacket.setBabySound(false); + levelSoundEventPacket.setRelativeVolumeDisabled(false); + levelSoundEventPacket.setIdentifier(":"); + levelSoundEventPacket.setSound(event); + levelSoundEventPacket.setExtraData(session.getBlockMappings().getBedrockBlock(state).getRuntimeId()); + session.sendUpstreamPacket(levelSoundEventPacket); + } + + private SoundEvent getSound(boolean open, String identifier) { + if (identifier.contains("_door")) { + return open ? SoundEvent.DOOR_OPEN : SoundEvent.DOOR_CLOSE; + } + + if (identifier.contains("_trapdoor")) { + return open ? SoundEvent.TRAPDOOR_OPEN : SoundEvent.TRAPDOOR_CLOSE; + } + + // Fence Gate + return open ? SoundEvent.FENCE_GATE_OPEN : SoundEvent.FENCE_GATE_CLOSE; } } diff --git a/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java b/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java index 524d241db..693ce136a 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java @@ -133,7 +133,7 @@ public final class SoundUtils { } if (sound == null) { session.getGeyser().getLogger().debug("[Builtin] Sound for original '" + soundIdentifier + "' to mappings '" + soundMapping.getBedrock() - + "' was not a playable level sound, or has yet to be mapped to an enum in SoundEvent."); + + "' was not a playable level sound, or has yet to be mapped to an enum in SoundEvent."); return; } @@ -144,7 +144,7 @@ public final class SoundUtils { // Minecraft Wiki: 2^(x/12) = Java pitch where x is -12 to 12 // Java sends the note value as above starting with -12 and ending at 12 // Bedrock has a number for each type of note, then proceeds up the scale by adding to that number - soundPacket.setExtraData(soundMapping.getExtraData() + (int)(Math.round((Math.log10(pitch) / Math.log10(2)) * 12)) + 12); + soundPacket.setExtraData(soundMapping.getExtraData() + (int) (Math.round((Math.log10(pitch) / Math.log10(2)) * 12)) + 12); } else if (sound == SoundEvent.PLACE && soundMapping.getExtraData() == -1) { if (!soundMapping.getIdentifier().equals(":")) { int javaId = BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getOrDefault(soundMapping.getIdentifier(), Block.JAVA_AIR_ID); From 74cabed67444e1a6b79ba6966df08b7c1e2bf6ca Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 6 Jul 2024 20:47:49 +0200 Subject: [PATCH 09/84] Update to loom 1.7-SNAPSHOT, update gradle wrapper (#4820) --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78601024f..49c02d190 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ minecraft = "1.21" indra = "3.1.3" shadow = "8.1.1" architectury-plugin = "3.4-SNAPSHOT" -architectury-loom = "1.6-SNAPSHOT" +architectury-loom = "1.7-SNAPSHOT" minotaur = "2.8.7" lombok = "8.4" blossom = "2.1.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 34118 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 delta 34176 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 SGW6ptSs!X7{rG*o_g?|vpSEZK diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..a4413138c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From cbd59de37b3026e21fd01f71b70df00503ed0d8a Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 6 Jul 2024 23:14:15 +0200 Subject: [PATCH 10/84] Only apply door fix when we can actually access the old blockstate (#4827) --- .../java/org/geysermc/geyser/level/block/type/DoorBlock.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java index 2efbdb523..956d4b771 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/DoorBlock.java @@ -40,7 +40,7 @@ public class DoorBlock extends Block { // Needed to check whether we must force the client to update the door state. String doubleBlockHalf = state.getValue(Properties.DOUBLE_BLOCK_HALF); - if (doubleBlockHalf.equals("lower")) { + if (!session.getGeyser().getWorldManager().hasOwnChunkCache() && doubleBlockHalf.equals("lower")) { BlockState oldBlockState = session.getGeyser().getWorldManager().blockAt(session, position); // If these are the same, it means that we already updated the lower door block (manually in the workaround below), // and we do not need to update the block in the cache/on the client side using the super.updateBlock() method again. From a807fa6bb5c19f12894d82361372393b4d13c97c Mon Sep 17 00:00:00 2001 From: LetsGoAway <68365423+letsgoawaydev@users.noreply.github.com> Date: Sun, 7 Jul 2024 17:27:49 +0800 Subject: [PATCH 11/84] Fix typo in version command (#4830) --- .../src/main/java/org/geysermc/geyser/network/GameProtocol.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index de995301a..1e9e76205 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -67,7 +67,7 @@ public final class GameProtocol { .minecraftVersion("1.20.80/1.20.81") .build())); SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(DEFAULT_BEDROCK_CODEC.toBuilder() - .minecraftVersion("1.21.0/1.20.1") + .minecraftVersion("1.21.0/1.21.1") .build())); } From e0af0a54b78ada10d14c94469c75739ba06b6295 Mon Sep 17 00:00:00 2001 From: strom <167184523+stromsoftware@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:59:18 +0200 Subject: [PATCH 12/84] Fix: Text display passenger offsets (#4676) --- .../geyser/entity/EntityDefinitions.java | 4 +- .../geyser/entity/type/DisplayBaseEntity.java | 77 +++++++++++++++++++ .../geyser/entity/type/TextDisplayEntity.java | 18 +---- .../org/geysermc/geyser/util/EntityUtils.java | 24 ++++-- 4 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 1496f8a82..11b4a32d1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -307,11 +307,11 @@ public final class EntityDefinitions { .addTranslator(MetadataType.INT, TNTEntity::setFuseLength) .build(); - EntityDefinition displayBase = EntityDefinition.inherited(entityBase.factory(), entityBase) + EntityDefinition displayBase = EntityDefinition.inherited(DisplayBaseEntity::new, entityBase) .addTranslator(null) // Interpolation delay .addTranslator(null) // Transformation interpolation duration .addTranslator(null) // Position/Rotation interpolation duration - .addTranslator(null) // Translation + .addTranslator(MetadataType.VECTOR3, DisplayBaseEntity::setTranslation) // Translation .addTranslator(null) // Scale .addTranslator(null) // Left rotation .addTranslator(null) // Right rotation diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java new file mode 100644 index 000000000..16587d125 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 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.geyser.entity.type; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; + +import java.util.Optional; +import java.util.UUID; + +public class DisplayBaseEntity extends Entity { + + private @Nullable Vector3f baseTranslation; + + public DisplayBaseEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { + // Don't allow the display name to be hidden - messes with our armor stand. + // On JE: Hiding the display name still shows the display entity text. + } + + @Override + public void setDisplayName(EntityMetadata, ?> entityMetadata) { + // This would usually set EntityDataTypes.NAME, but we are instead using NAME for the text display. + // On JE: custom name does not override text display. + } + + public void setTranslation(EntityMetadata translationMeta){ + this.baseTranslation = translationMeta.getValue(); + if (this.baseTranslation == null) { + return; + } + if (this.vehicle == null) { + this.setRiderSeatPosition(this.baseTranslation); + this.moveRelative(this.baseTranslation.getX(), this.baseTranslation.getY(), this.baseTranslation.getZ(), yaw, pitch, headYaw, false); + } else { + EntityUtils.updateMountOffset(this, this.vehicle, true, true, false); + this.updateBedrockMetadata(); + } + } + + public Vector3f getTranslation() { + return baseTranslation; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java index 28f38f919..ff5604c19 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * Copyright (c) 2024 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 @@ -32,13 +32,11 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; -import java.util.Optional; import java.util.UUID; // Note: 1.19.4 requires that the billboard is set to something in order to show, on Java Edition -public class TextDisplayEntity extends Entity { +public class TextDisplayEntity extends DisplayBaseEntity { public TextDisplayEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -51,18 +49,6 @@ public class TextDisplayEntity extends Entity { this.dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1); } - @Override - public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { - // Don't allow the display name to be hidden - messes with our armor stand. - // On JE: Hiding the display name still shows the display entity text. - } - - @Override - public void setDisplayName(EntityMetadata, ?> entityMetadata) { - // This would usually set EntityDataTypes.NAME, but we are instead using NAME for the text display. - // On JE: custom name does not override text display. - } - public void setText(EntityMetadata entityMetadata) { this.dirtyMetadata.put(EntityDataTypes.NAME, MessageTranslator.convertMessage(entityMetadata.getValue())); } diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java index bfb70a4ed..53aefde1e 100644 --- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2024 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 @@ -32,6 +32,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.TextDisplayEntity; import org.geysermc.geyser.entity.type.living.ArmorStandEntity; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; @@ -172,11 +173,7 @@ public final class EntityUtils { case BOAT -> { // Without the X offset, more than one entity on a boat is stacked on top of each other if (moreThanOneEntity) { - if (rider) { - xOffset = 0.2f; - } else { - xOffset = -0.6f; - } + xOffset = rider ? 0.2f : -0.6f; if (passenger instanceof AnimalEntity) { xOffset += 0.2f; } @@ -203,6 +200,18 @@ public final class EntityUtils { case CHEST_BOAT -> xOffset = 0.15F; case CHICKEN -> zOffset = -0.1f; case TRADER_LLAMA, LLAMA -> zOffset = -0.3f; + case TEXT_DISPLAY -> { + if (passenger instanceof TextDisplayEntity textDisplay) { + Vector3f displayTranslation = textDisplay.getTranslation(); + if (displayTranslation == null) { + return; + } + + xOffset = displayTranslation.getX(); + yOffset = displayTranslation.getY() + 0.2f; + zOffset = displayTranslation.getZ(); + } + } } /* * Bedrock Differences @@ -228,8 +237,7 @@ public final class EntityUtils { if (mount instanceof ArmorStandEntity armorStand) { yOffset -= armorStand.getYOffset(); } - Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset); - passenger.setRiderSeatPosition(offset); + passenger.setRiderSeatPosition(Vector3f.from(xOffset, yOffset, zOffset)); } } From 0a928c41b01de14ef428b348f91d0226f2236968 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 8 Jul 2024 00:41:00 +0200 Subject: [PATCH 13/84] Show potions & custom items in the shulker box tooltip (#4822) * Fix: Potions/Custom items showing up improperly in shulker box previews * oops --- .../geyser/item/type/ShulkerBoxItem.java | 31 +++++++++++++++++-- .../translator/item/BedrockItemBuilder.java | 9 +++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java b/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java index a539fa739..c3b739adc 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java @@ -29,15 +29,19 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; +import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.geyser.translator.item.CustomItemTranslator; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; import java.util.ArrayList; import java.util.List; @@ -64,12 +68,35 @@ public class ShulkerBoxItem extends BlockItem { } ItemMapping boxMapping = session.getItemMappings().getMapping(item.getId()); - NbtMapBuilder boxItemNbt = BedrockItemBuilder.createItemNbt(boxMapping, item.getAmount(), boxMapping.getBedrockData()); // Final item tag to add to the list + int bedrockData = boxMapping.getBedrockData(); + String bedrockIdentifier = boxMapping.getBedrockIdentifier(); + DataComponents boxComponents = item.getDataComponents(); + + if (boxComponents != null) { + // Check for custom items + ItemDefinition customItemDefinition = CustomItemTranslator.getCustomItem(boxComponents, boxMapping); + if (customItemDefinition != null) { + bedrockIdentifier = customItemDefinition.getIdentifier(); + bedrockData = 0; + } else { + // Manual checks for potions/tipped arrows + if (boxMapping.getJavaItem() instanceof PotionItem || boxMapping.getJavaItem() instanceof ArrowItem) { + PotionContents potionContents = boxComponents.get(DataComponentType.POTION_CONTENTS); + if (potionContents != null) { + Potion potion = Potion.getByJavaId(potionContents.getPotionId()); + if (potion != null) { + bedrockData = potion.getBedrockId(); + } + } + } + } + } + + NbtMapBuilder boxItemNbt = BedrockItemBuilder.createItemNbt(bedrockIdentifier, item.getAmount(), bedrockData); // Final item tag to add to the list boxItemNbt.putByte("Slot", (byte) slot); boxItemNbt.putByte("WasPickedUp", (byte) 0); // ??? TODO might not be needed // Only the display name is what we have interest in, so just translate that if relevant - DataComponents boxComponents = item.getDataComponents(); if (boxComponents != null) { String customName = ItemTranslator.getCustomName(session, boxComponents, boxMapping, '7'); if (customName != null) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java b/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java index 52d5b7e31..e989288c2 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/BedrockItemBuilder.java @@ -145,8 +145,15 @@ public final class BedrockItemBuilder { * Creates item NBT to nest within NBT with name, count, and damage set. */ public static NbtMapBuilder createItemNbt(ItemMapping mapping, int count, int damage) { + return createItemNbt(mapping.getBedrockIdentifier(), count, damage); + } + + /** + * Creates item NBT to nest within NBT with name, count, and damage set. + */ + public static NbtMapBuilder createItemNbt(String bedrockIdentifier, int count, int damage) { NbtMapBuilder builder = NbtMap.builder(); - builder.putString("Name", mapping.getBedrockIdentifier()); + builder.putString("Name", bedrockIdentifier); builder.putByte("Count", (byte) count); builder.putShort("Damage", (short) damage); return builder; From f825007d9a5e5c3856aa09e05786f4dcb1f4102a Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:39:43 -0400 Subject: [PATCH 14/84] Support Bedrock 1.21.2 --- .../org/geysermc/geyser/network/GameProtocol.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 1e9e76205..c79ef365d 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -43,13 +43,17 @@ import java.util.StringJoiner; */ public final class GameProtocol { + // Surprise protocol bump WOW + private static final BedrockCodec BEDROCK_V686 = Bedrock_v685.CODEC.toBuilder() + .protocolVersion(686) + .minecraftVersion("1.21.2") + .build(); + /** * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v685.CODEC.toBuilder() - .minecraftVersion("1.21.1") - .build()); + public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(BEDROCK_V686); /** * A list of all supported Bedrock versions that can join Geyser @@ -66,9 +70,10 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v671.CODEC.toBuilder() .minecraftVersion("1.20.80/1.20.81") .build())); - SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(DEFAULT_BEDROCK_CODEC.toBuilder() + SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v685.CODEC.toBuilder() .minecraftVersion("1.21.0/1.21.1") .build())); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } /** From a3b1f203ec7d607644195d1630b0077a5fe211b5 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:59:48 -0400 Subject: [PATCH 15/84] Bump Bedrock version in README --- README.md | 2 +- core/src/main/resources/mappings | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e3b5a496a..14bdb17a9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,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 joined us here! -### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.1 and Minecraft Java 1.21 +### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.2 and Minecraft Java 1.21 ## Setting Up Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index aaf53d695..23cb22f9c 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit aaf53d6953c927e5ac1b87fd6627ffbfd4aa7cf5 +Subproject commit 23cb22f9ceeb7f24b896a69a711944d7f3e756ed From 7fdb410d26f8759c906d6673eacce9f60d00ff4b Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:00:27 -0400 Subject: [PATCH 16/84] Fix mappings --- core/src/main/resources/mappings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 23cb22f9c..aaf53d695 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 23cb22f9ceeb7f24b896a69a711944d7f3e756ed +Subproject commit aaf53d6953c927e5ac1b87fd6627ffbfd4aa7cf5 From 2a6025f3fc841f471c92b23fd2ed982eca36ae4f Mon Sep 17 00:00:00 2001 From: Eclipse Date: Wed, 10 Jul 2024 10:02:32 +0000 Subject: [PATCH 17/84] Fix reading enchantments from server (#4836) --- .../geyser/inventory/recipe/TrimRecipe.java | 21 ++++--- .../updater/AnvilInventoryUpdater.java | 27 +++++---- .../geyser/item/enchantment/Enchantment.java | 59 ++++++++++++++----- .../geysermc/geyser/level/JavaDimension.java | 6 +- .../geysermc/geyser/level/JukeboxSong.java | 8 +-- .../geyser/session/cache/RegistryCache.java | 38 ++++++++---- .../geyser/session/cache/TagCache.java | 8 ++- .../cache/registry/RegistryEntryContext.java | 56 ++++++++++++++++++ .../geysermc/geyser/text/TextDecoration.java | 6 +- 9 files changed, 166 insertions(+), 63 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java index 8289813a4..b5e76a296 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/TrimRecipe.java @@ -32,9 +32,8 @@ import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor; import org.geysermc.geyser.registry.type.ItemMapping; -import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; /** * Stores information on trim materials and patterns, including smithing armor hacks for pre-1.20. @@ -46,18 +45,18 @@ public final class TrimRecipe { public static final ItemDescriptorWithCount ADDITION = tagDescriptor("minecraft:trim_materials"); public static final ItemDescriptorWithCount TEMPLATE = tagDescriptor("minecraft:trim_templates"); - public static TrimMaterial readTrimMaterial(GeyserSession session, RegistryEntry entry) { - String key = entry.getId().asMinimalString(); + public static TrimMaterial readTrimMaterial(RegistryEntryContext context) { + String key = context.id().asMinimalString(); // Color is used when hovering over the item // Find the nearest legacy color from the RGB Java gives us to work with // Also yes this is a COMPLETE hack but it works ok!!!!! - String colorTag = entry.getData().getCompound("description").getString("color"); + String colorTag = context.data().getCompound("description").getString("color"); TextColor color = TextColor.fromHexString(colorTag); String legacy = MessageTranslator.convertMessage(Component.space().color(color)); - String itemIdentifier = entry.getData().getString("ingredient"); - ItemMapping itemMapping = session.getItemMappings().getMapping(itemIdentifier); + String itemIdentifier = context.data().getString("ingredient"); + ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier); if (itemMapping == null) { // This should never happen so not sure what to do here. itemMapping = ItemMapping.AIR; @@ -66,11 +65,11 @@ public final class TrimRecipe { return new TrimMaterial(key, legacy.substring(2).trim(), itemMapping.getBedrockIdentifier()); } - public static TrimPattern readTrimPattern(GeyserSession session, RegistryEntry entry) { - String key = entry.getId().asMinimalString(); + public static TrimPattern readTrimPattern(RegistryEntryContext context) { + String key = context.id().asMinimalString(); - String itemIdentifier = entry.getData().getString("template_item"); - ItemMapping itemMapping = session.getItemMappings().getMapping(itemIdentifier); + String itemIdentifier = context.data().getString("template_item"); + ItemMapping itemMapping = context.session().getItemMappings().getMapping(itemIdentifier); if (itemMapping == null) { // This should never happen so not sure what to do here. itemMapping = ItemMapping.AIR; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java index c3ac73372..7afd31cc9 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.inventory.updater; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.stream.IntStream; import net.kyori.adventure.text.Component; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -41,11 +42,14 @@ import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.EnchantmentTag; +import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.ItemUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket; @@ -310,17 +314,18 @@ public class AnvilInventoryUpdater extends InventoryUpdater { for (Object2IntMap.Entry entry : getEnchantments(session, material).object2IntEntrySet()) { Enchantment enchantment = entry.getKey(); - boolean canApply = isEnchantedBook(input) || session.getTagCache().is(enchantment.supportedItems(), input); - var exclusiveSet = enchantment.exclusiveSet(); - if (exclusiveSet != null) { - int[] incompatibleEnchantments = session.getTagCache().get(exclusiveSet); - for (int i : incompatibleEnchantments) { - Enchantment incompatible = session.getRegistryCache().enchantments().byId(i); - if (combinedEnchantments.containsKey(incompatible)) { - canApply = false; - if (!bedrock) { - cost++; - } + HolderSet supportedItems = enchantment.supportedItems(); + int[] supportedItemIds = supportedItems.resolve(tagId -> session.getTagCache().get(ItemTag.ALL_ITEM_TAGS.get(tagId))); + boolean canApply = isEnchantedBook(input) || IntStream.of(supportedItemIds).anyMatch(id -> id == input.getJavaId()); + + HolderSet exclusiveSet = enchantment.exclusiveSet(); + int[] incompatibleEnchantments = exclusiveSet.resolve(tagId -> session.getTagCache().get(EnchantmentTag.ALL_ENCHANTMENT_TAGS.get(tagId))); + for (int i : incompatibleEnchantments) { + Enchantment incompatible = session.getRegistryCache().enchantments().byId(i); + if (combinedEnchantments.containsKey(incompatible)) { + canApply = false; + if (!bedrock) { + cost++; } } } diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java index c5c0d2611..3c0caa60c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java +++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java @@ -25,18 +25,21 @@ package org.geysermc.geyser.item.enchantment; +import java.util.List; +import java.util.function.Function; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.inventory.item.BedrockEnchantment; -import org.geysermc.geyser.session.cache.tags.EnchantmentTag; -import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; /** * @param description only populated if {@link #bedrockEnchantment()} is not null. @@ -44,28 +47,32 @@ import java.util.Set; */ public record Enchantment(String identifier, Set effects, - ItemTag supportedItems, + HolderSet supportedItems, int maxLevel, String description, int anvilCost, - @Nullable EnchantmentTag exclusiveSet, + HolderSet exclusiveSet, @Nullable BedrockEnchantment bedrockEnchantment) { - // Implementation note: I have a feeling the tags can be a list of items, because in vanilla they're HolderSet classes. - // I'm not sure how that's wired over the network, so we'll put it off. - public static Enchantment read(RegistryEntry entry) { - NbtMap data = entry.getData(); + public static Enchantment read(RegistryEntryContext context) { + NbtMap data = context.data(); Set effects = readEnchantmentComponents(data.getCompound("effects")); - String supportedItems = data.getString("supported_items").substring(1); // Remove '#' at beginning that indicates tag + + HolderSet supportedItems = readHolderSet(data.get("supported_items"), itemId -> Registries.JAVA_ITEM_IDENTIFIERS.getOrDefault(itemId.asString(), Items.AIR).javaId()); + int maxLevel = data.getInt("max_level"); int anvilCost = data.getInt("anvil_cost"); - String exclusiveSet = data.getString("exclusive_set", null); - EnchantmentTag exclusiveSetTag = exclusiveSet == null ? null : EnchantmentTag.ALL_ENCHANTMENT_TAGS.get(MinecraftKey.key(exclusiveSet.substring(1))); - BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(entry.getId().asString()); + + HolderSet exclusiveSet = readHolderSet(data.getOrDefault("exclusive_set", null), context::getNetworkId); + + BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(context.id().asString()); + + // TODO - description is a component. So if a hardcoded literal string is given, this will display normally on Java, + // but Geyser will attempt to lookup the literal string as translation - and will fail, displaying an empty string as enchantment name. String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null; - return new Enchantment(entry.getId().asString(), effects, ItemTag.ALL_ITEM_TAGS.get(MinecraftKey.key(supportedItems)), maxLevel, - description, anvilCost, exclusiveSetTag, bedrockEnchantment); + return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel, + description, anvilCost, exclusiveSet, bedrockEnchantment); } private static Set readEnchantmentComponents(NbtMap effects) { @@ -77,4 +84,24 @@ public record Enchantment(String identifier, } return Set.copyOf(components); // Also ensures any empty sets are consolidated } + + // TODO holder set util? + private static HolderSet readHolderSet(@Nullable Object holderSet, Function keyIdMapping) { + if (holderSet == null) { + return new HolderSet(new int[]{}); + } + + if (holderSet instanceof String stringTag) { + // Tag + if (stringTag.startsWith("#")) { + return new HolderSet(Key.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag + } else { + return new HolderSet(new int[]{keyIdMapping.apply(Key.key(stringTag))}); + } + } else if (holderSet instanceof List list) { + // Assume the list is a list of strings + return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).map(keyIdMapping).mapToInt(Integer::intValue).toArray()); + } + throw new IllegalArgumentException("Holder set must either be a tag, a string ID or a list of string IDs"); + } } diff --git a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java index 6112dc6cf..dd0f4215e 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java +++ b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java @@ -26,7 +26,7 @@ package org.geysermc.geyser.level; import org.cloudburstmc.nbt.NbtMap; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; /** * Represents the information we store from the current Java dimension @@ -35,8 +35,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; */ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale) { - public static JavaDimension read(RegistryEntry entry) { - NbtMap dimension = entry.getData(); + public static JavaDimension read(RegistryEntryContext entry) { + NbtMap dimension = entry.data(); int minY = dimension.getInt("min_y"); int maxY = dimension.getInt("height"); // Logical height can be ignored probably - seems to be for artificial limits like the Nether. diff --git a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java index 156a62cd1..b00dc9f98 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java +++ b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java @@ -27,13 +27,13 @@ package org.geysermc.geyser.level; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; public record JukeboxSong(String soundEvent, String description) { - public static JukeboxSong read(RegistryEntry entry) { - NbtMap data = entry.getData(); + public static JukeboxSong read(RegistryEntryContext context) { + NbtMap data = context.data(); Object soundEventObject = data.get("sound_event"); String soundEvent; if (soundEventObject instanceof NbtMap map) { @@ -42,7 +42,7 @@ public record JukeboxSong(String soundEvent, String description) { soundEvent = string; } else { soundEvent = ""; - GeyserImpl.getInstance().getLogger().debug("Sound event for " + entry.getId() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject); + GeyserImpl.getInstance().getLogger().debug("Sound event for " + context.id() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject); } String description = MessageTranslator.deserializeDescription(data); return new JukeboxSong(soundEvent, description); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java index 3121af369..a393d461d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java @@ -27,6 +27,8 @@ package org.geysermc.geyser.session.cache; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -45,6 +47,7 @@ import org.geysermc.geyser.level.JukeboxSong; import org.geysermc.geyser.level.PaintingType; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry; import org.geysermc.geyser.text.TextDecoration; import org.geysermc.geyser.translator.level.BiomeTranslator; @@ -59,7 +62,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.ToIntFunction; @@ -76,16 +78,16 @@ public final class RegistryCache { private static final Map>> REGISTRIES = new HashMap<>(); static { - register("chat_type", cache -> cache.chatTypes, ($, entry) -> TextDecoration.readChatType(entry)); - register("dimension_type", cache -> cache.dimensions, ($, entry) -> JavaDimension.read(entry)); - register("enchantment", cache -> cache.enchantments, ($, entry) -> Enchantment.read(entry)); - register("jukebox_song", cache -> cache.jukeboxSongs, ($, entry) -> JukeboxSong.read(entry)); - register("painting_variant", cache -> cache.paintings, ($, entry) -> PaintingType.getByName(entry.getId())); + register("chat_type", cache -> cache.chatTypes, TextDecoration::readChatType); + register("dimension_type", cache -> cache.dimensions, JavaDimension::read); + register("enchantment", cache -> cache.enchantments, Enchantment::read); + register("jukebox_song", cache -> cache.jukeboxSongs, JukeboxSong::read); + register("painting_variant", cache -> cache.paintings, context -> PaintingType.getByName(context.id())); register("trim_material", cache -> cache.trimMaterials, TrimRecipe::readTrimMaterial); register("trim_pattern", cache -> cache.trimPatterns, TrimRecipe::readTrimPattern); register("worldgen/biome", (cache, array) -> cache.biomeTranslations = array, BiomeTranslator::loadServerBiome); - register("banner_pattern", cache -> cache.bannerPatterns, ($, entry) -> BannerPattern.getByJavaIdentifier(entry.getId())); - register("wolf_variant", cache -> cache.wolfVariants, ($, entry) -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(entry.getId().asString())); + register("banner_pattern", cache -> cache.bannerPatterns, context -> BannerPattern.getByJavaIdentifier(context.id())); + register("wolf_variant", cache -> cache.wolfVariants, context -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(context.id().asString())); // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); @@ -149,25 +151,35 @@ public final class RegistryCache { * @param reader converts the RegistryEntry NBT into a class file * @param the class that represents these entries. */ - private static void register(String registry, Function> localCacheFunction, BiFunction reader) { - Key key = MinecraftKey.key(registry); - REGISTRIES.put(key, (registryCache, entries) -> { + private static void register(String registry, Function> localCacheFunction, Function reader) { + Key registryKey = MinecraftKey.key(registry); + REGISTRIES.put(registryKey, (registryCache, entries) -> { Map localRegistry = null; JavaRegistry localCache = localCacheFunction.apply(registryCache); // Clear each local cache every time a new registry entry is given to us // (e.g. proxy server switches) + + // Store each of the entries resource location IDs and their respective network ID, + // used for the key mapper that's currently only used by the Enchantment class + Object2IntMap entryIdMap = new Object2IntOpenHashMap<>(); + for (int i = 0; i < entries.size(); i++) { + entryIdMap.put(entries.get(i).getId(), i); + } + List builder = new ArrayList<>(entries.size()); for (int i = 0; i < entries.size(); i++) { RegistryEntry entry = entries.get(i); // If the data is null, that's the server telling us we need to use our default values. if (entry.getData() == null) { if (localRegistry == null) { // Lazy initialize - localRegistry = DEFAULTS.get(key); + localRegistry = DEFAULTS.get(registryKey); } entry = new RegistryEntry(entry.getId(), localRegistry.get(entry.getId())); } + + RegistryEntryContext context = new RegistryEntryContext(entry, entryIdMap, registryCache.session); // This is what Geyser wants to keep as a value for this registry. - T cacheEntry = reader.apply(registryCache.session, entry); + T cacheEntry = reader.apply(context); builder.add(i, cacheEntry); } localCache.reset(builder); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index c8bfc7eed..f4d69dcdb 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -130,8 +130,12 @@ public final class TagCache { return contains(values, item.javaId()); } - public int[] get(EnchantmentTag tag) { - return this.enchantments[tag.ordinal()]; + public int[] get(ItemTag itemTag) { + return this.items[itemTag.ordinal()]; + } + + public int[] get(EnchantmentTag enchantmentTag) { + return this.enchantments[enchantmentTag.ordinal()]; } private static boolean contains(int[] array, int i) { diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java new file mode 100644 index 000000000..415890d95 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 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.geyser.session.cache.registry; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import java.util.Map; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; + +/** + * Used to store context around a single registry entry when reading said entry's NBT. + * + * @param entry the registry entry being read. + * @param keyIdMap a map for each of the resource location's in the registry and their respective network IDs. + * @param session the Geyser session. + */ +public record RegistryEntryContext(RegistryEntry entry, Object2IntMap keyIdMap, GeyserSession session) { + + public int getNetworkId(Key registryKey) { + return keyIdMap.getOrDefault(registryKey, 0); + } + + public Key id() { + return entry.getId(); + } + + // Not annotated as nullable because data should never be null here + public NbtMap data() { + return entry.getData(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java b/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java index ab9e2b5ed..94aec22ef 100644 --- a/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java +++ b/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java @@ -29,7 +29,7 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; -import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration; @@ -43,11 +43,11 @@ public record TextDecoration(String translationKey, List parameters, throw new UnsupportedOperationException(); } - public static ChatType readChatType(RegistryEntry entry) { + public static ChatType readChatType(RegistryEntryContext context) { // Note: The ID is NOT ALWAYS THE SAME! ViaVersion as of 1.19 adds two registry entries that do NOT match vanilla. // (This note has been passed around through several classes and iterations. It stays as a warning // to anyone that dares to try and hardcode registry IDs.) - NbtMap tag = entry.getData(); + NbtMap tag = context.data(); NbtMap chat = tag.getCompound("chat", null); if (chat != null) { String translationKey = chat.getString("translation_key"); From 93b0a612659d026a293a53f632f89d1c5775eacf Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 12 Jul 2024 20:55:40 +0200 Subject: [PATCH 18/84] Fix: hanging sign recipe not showing up in the recipe book (#4814) --- .../populator/CreativeItemRegistryPopulator.java | 12 +----------- .../registry/populator/ItemRegistryPopulator.java | 7 ++----- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java index c536d739c..2c033edc7 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java @@ -49,8 +49,6 @@ import java.util.function.Consumer; public class CreativeItemRegistryPopulator { private static final List> JAVA_ONLY_ITEM_FILTER = List.of( - // Just shows an empty texture; either way it doesn't exist in the creative menu on Java - (identifier, data) -> identifier.equals("minecraft:debug_stick"), // Bedrock-only as its own item (identifier, data) -> identifier.equals("minecraft:empty_map") && data == 2, // Bedrock-only banner patterns @@ -103,16 +101,8 @@ public class CreativeItemRegistryPopulator { } GeyserBedrockBlock blockDefinition = null; - JsonNode blockRuntimeIdNode = itemNode.get("blockRuntimeId"); JsonNode blockStateNode; - if (blockRuntimeIdNode != null) { - bedrockBlockRuntimeId = blockRuntimeIdNode.asInt(); - if (bedrockBlockRuntimeId == 0 && !identifier.equals("minecraft:blue_candle")) { // FIXME - bedrockBlockRuntimeId = -1; - } - - blockDefinition = bedrockBlockRuntimeId == -1 ? null : blockMappings.getDefinition(bedrockBlockRuntimeId); - } else if ((blockStateNode = itemNode.get("block_state_b64")) != null) { + if ((blockStateNode = itemNode.get("block_state_b64")) != null) { byte[] bytes = Base64.getDecoder().decode(blockStateNode.asText()); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); try { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index e19066462..85207ac3a 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -252,10 +252,7 @@ public class ItemRegistryPopulator { } else { // Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1); - if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) { - // Fallback - bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId); - } else { + if (aValidBedrockBlockId != -1 || customBlockItemOverride != null) { // As of 1.16.220, every item requires a block runtime ID attached to it. // This is mostly for identifying different blocks with the same item ID - wool, slabs, some walls. // However, in order for some visuals and crafting to work, we need to send the first matching block state @@ -266,7 +263,7 @@ public class ItemRegistryPopulator { boolean firstPass = true; // Block states are all grouped together. In the mappings, we store the first block runtime ID in order, // and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents - Integer lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId(); + int lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId(); for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) { GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlock(i); NbtMap blockTag = bedrockBlockRuntimeId.getState(); From b0c7ddb68dbf3141f9710ea970e5f06fd246c2a4 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 13 Jul 2024 18:53:13 +0200 Subject: [PATCH 19/84] Fix: Item color names/glint for rare items (#4763) * debug stick changes * yay light purple item name * Address review * Auto-generate rarity * Add glint generation * Translate all rare items properly * Use putIfAbsent instead of double checks --- .../java/org/geysermc/geyser/item/Items.java | 121 +++++++++--------- .../geyser/item/components/Rarity.java | 51 ++++++++ .../org/geysermc/geyser/item/type/Item.java | 25 ++++ .../populator/ItemRegistryPopulator.java | 4 +- .../translator/item/ItemTranslator.java | 34 +++-- 5 files changed, 163 insertions(+), 72 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/item/components/Rarity.java diff --git a/core/src/main/java/org/geysermc/geyser/item/Items.java b/core/src/main/java/org/geysermc/geyser/item/Items.java index 1ddd14982..462e98d19 100644 --- a/core/src/main/java/org/geysermc/geyser/item/Items.java +++ b/core/src/main/java/org/geysermc/geyser/item/Items.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.item; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.components.ToolTier; import org.geysermc.geyser.item.type.*; import org.geysermc.geyser.level.block.Blocks; @@ -122,7 +123,7 @@ public final class Items { public static final Item RAW_IRON_BLOCK = register(new BlockItem(builder(), Blocks.RAW_IRON_BLOCK)); public static final Item RAW_COPPER_BLOCK = register(new BlockItem(builder(), Blocks.RAW_COPPER_BLOCK)); public static final Item RAW_GOLD_BLOCK = register(new BlockItem(builder(), Blocks.RAW_GOLD_BLOCK)); - public static final Item HEAVY_CORE = register(new BlockItem(builder(), Blocks.HEAVY_CORE)); + public static final Item HEAVY_CORE = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.HEAVY_CORE)); public static final Item AMETHYST_BLOCK = register(new BlockItem(builder(), Blocks.AMETHYST_BLOCK)); public static final Item BUDDING_AMETHYST = register(new BlockItem(builder(), Blocks.BUDDING_AMETHYST)); public static final Item IRON_BLOCK = register(new BlockItem(builder(), Blocks.IRON_BLOCK)); @@ -416,7 +417,7 @@ public final class Items { public static final Item END_PORTAL_FRAME = register(new BlockItem(builder(), Blocks.END_PORTAL_FRAME)); public static final Item END_STONE = register(new BlockItem(builder(), Blocks.END_STONE)); public static final Item END_STONE_BRICKS = register(new BlockItem(builder(), Blocks.END_STONE_BRICKS)); - public static final Item DRAGON_EGG = register(new BlockItem(builder(), Blocks.DRAGON_EGG)); + public static final Item DRAGON_EGG = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.DRAGON_EGG)); public static final Item SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SANDSTONE_STAIRS)); public static final Item ENDER_CHEST = register(new BlockItem(builder(), Blocks.ENDER_CHEST)); public static final Item EMERALD_BLOCK = register(new BlockItem(builder(), Blocks.EMERALD_BLOCK)); @@ -432,8 +433,8 @@ public final class Items { public static final Item BAMBOO_MOSAIC_STAIRS = register(new BlockItem(builder(), Blocks.BAMBOO_MOSAIC_STAIRS)); public static final Item CRIMSON_STAIRS = register(new BlockItem(builder(), Blocks.CRIMSON_STAIRS)); public static final Item WARPED_STAIRS = register(new BlockItem(builder(), Blocks.WARPED_STAIRS)); - public static final Item COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.COMMAND_BLOCK)); - public static final Item BEACON = register(new BlockItem(builder(), Blocks.BEACON)); + public static final Item COMMAND_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.COMMAND_BLOCK)); + public static final Item BEACON = register(new BlockItem(builder().rarity(Rarity.RARE), Blocks.BEACON)); public static final Item COBBLESTONE_WALL = register(new BlockItem(builder(), Blocks.COBBLESTONE_WALL)); public static final Item MOSSY_COBBLESTONE_WALL = register(new BlockItem(builder(), Blocks.MOSSY_COBBLESTONE_WALL)); public static final Item BRICK_WALL = register(new BlockItem(builder(), Blocks.BRICK_WALL)); @@ -480,8 +481,8 @@ public final class Items { public static final Item GREEN_TERRACOTTA = register(new BlockItem(builder(), Blocks.GREEN_TERRACOTTA)); public static final Item RED_TERRACOTTA = register(new BlockItem(builder(), Blocks.RED_TERRACOTTA)); public static final Item BLACK_TERRACOTTA = register(new BlockItem(builder(), Blocks.BLACK_TERRACOTTA)); - public static final Item BARRIER = register(new BlockItem(builder(), Blocks.BARRIER)); - public static final Item LIGHT = register(new BlockItem(builder(), Blocks.LIGHT)); + public static final Item BARRIER = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.BARRIER)); + public static final Item LIGHT = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.LIGHT)); public static final Item HAY_BLOCK = register(new BlockItem(builder(), Blocks.HAY_BLOCK)); public static final Item WHITE_CARPET = register(new BlockItem(builder(), Blocks.WHITE_CARPET)); public static final Item ORANGE_CARPET = register(new BlockItem(builder(), Blocks.ORANGE_CARPET)); @@ -551,14 +552,14 @@ public final class Items { public static final Item CHISELED_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.CHISELED_RED_SANDSTONE)); public static final Item CUT_RED_SANDSTONE = register(new BlockItem(builder(), Blocks.CUT_RED_SANDSTONE)); public static final Item RED_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.RED_SANDSTONE_STAIRS)); - public static final Item REPEATING_COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.REPEATING_COMMAND_BLOCK)); - public static final Item CHAIN_COMMAND_BLOCK = register(new BlockItem(builder(), Blocks.CHAIN_COMMAND_BLOCK)); + public static final Item REPEATING_COMMAND_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.REPEATING_COMMAND_BLOCK)); + public static final Item CHAIN_COMMAND_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.CHAIN_COMMAND_BLOCK)); public static final Item MAGMA_BLOCK = register(new BlockItem(builder(), Blocks.MAGMA_BLOCK)); public static final Item NETHER_WART_BLOCK = register(new BlockItem(builder(), Blocks.NETHER_WART_BLOCK)); public static final Item WARPED_WART_BLOCK = register(new BlockItem(builder(), Blocks.WARPED_WART_BLOCK)); public static final Item RED_NETHER_BRICKS = register(new BlockItem(builder(), Blocks.RED_NETHER_BRICKS)); public static final Item BONE_BLOCK = register(new BlockItem(builder(), Blocks.BONE_BLOCK)); - public static final Item STRUCTURE_VOID = register(new BlockItem(builder(), Blocks.STRUCTURE_VOID)); + public static final Item STRUCTURE_VOID = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.STRUCTURE_VOID)); public static final Item SHULKER_BOX = register(new ShulkerBoxItem(builder().stackSize(1), Blocks.SHULKER_BOX)); public static final Item WHITE_SHULKER_BOX = register(new ShulkerBoxItem(builder().stackSize(1), Blocks.WHITE_SHULKER_BOX)); public static final Item ORANGE_SHULKER_BOX = register(new ShulkerBoxItem(builder().stackSize(1), Blocks.ORANGE_SHULKER_BOX)); @@ -657,7 +658,7 @@ public final class Items { public static final Item DEAD_FIRE_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_FIRE_CORAL_FAN, Blocks.DEAD_FIRE_CORAL_WALL_FAN)); public static final Item DEAD_HORN_CORAL_FAN = register(new BlockItem(builder(), Blocks.DEAD_HORN_CORAL_FAN, Blocks.DEAD_HORN_CORAL_WALL_FAN)); public static final Item BLUE_ICE = register(new BlockItem(builder(), Blocks.BLUE_ICE)); - public static final Item CONDUIT = register(new BlockItem(builder(), Blocks.CONDUIT)); + public static final Item CONDUIT = register(new BlockItem(builder().rarity(Rarity.RARE), Blocks.CONDUIT)); public static final Item POLISHED_GRANITE_STAIRS = register(new BlockItem(builder(), Blocks.POLISHED_GRANITE_STAIRS)); public static final Item SMOOTH_RED_SANDSTONE_STAIRS = register(new BlockItem(builder(), Blocks.SMOOTH_RED_SANDSTONE_STAIRS)); public static final Item MOSSY_STONE_BRICK_STAIRS = register(new BlockItem(builder(), Blocks.MOSSY_STONE_BRICK_STAIRS)); @@ -810,7 +811,7 @@ public final class Items { public static final Item HOPPER_MINECART = register(new Item("hopper_minecart", builder().stackSize(1))); public static final Item CARROT_ON_A_STICK = register(new Item("carrot_on_a_stick", builder().stackSize(1).maxDamage(25))); public static final Item WARPED_FUNGUS_ON_A_STICK = register(new Item("warped_fungus_on_a_stick", builder().stackSize(1).maxDamage(100))); - public static final Item ELYTRA = register(new ElytraItem("elytra", builder().stackSize(1).maxDamage(432))); + public static final Item ELYTRA = register(new ElytraItem("elytra", builder().stackSize(1).maxDamage(432).rarity(Rarity.UNCOMMON))); public static final Item OAK_BOAT = register(new BoatItem("oak_boat", builder().stackSize(1))); public static final Item OAK_CHEST_BOAT = register(new BoatItem("oak_chest_boat", builder().stackSize(1))); public static final Item SPRUCE_BOAT = register(new BoatItem("spruce_boat", builder().stackSize(1))); @@ -829,8 +830,8 @@ public final class Items { public static final Item MANGROVE_CHEST_BOAT = register(new BoatItem("mangrove_chest_boat", builder().stackSize(1))); public static final Item BAMBOO_RAFT = register(new BoatItem("bamboo_raft", builder().stackSize(1))); public static final Item BAMBOO_CHEST_RAFT = register(new BoatItem("bamboo_chest_raft", builder().stackSize(1))); - public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder(), Blocks.STRUCTURE_BLOCK)); - public static final Item JIGSAW = register(new BlockItem(builder(), Blocks.JIGSAW)); + public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.STRUCTURE_BLOCK)); + public static final Item JIGSAW = register(new BlockItem(builder().rarity(Rarity.EPIC), Blocks.JIGSAW)); public static final Item TURTLE_HELMET = register(new ArmorItem("turtle_helmet", ArmorMaterial.TURTLE, builder().stackSize(1).maxDamage(275))); public static final Item TURTLE_SCUTE = register(new Item("turtle_scute", builder())); public static final Item ARMADILLO_SCUTE = register(new Item("armadillo_scute", builder())); @@ -921,8 +922,8 @@ public final class Items { public static final Item PORKCHOP = register(new Item("porkchop", builder())); public static final Item COOKED_PORKCHOP = register(new Item("cooked_porkchop", builder())); public static final Item PAINTING = register(new Item("painting", builder())); - public static final Item GOLDEN_APPLE = register(new Item("golden_apple", builder())); - public static final Item ENCHANTED_GOLDEN_APPLE = register(new Item("enchanted_golden_apple", builder())); + public static final Item GOLDEN_APPLE = register(new Item("golden_apple", builder().rarity(Rarity.RARE))); + public static final Item ENCHANTED_GOLDEN_APPLE = register(new Item("enchanted_golden_apple", builder().rarity(Rarity.EPIC))); public static final Item OAK_SIGN = register(new BlockItem(builder().stackSize(16), Blocks.OAK_SIGN, Blocks.OAK_WALL_SIGN)); public static final Item SPRUCE_SIGN = register(new BlockItem(builder().stackSize(16), Blocks.SPRUCE_SIGN, Blocks.SPRUCE_WALL_SIGN)); public static final Item BIRCH_SIGN = register(new BlockItem(builder().stackSize(16), Blocks.BIRCH_SIGN, Blocks.BIRCH_WALL_SIGN)); @@ -1042,7 +1043,7 @@ public final class Items { public static final Item BLAZE_POWDER = register(new Item("blaze_powder", builder())); public static final Item MAGMA_CREAM = register(new Item("magma_cream", builder())); public static final Item BREWING_STAND = register(new BlockItem(builder(), Blocks.BREWING_STAND)); - public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.LAVA_CAULDRON, Blocks.POWDER_SNOW_CAULDRON)); + public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.LAVA_CAULDRON)); public static final Item ENDER_EYE = register(new Item("ender_eye", builder())); public static final Item GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder())); public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", builder())); @@ -1125,12 +1126,12 @@ public final class Items { public static final Item ZOMBIE_HORSE_SPAWN_EGG = register(new SpawnEggItem("zombie_horse_spawn_egg", builder())); public static final Item ZOMBIE_VILLAGER_SPAWN_EGG = register(new SpawnEggItem("zombie_villager_spawn_egg", builder())); public static final Item ZOMBIFIED_PIGLIN_SPAWN_EGG = register(new SpawnEggItem("zombified_piglin_spawn_egg", builder())); - public static final Item EXPERIENCE_BOTTLE = register(new Item("experience_bottle", builder())); + public static final Item EXPERIENCE_BOTTLE = register(new Item("experience_bottle", builder().rarity(Rarity.UNCOMMON))); public static final Item FIRE_CHARGE = register(new Item("fire_charge", builder())); public static final Item WIND_CHARGE = register(new Item("wind_charge", builder())); public static final Item WRITABLE_BOOK = register(new WritableBookItem("writable_book", builder().stackSize(1))); public static final Item WRITTEN_BOOK = register(new WrittenBookItem("written_book", builder().stackSize(16))); - public static final Item MACE = register(new MaceItem("mace", builder().stackSize(1).maxDamage(500))); + public static final Item MACE = register(new MaceItem("mace", builder().stackSize(1).maxDamage(500).rarity(Rarity.EPIC))); public static final Item ITEM_FRAME = register(new Item("item_frame", builder())); public static final Item GLOW_ITEM_FRAME = register(new Item("glow_item_frame", builder())); public static final Item FLOWER_POT = register(new BlockItem(builder(), Blocks.FLOWER_POT)); @@ -1140,18 +1141,18 @@ public final class Items { public static final Item POISONOUS_POTATO = register(new Item("poisonous_potato", builder())); public static final Item MAP = register(new MapItem("map", builder())); public static final Item GOLDEN_CARROT = register(new Item("golden_carrot", builder())); - public static final Item SKELETON_SKULL = register(new BlockItem(builder(), Blocks.SKELETON_SKULL, Blocks.SKELETON_WALL_SKULL)); - public static final Item WITHER_SKELETON_SKULL = register(new BlockItem(builder(), Blocks.WITHER_SKELETON_SKULL, Blocks.WITHER_SKELETON_WALL_SKULL)); - public static final Item PLAYER_HEAD = register(new PlayerHeadItem(builder(), Blocks.PLAYER_HEAD, Blocks.PLAYER_WALL_HEAD)); - public static final Item ZOMBIE_HEAD = register(new BlockItem(builder(), Blocks.ZOMBIE_HEAD, Blocks.ZOMBIE_WALL_HEAD)); - public static final Item CREEPER_HEAD = register(new BlockItem(builder(), Blocks.CREEPER_HEAD, Blocks.CREEPER_WALL_HEAD)); - public static final Item DRAGON_HEAD = register(new BlockItem(builder(), Blocks.DRAGON_HEAD, Blocks.DRAGON_WALL_HEAD)); - public static final Item PIGLIN_HEAD = register(new BlockItem(builder(), Blocks.PIGLIN_HEAD, Blocks.PIGLIN_WALL_HEAD)); - public static final Item NETHER_STAR = register(new Item("nether_star", builder())); + public static final Item SKELETON_SKULL = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.SKELETON_SKULL, Blocks.SKELETON_WALL_SKULL)); + public static final Item WITHER_SKELETON_SKULL = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.WITHER_SKELETON_SKULL, Blocks.WITHER_SKELETON_WALL_SKULL)); + public static final Item PLAYER_HEAD = register(new PlayerHeadItem(builder().rarity(Rarity.UNCOMMON), Blocks.PLAYER_HEAD, Blocks.PLAYER_WALL_HEAD)); + public static final Item ZOMBIE_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.ZOMBIE_HEAD, Blocks.ZOMBIE_WALL_HEAD)); + public static final Item CREEPER_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.CREEPER_HEAD, Blocks.CREEPER_WALL_HEAD)); + public static final Item DRAGON_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.DRAGON_HEAD, Blocks.DRAGON_WALL_HEAD)); + public static final Item PIGLIN_HEAD = register(new BlockItem(builder().rarity(Rarity.UNCOMMON), Blocks.PIGLIN_HEAD, Blocks.PIGLIN_WALL_HEAD)); + public static final Item NETHER_STAR = register(new Item("nether_star", builder().rarity(Rarity.UNCOMMON))); public static final Item PUMPKIN_PIE = register(new Item("pumpkin_pie", builder())); public static final Item FIREWORK_ROCKET = register(new FireworkRocketItem("firework_rocket", builder())); public static final Item FIREWORK_STAR = register(new FireworkStarItem("firework_star", builder())); - public static final Item ENCHANTED_BOOK = register(new EnchantedBookItem("enchanted_book", builder().stackSize(1))); + public static final Item ENCHANTED_BOOK = register(new EnchantedBookItem("enchanted_book", builder().stackSize(1).rarity(Rarity.UNCOMMON))); public static final Item NETHER_BRICK = register(new Item("nether_brick", builder())); public static final Item PRISMARINE_SHARD = register(new Item("prismarine_shard", builder())); public static final Item PRISMARINE_CRYSTALS = register(new Item("prismarine_crystals", builder())); @@ -1167,7 +1168,7 @@ public final class Items { public static final Item LEATHER_HORSE_ARMOR = register(new DyeableArmorItem("leather_horse_armor", ArmorMaterial.LEATHER, builder().stackSize(1))); public static final Item LEAD = register(new Item("lead", builder())); public static final Item NAME_TAG = register(new Item("name_tag", builder())); - public static final Item COMMAND_BLOCK_MINECART = register(new Item("command_block_minecart", builder().stackSize(1))); + public static final Item COMMAND_BLOCK_MINECART = register(new Item("command_block_minecart", builder().stackSize(1).rarity(Rarity.EPIC))); public static final Item MUTTON = register(new Item("mutton", builder())); public static final Item COOKED_MUTTON = register(new Item("cooked_mutton", builder())); public static final Item WHITE_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.WHITE_BANNER, Blocks.WHITE_WALL_BANNER)); @@ -1186,7 +1187,7 @@ public final class Items { public static final Item GREEN_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.GREEN_BANNER, Blocks.GREEN_WALL_BANNER)); public static final Item RED_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.RED_BANNER, Blocks.RED_WALL_BANNER)); public static final Item BLACK_BANNER = register(new BannerItem(builder().stackSize(16), Blocks.BLACK_BANNER, Blocks.BLACK_WALL_BANNER)); - public static final Item END_CRYSTAL = register(new Item("end_crystal", builder())); + public static final Item END_CRYSTAL = register(new Item("end_crystal", builder().rarity(Rarity.RARE))); public static final Item CHORUS_FRUIT = register(new Item("chorus_fruit", builder())); public static final Item POPPED_CHORUS_FRUIT = register(new Item("popped_chorus_fruit", builder())); public static final Item TORCHFLOWER_SEEDS = register(new BlockItem("torchflower_seeds", builder(), Blocks.TORCHFLOWER_CROP)); @@ -1194,52 +1195,52 @@ public final class Items { public static final Item BEETROOT = register(new Item("beetroot", builder())); public static final Item BEETROOT_SEEDS = register(new BlockItem("beetroot_seeds", builder(), Blocks.BEETROOTS)); public static final Item BEETROOT_SOUP = register(new Item("beetroot_soup", builder().stackSize(1))); - public static final Item DRAGON_BREATH = register(new Item("dragon_breath", builder())); + public static final Item DRAGON_BREATH = register(new Item("dragon_breath", builder().rarity(Rarity.UNCOMMON))); public static final Item SPLASH_POTION = register(new PotionItem("splash_potion", builder().stackSize(1))); public static final Item SPECTRAL_ARROW = register(new Item("spectral_arrow", builder())); public static final Item TIPPED_ARROW = register(new TippedArrowItem("tipped_arrow", builder())); public static final Item LINGERING_POTION = register(new PotionItem("lingering_potion", builder().stackSize(1))); public static final Item SHIELD = register(new ShieldItem("shield", builder().stackSize(1).maxDamage(336))); - public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder().stackSize(1))); + public static final Item TOTEM_OF_UNDYING = register(new Item("totem_of_undying", builder().stackSize(1).rarity(Rarity.UNCOMMON))); public static final Item SHULKER_SHELL = register(new Item("shulker_shell", builder())); public static final Item IRON_NUGGET = register(new Item("iron_nugget", builder())); - public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder().stackSize(1))); - public static final Item DEBUG_STICK = register(new Item("debug_stick", builder().stackSize(1))); - public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder().stackSize(1))); - public static final Item MUSIC_DISC_CAT = register(new Item("music_disc_cat", builder().stackSize(1))); - public static final Item MUSIC_DISC_BLOCKS = register(new Item("music_disc_blocks", builder().stackSize(1))); - public static final Item MUSIC_DISC_CHIRP = register(new Item("music_disc_chirp", builder().stackSize(1))); - public static final Item MUSIC_DISC_CREATOR = register(new Item("music_disc_creator", builder().stackSize(1))); - public static final Item MUSIC_DISC_CREATOR_MUSIC_BOX = register(new Item("music_disc_creator_music_box", builder().stackSize(1))); - public static final Item MUSIC_DISC_FAR = register(new Item("music_disc_far", builder().stackSize(1))); - public static final Item MUSIC_DISC_MALL = register(new Item("music_disc_mall", builder().stackSize(1))); - public static final Item MUSIC_DISC_MELLOHI = register(new Item("music_disc_mellohi", builder().stackSize(1))); - public static final Item MUSIC_DISC_STAL = register(new Item("music_disc_stal", builder().stackSize(1))); - public static final Item MUSIC_DISC_STRAD = register(new Item("music_disc_strad", builder().stackSize(1))); - public static final Item MUSIC_DISC_WARD = register(new Item("music_disc_ward", builder().stackSize(1))); - public static final Item MUSIC_DISC_11 = register(new Item("music_disc_11", builder().stackSize(1))); - public static final Item MUSIC_DISC_WAIT = register(new Item("music_disc_wait", builder().stackSize(1))); - public static final Item MUSIC_DISC_OTHERSIDE = register(new Item("music_disc_otherside", builder().stackSize(1))); - public static final Item MUSIC_DISC_RELIC = register(new Item("music_disc_relic", builder().stackSize(1))); - public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder().stackSize(1))); - public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder().stackSize(1))); - public static final Item MUSIC_DISC_PRECIPICE = register(new Item("music_disc_precipice", builder().stackSize(1))); + public static final Item KNOWLEDGE_BOOK = register(new Item("knowledge_book", builder().stackSize(1).rarity(Rarity.EPIC))); + public static final Item DEBUG_STICK = register(new Item("debug_stick", builder().stackSize(1).rarity(Rarity.EPIC).glint(true))); + public static final Item MUSIC_DISC_13 = register(new Item("music_disc_13", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CAT = register(new Item("music_disc_cat", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_BLOCKS = register(new Item("music_disc_blocks", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CHIRP = register(new Item("music_disc_chirp", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CREATOR = register(new Item("music_disc_creator", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_CREATOR_MUSIC_BOX = register(new Item("music_disc_creator_music_box", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_FAR = register(new Item("music_disc_far", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_MALL = register(new Item("music_disc_mall", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_MELLOHI = register(new Item("music_disc_mellohi", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_STAL = register(new Item("music_disc_stal", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_STRAD = register(new Item("music_disc_strad", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_WARD = register(new Item("music_disc_ward", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_11 = register(new Item("music_disc_11", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_WAIT = register(new Item("music_disc_wait", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_OTHERSIDE = register(new Item("music_disc_otherside", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_RELIC = register(new Item("music_disc_relic", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item MUSIC_DISC_PRECIPICE = register(new Item("music_disc_precipice", builder().stackSize(1).rarity(Rarity.RARE))); public static final Item DISC_FRAGMENT_5 = register(new Item("disc_fragment_5", builder())); - public static final Item TRIDENT = register(new Item("trident", builder().stackSize(1).maxDamage(250).attackDamage(9.0))); + public static final Item TRIDENT = register(new Item("trident", builder().stackSize(1).maxDamage(250).attackDamage(9.0).rarity(Rarity.EPIC))); public static final Item PHANTOM_MEMBRANE = register(new Item("phantom_membrane", builder())); public static final Item NAUTILUS_SHELL = register(new Item("nautilus_shell", builder())); - public static final Item HEART_OF_THE_SEA = register(new Item("heart_of_the_sea", builder())); + public static final Item HEART_OF_THE_SEA = register(new Item("heart_of_the_sea", builder().rarity(Rarity.UNCOMMON))); public static final Item CROSSBOW = register(new CrossbowItem("crossbow", builder().stackSize(1).maxDamage(465))); public static final Item SUSPICIOUS_STEW = register(new Item("suspicious_stew", builder().stackSize(1))); public static final Item LOOM = register(new BlockItem(builder(), Blocks.LOOM)); public static final Item FLOWER_BANNER_PATTERN = register(new Item("flower_banner_pattern", builder().stackSize(1))); - public static final Item CREEPER_BANNER_PATTERN = register(new Item("creeper_banner_pattern", builder().stackSize(1))); - public static final Item SKULL_BANNER_PATTERN = register(new Item("skull_banner_pattern", builder().stackSize(1))); - public static final Item MOJANG_BANNER_PATTERN = register(new Item("mojang_banner_pattern", builder().stackSize(1))); + public static final Item CREEPER_BANNER_PATTERN = register(new Item("creeper_banner_pattern", builder().stackSize(1).rarity(Rarity.UNCOMMON))); + public static final Item SKULL_BANNER_PATTERN = register(new Item("skull_banner_pattern", builder().stackSize(1).rarity(Rarity.UNCOMMON))); + public static final Item MOJANG_BANNER_PATTERN = register(new Item("mojang_banner_pattern", builder().stackSize(1).rarity(Rarity.EPIC))); public static final Item GLOBE_BANNER_PATTERN = register(new Item("globe_banner_pattern", builder().stackSize(1))); - public static final Item PIGLIN_BANNER_PATTERN = register(new Item("piglin_banner_pattern", builder().stackSize(1))); - public static final Item FLOW_BANNER_PATTERN = register(new Item("flow_banner_pattern", builder().stackSize(1))); - public static final Item GUSTER_BANNER_PATTERN = register(new Item("guster_banner_pattern", builder().stackSize(1))); + public static final Item PIGLIN_BANNER_PATTERN = register(new Item("piglin_banner_pattern", builder().stackSize(1).rarity(Rarity.UNCOMMON))); + public static final Item FLOW_BANNER_PATTERN = register(new Item("flow_banner_pattern", builder().stackSize(1).rarity(Rarity.RARE))); + public static final Item GUSTER_BANNER_PATTERN = register(new Item("guster_banner_pattern", builder().stackSize(1).rarity(Rarity.RARE))); public static final Item GOAT_HORN = register(new GoatHornItem("goat_horn", builder().stackSize(1))); public static final Item COMPOSTER = register(new BlockItem(builder(), Blocks.COMPOSTER)); public static final Item BARREL = register(new BlockItem(builder(), Blocks.BARREL)); diff --git a/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java b/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java new file mode 100644 index 000000000..6fa74ea35 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/components/Rarity.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 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.geyser.item.components; + +import lombok.Getter; + +@Getter +public enum Rarity { + COMMON("common", 'f'), + UNCOMMON("uncommon", 'e'), + RARE("rare", 'b'), + EPIC("epic", 'd'); + + private final String name; + private final char color; + + Rarity(final String name, char chatColor) { + this.name = name; + this.color = chatColor; + } + + private static final Rarity[] VALUES = values(); + + public static Rarity fromId(int id) { + return VALUES.length > id ? VALUES[id] : VALUES[0]; + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 362b760c7..57538565a 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -35,6 +35,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; @@ -63,12 +64,16 @@ public class Item { private final int stackSize; private final int attackDamage; private final int maxDamage; + private final Rarity rarity; + private final boolean glint; public Item(String javaIdentifier, Builder builder) { this.javaIdentifier = MinecraftKey.key(javaIdentifier).asString().intern(); this.stackSize = builder.stackSize; this.maxDamage = builder.maxDamage; this.attackDamage = builder.attackDamage; + this.rarity = builder.rarity; + this.glint = builder.glint; } public String javaIdentifier() { @@ -91,6 +96,14 @@ public class Item { return stackSize; } + public Rarity rarity() { + return rarity; + } + + public boolean glint() { + return glint; + } + public boolean isValidRepairItem(Item other) { return false; } @@ -275,6 +288,8 @@ public class Item { private int stackSize = 64; private int maxDamage; private int attackDamage; + private Rarity rarity = Rarity.COMMON; + private boolean glint = false; public Builder stackSize(int stackSize) { this.stackSize = stackSize; @@ -292,6 +307,16 @@ public class Item { return this; } + public Builder rarity(Rarity rarity) { + this.rarity = rarity; + return this; + } + + public Builder glint(boolean glintOverride) { + this.glint = glintOverride; + return this; + } + private Builder() { } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 85207ac3a..2d26d14f8 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -58,6 +58,7 @@ import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.GeyserCustomMappingData; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -399,8 +400,9 @@ public class ItemRegistryPopulator { } } - if (javaOnlyItems.contains(javaItem)) { + if (javaOnlyItems.contains(javaItem) || javaItem.rarity() != Rarity.COMMON) { // These items don't exist on Bedrock, so set up a variable that indicates they should have custom names + // Or, ensure that we are translating these at all times to account for rarity colouring mappingBuilder = mappingBuilder.translationString((bedrockBlock != null ? "block." : "item.") + entry.getKey().replace(":", ".")); GeyserImpl.getInstance().getLogger().debug("Adding " + entry.getKey() + " as an item that needs to be translated."); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index abe39f177..6a781dcb8 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -33,7 +33,9 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -41,6 +43,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.BlockRegistries; @@ -145,7 +148,20 @@ public final class ItemTranslator { if (components.get(DataComponentType.HIDE_TOOLTIP) != null) hideTooltips = true; } - String customName = getCustomName(session, components, bedrockItem); + Rarity rarity = javaItem.rarity(); + boolean enchantmentGlint = javaItem.glint(); + if (components != null) { + Integer rarityIndex = components.get(DataComponentType.RARITY); + if (rarityIndex != null) { + rarity = Rarity.fromId(rarityIndex); + } + Boolean enchantmentGlintOverride = components.get(DataComponentType.ENCHANTMENT_GLINT_OVERRIDE); + if (enchantmentGlintOverride != null) { + enchantmentGlint = enchantmentGlintOverride; + } + } + + String customName = getCustomName(session, components, bedrockItem, rarity.getColor()); if (customName != null) { nbtBuilder.setCustomName(customName); } @@ -162,6 +178,12 @@ public final class ItemTranslator { addAdvancedTooltips(components, nbtBuilder, javaItem, session.locale()); } + // Add enchantment override. We can't remove it - enchantments would stop showing - but we can add it. + if (enchantmentGlint) { + NbtMapBuilder nbtMapBuilder = nbtBuilder.getOrCreateNbt(); + nbtMapBuilder.putIfAbsent("ench", NbtList.EMPTY); + } + ItemData.Builder builder = javaItem.translateToBedrock(count, components, bedrockItem, session.getItemMappings()); // Finalize the Bedrock NBT builder.tag(nbtBuilder.build()); @@ -401,16 +423,6 @@ public final class ItemTranslator { } } - /** - * Translates the display name of the item - * @param session the Bedrock client's session - * @param components the components to translate - * @param mapping the item entry, in case it requires translation - */ - public static String getCustomName(GeyserSession session, DataComponents components, ItemMapping mapping) { - return getCustomName(session, components, mapping, 'f'); - } - /** * @param translationColor if this item is not available on Java, the color that the new name should be. * Normally, this should just be white, but for shulker boxes this should be gray. From 6e0bad3c40605c4ac2f3cbac7c6af673f679ead5 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 13 Jul 2024 15:11:22 -0400 Subject: [PATCH 20/84] Fix #4837 by not hardcoding dimension IDs --- .../living/monster/EnderDragonEntity.java | 8 +- .../geysermc/geyser/level/JavaDimension.java | 22 ++- .../geyser/session/GeyserSession.java | 2 - .../protocol/java/JavaLoginTranslator.java | 17 +- .../protocol/java/JavaRespawnTranslator.java | 11 +- .../java/entity/JavaAnimateTranslator.java | 2 +- .../java/level/JavaLevelEventTranslator.java | 3 +- .../level/JavaLevelParticlesTranslator.java | 2 +- .../java/level/JavaMapItemDataTranslator.java | 2 +- ...JavaSetDefaultSpawnPositionTranslator.java | 2 +- .../org/geysermc/geyser/util/ChunkUtils.java | 5 +- .../geysermc/geyser/util/DimensionUtils.java | 157 ++++++++++-------- 12 files changed, 135 insertions(+), 98 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java index 0162d498e..04044fcb4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java @@ -30,7 +30,11 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; +import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; +import org.cloudburstmc.protocol.bedrock.packet.SpawnParticleEffectPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.living.MobEntity; @@ -260,7 +264,7 @@ public class EnderDragonEntity extends MobEntity implements Tickable { // 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.setDimensionId(DimensionUtils.javaToBedrock(session)); spawnParticleEffectPacket.setPosition(head.getPosition().add(random.nextGaussian() / 2f, random.nextGaussian() / 2f, random.nextGaussian() / 2f)); spawnParticleEffectPacket.setIdentifier("minecraft:dragon_breath_fire"); spawnParticleEffectPacket.setMolangVariablesJson(Optional.empty()); diff --git a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java index dd0f4215e..7462844fc 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java +++ b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java @@ -25,15 +25,17 @@ package org.geysermc.geyser.level; +import net.kyori.adventure.key.Key; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.util.DimensionUtils; /** * Represents the information we store from the current Java dimension * @param piglinSafe Whether piglins and hoglins are safe from conversion in this dimension. * This controls if they have the shaking effect applied in the dimension. */ -public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale) { +public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale, int bedrockId, boolean isNetherLike) { public static JavaDimension read(RegistryEntryContext entry) { NbtMap dimension = entry.data(); @@ -46,6 +48,22 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double world // Load world coordinate scale for the world border double coordinateScale = dimension.getDouble("coordinate_scale"); - return new JavaDimension(minY, maxY, piglinSafe, coordinateScale); + boolean isNetherLike; + // Cache the Bedrock version of this dimension, and base it off the ID - THE ID CAN CHANGE!!! + // https://github.com/GeyserMC/Geyser/issues/4837 + int bedrockId; + Key id = entry.id(); + if ("minecraft".equals(id.namespace())) { + String identifier = id.asString(); + bedrockId = DimensionUtils.javaToBedrock(identifier); + isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(identifier); + } else { + // Effects should give is a clue on how this (custom) dimension is supposed to look like + String effects = dimension.getString("effects"); + bedrockId = DimensionUtils.javaToBedrock(effects); + isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(effects); + } + + return new JavaDimension(minY, maxY, piglinSafe, coordinateScale, bedrockId, isNetherLike); } } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 25dd21662..3d47956b9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -312,8 +312,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * The dimension of the player. * As all entities are in the same world, this can be safely applied to all other entities. */ - @Setter - private int dimension = DimensionUtils.OVERWORLD; @MonotonicNonNull @Setter private JavaDimension dimensionType = null; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 6988d6cc8..1e885403b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.protocol.java; import net.kyori.adventure.key.Key; import org.geysermc.erosion.Constants; +import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerSpawnInfo; import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; @@ -65,12 +66,15 @@ public class JavaLoginTranslator extends PacketTranslator { SpawnParticleEffectPacket stringPacket = new SpawnParticleEffectPacket(); stringPacket.setIdentifier(particleMapping.identifier()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaMapItemDataTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaMapItemDataTranslator.java index 1591b4952..52a08ab29 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaMapItemDataTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaMapItemDataTranslator.java @@ -46,7 +46,7 @@ public class JavaMapItemDataTranslator extends PacketTranslator entityEffects = session.getEffectCache().getEntityEffects(); + for (Effect effect : entityEffects) { + MobEffectPacket mobEffectPacket = new MobEffectPacket(); + mobEffectPacket.setEvent(MobEffectPacket.Event.REMOVE); + mobEffectPacket.setRuntimeEntityId(player.getGeyserId()); + mobEffectPacket.setEffectId(EntityUtils.toBedrockEffectId(effect)); + session.sendUpstreamPacket(mobEffectPacket); + } + // Effects are re-sent from server + entityEffects.clear(); + + finalizeDimensionSwitch(session, player); + + // If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension, + // we check if the player is entering the nether and apply the nether fog to fake the fact that the client + // thinks they are in the end dimension. + if (isCustomBedrockNetherId()) { + if (javaDimension.isNetherLike()) { + session.camera().sendFog(BEDROCK_FOG_HELL); + } else if (previousDimension.isNetherLike()) { + session.camera().removeFog(BEDROCK_FOG_HELL); + } + } + } + + /** + * Switch dimensions without clearing internal logic. + */ + public static void fastSwitchDimension(GeyserSession session, int bedrockDimension) { + changeDimension(session, bedrockDimension); + finalizeDimensionSwitch(session, session.getPlayerEntity()); + } + + private static void changeDimension(GeyserSession session, int bedrockDimension) { if (session.getServerRenderDistance() > 32 && !session.isEmulatePost1_13Logic()) { // The server-sided view distance wasn't a thing until Minecraft Java 1.14 // So ViaVersion compensates by sending a "view distance" of 64 @@ -77,7 +116,7 @@ public class DimensionUtils { // To solve this, we cap at 32 unless we know that the render distance actually exceeds 32 // Also, as of 1.19: PS4 crashes with a ChunkRadiusUpdatedPacket too large session.getGeyser().getLogger().debug("Applying dimension switching workaround for Bedrock render distance of " - + session.getServerRenderDistance()); + + session.getServerRenderDistance()); ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket(); chunkRadiusUpdatedPacket.setRadius(32); session.sendUpstreamPacket(chunkRadiusUpdatedPacket); @@ -92,24 +131,14 @@ public class DimensionUtils { changeDimensionPacket.setPosition(pos); session.sendUpstreamPacket(changeDimensionPacket); - session.setDimension(javaDimension); - setBedrockDimension(session, javaDimension); + setBedrockDimension(session, bedrockDimension); - player.setPosition(pos); + session.getPlayerEntity().setPosition(pos); session.setSpawned(false); session.setLastChunkPosition(null); + } - Set entityEffects = session.getEffectCache().getEntityEffects(); - for (Effect effect : entityEffects) { - MobEffectPacket mobEffectPacket = new MobEffectPacket(); - mobEffectPacket.setEvent(MobEffectPacket.Event.REMOVE); - mobEffectPacket.setRuntimeEntityId(player.getGeyserId()); - mobEffectPacket.setEffectId(EntityUtils.toBedrockEffectId(effect)); - session.sendUpstreamPacket(mobEffectPacket); - } - // Effects are re-sent from server - entityEffects.clear(); - + private static void finalizeDimensionSwitch(GeyserSession session, Entity player) { //let java server handle portal travel sound StopSoundPacket stopSoundPacket = new StopSoundPacket(); stopSoundPacket.setStoppingAllSound(true); @@ -130,23 +159,12 @@ public class DimensionUtils { // TODO - fix this hack of a fix by sending the final dimension switching logic after sections have been sent. // The client wants sections sent to it before it can successfully respawn. ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); - - // If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension, - // we check if the player is entering the nether and apply the nether fog to fake the fact that the client - // thinks they are in the end dimension. - if (isCustomBedrockNetherId()) { - if (NETHER == javaDimension) { - session.camera().sendFog(BEDROCK_FOG_HELL); - } else if (NETHER == previousDimension) { - session.camera().removeFog(BEDROCK_FOG_HELL); - } - } } - public static void setBedrockDimension(GeyserSession session, int javaDimension) { - session.getChunkCache().setBedrockDimension(switch (javaDimension) { - case DimensionUtils.THE_END -> BedrockDimension.THE_END; - case DimensionUtils.NETHER -> DimensionUtils.isCustomBedrockNetherId() ? BedrockDimension.THE_END : BedrockDimension.THE_NETHER; + public static void setBedrockDimension(GeyserSession session, int bedrockDimension) { + session.getChunkCache().setBedrockDimension(switch (bedrockDimension) { + case BEDROCK_END_ID -> BedrockDimension.THE_END; + case BEDROCK_DEFAULT_NETHER_ID -> BedrockDimension.THE_NETHER; // JavaDimension *should* be set to BEDROCK_END_ID if the Nether workaround is enabled. default -> BedrockDimension.OVERWORLD; }); } @@ -155,26 +173,12 @@ public class DimensionUtils { if (dimension == BedrockDimension.THE_NETHER) { return BEDROCK_NETHER_ID; } else if (dimension == BedrockDimension.THE_END) { - return 2; + return BEDROCK_END_ID; } else { - return 0; + return BEDROCK_OVERWORLD_ID; } } - /** - * Map the Java edition dimension IDs to Bedrock edition - * - * @param javaDimension Dimension ID to convert - * @return Converted Bedrock edition dimension ID - */ - public static int javaToBedrock(int javaDimension) { - return switch (javaDimension) { - case NETHER -> BEDROCK_NETHER_ID; - case THE_END -> 2; - default -> 0; - }; - } - /** * Map the Java edition dimension IDs to Bedrock edition * @@ -183,12 +187,23 @@ public class DimensionUtils { */ public static int javaToBedrock(String javaDimension) { return switch (javaDimension) { - case "minecraft:the_nether" -> BEDROCK_NETHER_ID; + case NETHER_IDENTIFIER -> BEDROCK_NETHER_ID; case "minecraft:the_end" -> 2; default -> 0; }; } + /** + * Gets the Bedrock dimension ID, with a safety check if a packet is created before the player is logged/spawned in. + */ + public static int javaToBedrock(GeyserSession session) { + JavaDimension dimension = session.getDimensionType(); + if (dimension == null) { + return BEDROCK_OVERWORLD_ID; + } + return dimension.bedrockId(); + } + /** * The Nether dimension in Bedrock does not permit building above Y128 - the Bedrock above the dimension. * This workaround sets the Nether as the End dimension to ignore this limit. @@ -197,28 +212,28 @@ public class DimensionUtils { */ public static void changeBedrockNetherId(boolean isAboveNetherBedrockBuilding) { // Change dimension ID to the End to allow for building above Bedrock - BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? 2 : 1; + BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? BEDROCK_END_ID : BEDROCK_DEFAULT_NETHER_ID; } /** * 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 + * @param currentBedrockDimension the current dimension of the player + * @param newBedrockDimension the new dimension that the player will be transferred to + * @return the Bedrock fake dimension to transfer to */ - public static int getTemporaryDimension(int currentDimension, int newDimension) { + public static int getTemporaryDimension(int currentBedrockDimension, int newBedrockDimension) { if (isCustomBedrockNetherId()) { // Prevents rare instances of Bedrock locking up - return javaToBedrock(newDimension) == 2 ? OVERWORLD : NETHER; + return newBedrockDimension == BEDROCK_END_ID ? BEDROCK_OVERWORLD_ID : BEDROCK_END_ID; } // Check current Bedrock dimension and not just the Java dimension. // Fixes rare instances like https://github.com/GeyserMC/Geyser/issues/3161 - return javaToBedrock(currentDimension) == 0 ? NETHER : OVERWORLD; + return currentBedrockDimension == BEDROCK_OVERWORLD_ID ? BEDROCK_DEFAULT_NETHER_ID : BEDROCK_OVERWORLD_ID; } public static boolean isCustomBedrockNetherId() { - return BEDROCK_NETHER_ID == 2; + return BEDROCK_NETHER_ID == BEDROCK_END_ID; } } From 6ab0186fc9e7753fc15620d2a9eba57fc0b76ab0 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 13 Jul 2024 22:00:15 +0200 Subject: [PATCH 21/84] Fix: Manually translated item names for block items not showing up properly (#4857) --- .../geyser/registry/populator/ItemRegistryPopulator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 2d26d14f8..aad5e494d 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -59,6 +59,7 @@ import org.geysermc.geyser.inventory.item.StoredItemMappings; import org.geysermc.geyser.item.GeyserCustomMappingData; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.components.Rarity; +import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -403,7 +404,7 @@ public class ItemRegistryPopulator { if (javaOnlyItems.contains(javaItem) || javaItem.rarity() != Rarity.COMMON) { // These items don't exist on Bedrock, so set up a variable that indicates they should have custom names // Or, ensure that we are translating these at all times to account for rarity colouring - mappingBuilder = mappingBuilder.translationString((bedrockBlock != null ? "block." : "item.") + entry.getKey().replace(":", ".")); + mappingBuilder = mappingBuilder.translationString((javaItem instanceof BlockItem ? "block." : "item.") + entry.getKey().replace(":", ".")); GeyserImpl.getInstance().getLogger().debug("Adding " + entry.getKey() + " as an item that needs to be translated."); } From 49f66c2a022b67a48f300fa652062241046f5eb9 Mon Sep 17 00:00:00 2001 From: LetsGoAway <68365423+letsgoawaydev@users.noreply.github.com> Date: Sun, 14 Jul 2024 16:44:39 +0800 Subject: [PATCH 22/84] Correctly cap scale attribute (#4856) * Fix scale attribute cap * Update LivingEntity.java --- .../geysermc/geyser/entity/attribute/GeyserAttributeType.java | 2 +- .../java/org/geysermc/geyser/entity/type/LivingEntity.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java index f19912a8c..a4a0df8b8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java @@ -49,7 +49,7 @@ public enum GeyserAttributeType { ATTACK_KNOCKBACK("minecraft:generic.attack_knockback", null, 1.5f, Float.MAX_VALUE, 0f), ATTACK_SPEED("minecraft:generic.attack_speed", null, 0f, 1024f, 4f), MAX_HEALTH("minecraft:generic.max_health", null, 0f, 1024f, 20f), - SCALE("minecraft:generic.scale", null, 0.0625f, 16f, 1f), // Unused. Do we need this? + SCALE("minecraft:generic.scale", null, 0.0625f, 16f, 1f), // Bedrock Attributes ABSORPTION(null, "minecraft:absorption", 0f, 1024f, 0f), diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 499084555..2a1bc1188 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -48,6 +48,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; @@ -252,7 +253,7 @@ public class LivingEntity extends Entity { } private void setAttributeScale(float scale) { - this.attributeScale = scale; + this.attributeScale = MathUtils.clamp(scale, GeyserAttributeType.SCALE.getMinimum(), GeyserAttributeType.SCALE.getMaximum()); applyScale(); } From 9cdda707a31d299a24dcff8081fc306c69902e7b Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 14 Jul 2024 20:17:22 +0200 Subject: [PATCH 23/84] Fix: Send a container close packet to Java for containers that could not be opened (#4861) * Close containers if we did not manage to open it * Mark session inventory translator as nonnull --- .../java/org/geysermc/geyser/session/GeyserSession.java | 2 +- .../protocol/java/inventory/JavaOpenBookTranslator.java | 1 + .../main/java/org/geysermc/geyser/util/InventoryUtils.java | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 25dd21662..c2f94b1c6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -222,7 +222,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private boolean closingInventory; @Setter - private InventoryTranslator inventoryTranslator = InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR; + private @NonNull InventoryTranslator inventoryTranslator = InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR; /** * Use {@link #getNextItemNetId()} instead for consistency diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenBookTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenBookTranslator.java index e7cf21a69..172880725 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenBookTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenBookTranslator.java @@ -70,6 +70,7 @@ public class JavaOpenBookTranslator extends PacketTranslator { Inventory openInv = session.getOpenInventory(); @@ -110,7 +111,11 @@ public class InventoryUtils { inventory.setDisplayed(true); } } else { + // Can occur if we e.g. did not find a spot to put a fake container in + ServerboundContainerClosePacket closePacket = new ServerboundContainerClosePacket(inventory.getJavaId()); + session.sendDownstreamGamePacket(closePacket); session.setOpenInventory(null); + session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR); } } From 06890504a2bfcdbf218a9d2c2874fa1262d91cca Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 14 Jul 2024 21:55:57 +0200 Subject: [PATCH 24/84] Fix: Totem animation when playing totem effects manually (#4860) * Fix: Totem animation for manually played totem effects * Ensure we always reset the offhand correctly --- .../geyser/inventory/PlayerInventory.java | 5 ++++ .../inventory/item/StoredItemMappings.java | 2 ++ .../entity/JavaEntityEventTranslator.java | 27 ++++++++++++++++++- .../geysermc/geyser/util/InventoryUtils.java | 6 +++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java index 9bef4b08e..c3756d663 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java @@ -29,6 +29,7 @@ import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.jetbrains.annotations.Range; @@ -73,6 +74,10 @@ public class PlayerInventory extends Inventory { return items[36 + heldItemSlot]; } + public boolean eitherHandMatchesItem(@NonNull Item item) { + return getItemInHand().asItem() == item || getItemInHand(Hand.OFF_HAND).asItem() == item; + } + public void setItemInHand(@NonNull GeyserItemStack item) { if (36 + heldItemSlot > this.size) { GeyserImpl.getInstance().getLogger().debug("Held item slot was larger than expected!"); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index 05f6ba6cc..475a3e588 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -50,6 +50,7 @@ public class StoredItemMappings { private final ItemMapping milkBucket; private final ItemMapping powderSnowBucket; private final ItemMapping shield; + private final ItemMapping totem; private final ItemMapping upgradeTemplate; private final ItemMapping wheat; private final ItemMapping writableBook; @@ -66,6 +67,7 @@ public class StoredItemMappings { this.milkBucket = load(itemMappings, Items.MILK_BUCKET); this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET); this.shield = load(itemMappings, Items.SHIELD); + this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING); this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); this.wheat = load(itemMappings, Items.WHEAT); this.writableBook = load(itemMappings, Items.WRITABLE_BOOK); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java index e119d39ce..6c2e02cd3 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java @@ -29,7 +29,9 @@ import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; @@ -42,11 +44,15 @@ import org.geysermc.geyser.entity.type.FishingHookEntity; import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity; import org.geysermc.geyser.entity.type.living.monster.WardenEntity; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundEntityEventPacket; +import java.util.Collections; import java.util.concurrent.ThreadLocalRandom; @Translator(packet = ClientboundEntityEventPacket.class) @@ -154,6 +160,16 @@ public class JavaEntityEventTranslator extends PacketTranslator getTotemOfUndying() { + return protocolVersion -> ItemData.builder() + .definition(Registries.ITEMS.forVersion(protocolVersion).getStoredItems().totem().getBedrockDefinition()) + .count(1).build(); + } + /** * See {@link #findOrCreateItem(GeyserSession, String)}. This is for finding a specified {@link ItemStack}. * From efc8ba061028a2c67ea7120079fa8ce8c42ea701 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 15 Jul 2024 01:31:03 +0200 Subject: [PATCH 25/84] Fix: Block place sounds on mod platforms (#4859) --- .../mod/mixin/server/BlockPlaceMixin.java | 79 +++++++++++++++++++ .../mod/src/main/resources/geyser.mixins.json | 1 + .../java/level/JavaBlockUpdateTranslator.java | 20 ++--- 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java new file mode 100644 index 000000000..98620588e --- /dev/null +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/BlockPlaceMixin.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 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.geyser.platform.mod.mixin.server; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.SoundEvent; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(BlockItem.class) +public class BlockPlaceMixin { + + @Inject(method = "place", locals = LocalCapture.CAPTURE_FAILSOFT, at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;playSound(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/core/BlockPos;Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FF)V")) + private void geyser$hijackPlaySound(BlockPlaceContext blockPlaceContext, CallbackInfoReturnable cir, BlockPlaceContext blockPlaceContext2, BlockState blockState, BlockPos blockPos, Level level, Player player, ItemStack itemStack, BlockState blockState2, SoundType soundType) { + if (player == null) { + return; + } + + GeyserSession session = GeyserImpl.getInstance().connectionByUuid(player.getUUID()); + if (session == null) { + return; + } + + Vector3f position = Vector3f.from( + blockPos.getX(), + blockPos.getY(), + blockPos.getZ() + ); + + LevelSoundEventPacket placeBlockSoundPacket = new LevelSoundEventPacket(); + placeBlockSoundPacket.setSound(SoundEvent.PLACE); + placeBlockSoundPacket.setPosition(position); + placeBlockSoundPacket.setBabySound(false); + placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(Block.BLOCK_STATE_REGISTRY.getId(blockState2))); + placeBlockSoundPacket.setIdentifier(":"); + session.sendUpstreamPacket(placeBlockSoundPacket); + session.setLastBlockPlacePosition(null); + session.setLastBlockPlaced(null); + } +} diff --git a/bootstrap/mod/src/main/resources/geyser.mixins.json b/bootstrap/mod/src/main/resources/geyser.mixins.json index 47b2f60f3..2576e1ce6 100644 --- a/bootstrap/mod/src/main/resources/geyser.mixins.json +++ b/bootstrap/mod/src/main/resources/geyser.mixins.json @@ -4,6 +4,7 @@ "package": "org.geysermc.geyser.platform.mod.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ + "server.BlockPlaceMixin", "server.ServerConnectionListenerMixin" ], "server": [ diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java index d89775662..6d5fbc113 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockUpdateTranslator.java @@ -28,8 +28,8 @@ package org.geysermc.geyser.translator.protocol.java.level; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -43,24 +43,27 @@ public class JavaBlockUpdateTranslator extends PacketTranslator Date: Mon, 15 Jul 2024 15:16:45 -0400 Subject: [PATCH 26/84] Indicate support for Bedrock 1.21.3 --- README.md | 2 +- .../main/java/org/geysermc/geyser/network/GameProtocol.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14bdb17a9..9469b1bb6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,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 joined us here! -### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.2 and Minecraft Java 1.21 +### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.3 and Minecraft Java 1.21 ## Setting Up Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index c79ef365d..8f3f00021 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -73,7 +73,9 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v685.CODEC.toBuilder() .minecraftVersion("1.21.0/1.21.1") .build())); - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() + .minecraftVersion("1.21.2/1.21.3") + .build()); } /** From 677a56cf6c487d7cc74e139e76057fe5ba16b84b Mon Sep 17 00:00:00 2001 From: "masel.io" Date: Tue, 16 Jul 2024 11:23:30 +0200 Subject: [PATCH 27/84] Add Timeout to CompletableFuture in GeyserBungeePingPassthrough to Prevent Memory Leak (#4858) * fix: Add timeout for GeyserBungeePingPassthrough#getPingInformation Signed-off-by: ByteExceptionM * fix: Use Geyser Logger instead of Bungee Logger Signed-off-by: ByteExceptionM * Fix typo Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> * chore: Add ip suppression if configured Signed-off-by: ByteExceptionM * Remove empty line Co-authored-by: chris * Remove empty line Co-authored-by: chris --------- Signed-off-by: ByteExceptionM Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> Co-authored-by: chris --- .../bungeecord/GeyserBungeePingPassthrough.java | 14 +++++++++++++- .../geyser/ping/IGeyserPingPassthrough.java | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java index 3c3853ed8..1193a52b3 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java @@ -36,6 +36,7 @@ import net.md_5.bungee.api.event.ProxyPingEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.protocol.ProtocolConstants; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.ping.GeyserPingInfo; import org.geysermc.geyser.ping.IGeyserPingPassthrough; @@ -43,6 +44,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @AllArgsConstructor public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, Listener { @@ -59,7 +61,17 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List future.complete(event); } })); - ProxyPingEvent event = future.join(); + + ProxyPingEvent event; + + try { + event = future.get(100, TimeUnit.MILLISECONDS); + } catch (Throwable cause) { + String address = GeyserImpl.getInstance().getConfig().isLogPlayerIpAddresses() ? inetSocketAddress.toString() : ""; + GeyserImpl.getInstance().getLogger().error("Failed to get ping information for " + address, cause); + return null; + } + ServerPing response = event.getResponse(); return new GeyserPingInfo( response.getDescriptionComponent().toLegacyText(), diff --git a/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java b/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java index 69ac974cc..4e60d60e4 100644 --- a/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java +++ b/core/src/main/java/org/geysermc/geyser/ping/IGeyserPingPassthrough.java @@ -35,10 +35,10 @@ import java.net.InetSocketAddress; public interface IGeyserPingPassthrough { /** - * Get the MOTD of the server displayed on the multiplayer screen + * Gets the ping information, including the MOTD and player count, from the server * * @param inetSocketAddress the ip address of the client pinging the server - * @return string of the MOTD + * @return the ping information */ @Nullable GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress); From 669a76c628f6a99eb3304b3505331e1609373111 Mon Sep 17 00:00:00 2001 From: Valaphee The Meerkat <32491319+valaphee@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:13:34 +0200 Subject: [PATCH 28/84] Empty player list on phase transition, despawn skulls, always reset weather (#4847) * Empty player list on transition, despawn skulls * Always reset weather --- .../geyser/session/cache/EntityCache.java | 4 ++ .../geyser/session/cache/SkullCache.java | 8 +++ ...vaFinishConfigurationPacketTranslator.java | 49 +++++++++++++++++++ .../protocol/java/JavaLoginTranslator.java | 19 ------- .../geysermc/geyser/util/DimensionUtils.java | 15 ++++++ 5 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaFinishConfigurationPacketTranslator.java diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java index 6524e1ddc..3affa12cf 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java @@ -141,6 +141,10 @@ public class EntityCache { return playerEntities.values(); } + public void removeAllPlayerEntities() { + playerEntities.clear(); + } + public void addBossBar(UUID uuid, BossBar bossBar) { bossBars.put(uuid, bossBar); bossBar.addBossBar(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java index a40a1156d..0eec39b0b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/SkullCache.java @@ -243,8 +243,16 @@ public class SkullCache { } public void clear() { + for (Skull skull : skulls.values()) { + if (skull.entity != null) { + skull.entity.despawnEntity(); + } + } skulls.clear(); inRangeSkulls.clear(); + for (SkullPlayerEntity skull : unusedSkullEntities) { + skull.despawnEntity(); + } unusedSkullEntities.clear(); totalSkullEntities = 0; lastPlayerPosition = null; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaFinishConfigurationPacketTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaFinishConfigurationPacketTranslator.java new file mode 100644 index 000000000..8ade4a1f0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaFinishConfigurationPacketTranslator.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 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.geyser.translator.protocol.java; + +import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.packet.configuration.clientbound.ClientboundFinishConfigurationPacket; + +@Translator(packet = ClientboundFinishConfigurationPacket.class) +public class JavaFinishConfigurationPacketTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundFinishConfigurationPacket packet) { + // Clear the player list, as on Java the player list is cleared after transitioning from config to play phase + PlayerListPacket playerListPacket = new PlayerListPacket(); + playerListPacket.setAction(PlayerListPacket.Action.REMOVE); + for (PlayerEntity otherEntity : session.getEntityCache().getAllPlayerEntities()) { + playerListPacket.getEntries().add(new PlayerListPacket.Entry(otherEntity.getTabListUuid())); + } + session.sendUpstreamPacket(playerListPacket); + session.getEntityCache().removeAllPlayerEntities(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 6988d6cc8..6c065a392 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -79,25 +79,6 @@ public class JavaLoginTranslator extends PacketTranslator Date: Wed, 17 Jul 2024 14:21:41 -0400 Subject: [PATCH 29/84] Fix #4870 --- .../level/block/entity/CampfireBlockEntityTranslator.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CampfireBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CampfireBlockEntityTranslator.java index fb71a84cc..703c0954c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CampfireBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/CampfireBlockEntityTranslator.java @@ -42,10 +42,9 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator { public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) { List items = javaNbt.getList("Items", NbtType.COMPOUND); if (items != null) { - int i = 1; for (NbtMap itemTag : items) { - bedrockNbt.put("Item" + i, getItem(session, itemTag)); - i++; + int slot = itemTag.getByte("Slot") + 1; + bedrockNbt.put("Item" + slot, getItem(session, itemTag)); } } } @@ -55,8 +54,7 @@ public class CampfireBlockEntityTranslator extends BlockEntityTranslator { if (mapping == null) { mapping = ItemMapping.AIR; } - NbtMapBuilder tagBuilder = BedrockItemBuilder.createItemNbt(mapping, tag.getByte("Count"), mapping.getBedrockData()); - tagBuilder.put("tag", NbtMap.builder().build()); // I don't think this is necessary... - Camo, 1.20.5/1.20.80 + NbtMapBuilder tagBuilder = BedrockItemBuilder.createItemNbt(mapping, tag.getInt("count"), mapping.getBedrockData()); return tagBuilder.build(); } } From 9fe3acc81cb88d613a558111bc6b8f326add300d Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 17 Jul 2024 21:44:53 +0200 Subject: [PATCH 30/84] Properly fix hanging signs (#4872) --- .../populator/CreativeItemRegistryPopulator.java | 1 - .../registry/populator/ItemRegistryPopulator.java | 11 ++++++++++- .../geysermc/geyser/registry/type/ItemMapping.java | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java index 2c033edc7..8e42887ff 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CreativeItemRegistryPopulator.java @@ -80,7 +80,6 @@ public class CreativeItemRegistryPopulator { private static ItemData.@Nullable Builder createItemData(JsonNode itemNode, BlockMappings blockMappings, Map definitions) { int count = 1; int damage = 0; - int bedrockBlockRuntimeId; NbtMap tag = null; String identifier = itemNode.get("id").textValue(); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index aad5e494d..2c97fe13c 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -167,6 +167,7 @@ public class ItemRegistryPopulator { Map javaItemToMapping = new Object2ObjectOpenHashMap<>(); List creativeItems = new ArrayList<>(); + Set noBlockDefinitions = new ObjectOpenHashSet<>(); AtomicInteger creativeNetId = new AtomicInteger(); CreativeItemRegistryPopulator.populate(palette, definitions, itemBuilder -> { @@ -187,6 +188,9 @@ public class ItemRegistryPopulator { bedrockBlockIdOverrides.put(identifier, item.getBlockDefinition()); } } + } else { + // Item mappings should also NOT have a block definition for these. + noBlockDefinitions.add(item.getDefinition().getIdentifier()); } }); @@ -254,7 +258,12 @@ public class ItemRegistryPopulator { } else { // Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1); - if (aValidBedrockBlockId != -1 || customBlockItemOverride != null) { + if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) { + // Fallback + if (!noBlockDefinitions.contains(entry.getValue().getBedrockIdentifier())) { + bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId); + } + } else { // As of 1.16.220, every item requires a block runtime ID attached to it. // This is mostly for identifying different blocks with the same item ID - wool, slabs, some walls. // However, in order for some visuals and crafting to work, we need to send the first matching block state diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java index 437b8223a..8a2c77f28 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.registry.type; import it.unimi.dsi.fastutil.Pair; import lombok.Builder; import lombok.EqualsAndHashCode; +import lombok.ToString; import lombok.Value; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; @@ -42,6 +43,7 @@ import java.util.List; @Value @Builder @EqualsAndHashCode +@ToString public class ItemMapping { public static final ItemMapping AIR = new ItemMapping( "minecraft:air", From 8fd99e1e1a01bd5b12ef8a10c4b5778fa2b4de91 Mon Sep 17 00:00:00 2001 From: LetsGoAway <68365423+letsgoawaydev@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:09:22 +0800 Subject: [PATCH 31/84] Use the correct way of sending block breaking particles (#4825) --- .../player/BedrockActionTranslator.java | 197 ++++++++++-------- 1 file changed, 113 insertions(+), 84 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java index 6834d3190..cd1300a13 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java @@ -32,7 +32,12 @@ import org.cloudburstmc.protocol.bedrock.data.PlayerActionType; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; +import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.api.block.custom.CustomBlockState; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; @@ -52,8 +57,17 @@ import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.BlockUtils; import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; -import org.geysermc.mcprotocollib.protocol.data.game.entity.player.*; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.*; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractAction; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket; @Translator(packet = PlayerActionPacket.class) public class BedrockActionTranslator extends PacketTranslator { @@ -70,7 +84,7 @@ public class BedrockActionTranslator extends PacketTranslator { // Respawn process is finished and the server and client are both OK with respawning. EntityEventPacket eventPacket = new EntityEventPacket(); eventPacket.setRuntimeEntityId(entity.getGeyserId()); @@ -88,16 +102,16 @@ public class BedrockActionTranslator extends PacketTranslator { if (!entity.getFlag(EntityFlag.SWIMMING)) { ServerboundPlayerCommandPacket startSwimPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING); session.sendDownstreamGamePacket(startSwimPacket); session.setSwimming(true); } - break; - case STOP_SWIMMING: + } + case STOP_SWIMMING -> { // Prevent packet spam when Bedrock players are crawling near the edge of a block if (!session.getCollisionManager().mustPlayerCrawlHere()) { ServerboundPlayerCommandPacket stopSwimPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING); @@ -105,56 +119,55 @@ public class BedrockActionTranslator extends PacketTranslator { // Otherwise gliding will not work in creative ServerboundPlayerAbilitiesPacket playerAbilitiesPacket = new ServerboundPlayerAbilitiesPacket(false); session.sendDownstreamGamePacket(playerAbilitiesPacket); - case STOP_GLIDE: - ServerboundPlayerCommandPacket glidePacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_ELYTRA_FLYING); - session.sendDownstreamGamePacket(glidePacket); - break; - case START_SNEAK: + sendPlayerGlideToggle(session, entity); + } + case STOP_GLIDE -> sendPlayerGlideToggle(session, entity); + case START_SNEAK -> { ServerboundPlayerCommandPacket startSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING); session.sendDownstreamGamePacket(startSneakPacket); session.startSneaking(); - break; - case STOP_SNEAK: + } + case STOP_SNEAK -> { ServerboundPlayerCommandPacket stopSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SNEAKING); session.sendDownstreamGamePacket(stopSneakPacket); session.stopSneaking(); - break; - case START_SPRINT: + } + case START_SPRINT -> { if (!entity.getFlag(EntityFlag.SWIMMING)) { ServerboundPlayerCommandPacket startSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING); session.sendDownstreamGamePacket(startSprintPacket); session.setSprinting(true); } - break; - case STOP_SPRINT: + } + case STOP_SPRINT -> { if (!entity.getFlag(EntityFlag.SWIMMING)) { ServerboundPlayerCommandPacket stopSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING); session.sendDownstreamGamePacket(stopSprintPacket); } session.setSprinting(false); - break; - case DROP_ITEM: + } + case DROP_ITEM -> { ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, - vector, Direction.VALUES[packet.getFace()], 0); + vector, Direction.VALUES[packet.getFace()], 0); session.sendDownstreamGamePacket(dropItemPacket); - break; - case STOP_SLEEP: + } + case STOP_SLEEP -> { ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED); session.sendDownstreamGamePacket(stopSleepingPacket); - break; - case START_BREAK: { - // Ignore START_BREAK when the player is CREATIVE to avoid Spigot receiving 2 packets it interpets as block breaking. https://github.com/GeyserMC/Geyser/issues/4021 - if (session.getGameMode() == GameMode.CREATIVE) { + } + case START_BREAK -> { + // Ignore START_BREAK when the player is CREATIVE to avoid Spigot receiving 2 packets it interpets as block breaking. https://github.com/GeyserMC/Geyser/issues/4021 + if (session.getGameMode() == GameMode.CREATIVE) { break; } - + // Start the block breaking animation int blockState = session.getGeyser().getWorldManager().getBlockAt(session, vector); LevelEventPacket startBreak = new LevelEventPacket(); @@ -180,18 +193,20 @@ public class BedrockActionTranslator extends PacketTranslator { if (session.getGameMode() == GameMode.CREATIVE) { break; } @@ -201,52 +216,48 @@ public class BedrockActionTranslator extends PacketTranslator= (breakTime+=2) * 50) { + if (timeSinceStart >= (breakTime += 2) * 50) { // Play break sound and particle LevelEventPacket effectPacket = new LevelEventPacket(); effectPacket.setPosition(vectorFloat); effectPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK); effectPacket.setData(session.getBlockMappings().getBedrockBlockId(breakingBlock)); session.sendUpstreamPacket(effectPacket); - + // Break the block ServerboundPlayerActionPacket finishBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING, - vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence()); + vector, direction, session.getWorldCache().nextPredictionSequence()); session.sendDownstreamGamePacket(finishBreakingPacket); session.setBlockBreakStartTime(0); break; } } - + // Update the break time in the event that player conditions changed (jumping, effects applied) + LevelEventPacket updateBreak = new LevelEventPacket(); + updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK); + updateBreak.setPosition(vectorFloat); updateBreak.setData((int) (65535 / breakTime)); session.sendUpstreamPacket(updateBreak); - break; - case ABORT_BREAK: + } + case ABORT_BREAK -> { if (session.getGameMode() != GameMode.CREATIVE) { // As of 1.16.210: item frame items are taken out here. // Survival also sends START_BREAK, but by attaching our process here adventure mode also works Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, vector); if (itemFrameEntity != null) { ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(), - InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking()); + InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking()); session.sendDownstreamGamePacket(interactPacket); break; } @@ -260,25 +271,23 @@ public class BedrockActionTranslator extends PacketTranslator { + } + case DIMENSION_CHANGE_SUCCESS -> { //sometimes the client doesn't feel like loading PlayStatusPacket spawnPacket = new PlayStatusPacket(); spawnPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); session.sendUpstreamPacket(spawnPacket); - attributesPacket = new UpdateAttributesPacket(); + UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); attributesPacket.setRuntimeEntityId(entity.getGeyserId()); attributesPacket.getAttributes().addAll(entity.getAttributes().values()); session.sendUpstreamPacket(attributesPacket); - break; - case JUMP: - entity.setOnGround(false); // Increase block break time while jumping - break; - case MISSED_SWING: + } + case JUMP -> entity.setOnGround(false); // Increase block break time while jumping + case MISSED_SWING -> { // Java edition sends a cooldown when hitting air. // Normally handled by BedrockLevelSoundEventTranslator, but there is no sound on Java for this. CooldownUtils.sendCooldown(session); @@ -294,18 +303,18 @@ public class BedrockActionTranslator extends PacketTranslator { // Since 1.20.30 if (session.isCanFly()) { if (session.getGameMode() == GameMode.SPECTATOR) { - // should already be flying + // should already be flying session.sendAdventureSettings(); break; } if (session.getPlayerEntity().getFlag(EntityFlag.SWIMMING) && session.getCollisionManager().isPlayerInWater()) { - // As of 1.18.1, Java Edition cannot fly while in water, but it can fly while crawling - // If this isn't present, swimming on a 1.13.2 server and then attempting to fly will put you into a flying/swimming state that is invalid on JE + // As of 1.18.1, Java Edition cannot fly while in water, but it can fly while crawling + // If this isn't present, swimming on a 1.13.2 server and then attempting to fly will put you into a flying/swimming state that is invalid on JE session.sendAdventureSettings(); break; } @@ -313,9 +322,9 @@ public class BedrockActionTranslator extends PacketTranslator { session.setFlying(false); session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(false)); - break; - case DIMENSION_CHANGE_REQUEST_OR_CREATIVE_DESTROY_BLOCK: // Used by client to get book from lecterns and items from item frame in creative mode since 1.20.70 + } + case DIMENSION_CHANGE_REQUEST_OR_CREATIVE_DESTROY_BLOCK -> { // Used by client to get book from lecterns and items from item frame in creative mode since 1.20.70 BlockState state = session.getGeyser().getWorldManager().blockAt(session, vector); - + if (state.getValue(Properties.HAS_BOOK, false)) { session.setDroppingLecternBook(true); ServerboundUseItemOnPacket blockPacket = new ServerboundUseItemOnPacket( - vector, - Direction.DOWN, - Hand.MAIN_HAND, - 0, 0, 0, - false, - session.getWorldCache().nextPredictionSequence()); + vector, + Direction.DOWN, + Hand.MAIN_HAND, + 0, 0, 0, + false, + session.getWorldCache().nextPredictionSequence()); session.sendDownstreamGamePacket(blockPacket); break; } @@ -349,10 +358,30 @@ public class BedrockActionTranslator extends PacketTranslator levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_UP); + case DOWN -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_DOWN); + case NORTH -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_NORTH); + case EAST -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_EAST); + case SOUTH -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_SOUTH); + case WEST -> levelEventPacket.setType(LevelEvent.PARTICLE_BREAK_BLOCK_WEST); + } + levelEventPacket.setPosition(position.toFloat()); + levelEventPacket.setData(session.getBlockMappings().getBedrockBlock(blockState).getRuntimeId()); + session.sendUpstreamPacket(levelEventPacket); + } + + private void sendPlayerGlideToggle(GeyserSession session, Entity entity) { + ServerboundPlayerCommandPacket glidePacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_ELYTRA_FLYING); + session.sendDownstreamGamePacket(glidePacket); + } } From f62cef7acbd8fecab28793cc9649ddba2982bffe Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 21 Jul 2024 02:10:51 +0200 Subject: [PATCH 32/84] Fix: Only shutdown/close handlers in onDisable if they're nonnull (#4882) --- .../java/org/geysermc/geyser/GeyserImpl.java | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 88cc74691..9ee182edd 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -55,7 +55,11 @@ import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.event.EventBus; import org.geysermc.geyser.api.event.EventRegistrar; -import org.geysermc.geyser.api.event.lifecycle.*; +import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPostReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserPreReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; @@ -85,7 +89,13 @@ import org.geysermc.geyser.skin.SkinProvider; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.geyser.util.*; +import org.geysermc.geyser.util.AssetUtils; +import org.geysermc.geyser.util.CooldownUtils; +import org.geysermc.geyser.util.DimensionUtils; +import org.geysermc.geyser.util.Metrics; +import org.geysermc.geyser.util.NewsHandler; +import org.geysermc.geyser.util.VersionCheckUtils; +import org.geysermc.geyser.util.WebUtils; import org.geysermc.mcprotocollib.network.tcp.TcpSession; import java.io.File; @@ -97,11 +107,19 @@ import java.net.UnknownHostException; import java.nio.file.Path; import java.security.Key; import java.text.DecimalFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -645,16 +663,11 @@ public class GeyserImpl implements GeyserApi { bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.kick.done")); } - scheduledThread.shutdown(); - geyserServer.shutdown(); - if (skinUploader != null) { - skinUploader.close(); - } - newsHandler.shutdown(); - - if (this.erosionUnixListener != null) { - this.erosionUnixListener.close(); - } + runIfNonNull(scheduledThread, ScheduledExecutorService::shutdown); + runIfNonNull(geyserServer, GeyserServer::shutdown); + runIfNonNull(skinUploader, FloodgateSkinUploader::close); + runIfNonNull(newsHandler, NewsHandler::shutdown); + runIfNonNull(erosionUnixListener, UnixSocketClientListener::close); Registries.RESOURCE_PACKS.get().clear(); @@ -833,6 +846,12 @@ public class GeyserImpl implements GeyserApi { } } + private void runIfNonNull(T nullable, Consumer consumer) { + if (nullable != null) { + consumer.accept(nullable); + } + } + private void scheduleRefreshTokensWrite() { scheduledThread.execute(() -> { // Ensure all writes are handled on the same thread From 96f00981df9f6f180e90349f6e9a711fcce05883 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 21 Jul 2024 02:15:38 +0200 Subject: [PATCH 33/84] Somewhat fix: firework recipe not showing up in recipe book (#4873) * Somewhat fix firework crafting * Use instanceof instead of casting --- .../item/type/BedrockRequiresTagItem.java | 36 +++++++++++++++++++ .../geyser/item/type/FireworkRocketItem.java | 36 ++++++++++++++----- .../translator/item/ItemTranslator.java | 7 ++++ .../java/JavaUpdateRecipesTranslator.java | 33 +++++++++++++---- 4 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/item/type/BedrockRequiresTagItem.java diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BedrockRequiresTagItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BedrockRequiresTagItem.java new file mode 100644 index 000000000..c41d14396 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/type/BedrockRequiresTagItem.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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.geyser.item.type; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; + +public interface BedrockRequiresTagItem { + + void addRequiredNbt(GeyserSession session, @Nullable DataComponents components, BedrockItemBuilder builder); +} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java index 9c637afde..2e7848318 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java @@ -27,6 +27,8 @@ package org.geysermc.geyser.item.type; import it.unimi.dsi.fastutil.ints.IntArrays; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; @@ -41,7 +43,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.Fireworks; import java.util.ArrayList; import java.util.List; -public class FireworkRocketItem extends Item { +public class FireworkRocketItem extends Item implements BedrockRequiresTagItem { public FireworkRocketItem(String javaIdentifier, Builder builder) { super(javaIdentifier, builder); } @@ -58,14 +60,16 @@ public class FireworkRocketItem extends Item { fireworksNbt.putByte("Flight", (byte) fireworks.getFlightDuration()); List explosions = fireworks.getExplosions(); - if (explosions.isEmpty()) { - return; + if (!explosions.isEmpty()) { + List explosionNbt = new ArrayList<>(); + for (Fireworks.FireworkExplosion explosion : explosions) { + explosionNbt.add(translateExplosionToBedrock(explosion)); + } + fireworksNbt.putList("Explosions", NbtType.COMPOUND, explosionNbt); + } else { + // This is the default firework + fireworksNbt.put("Explosions", NbtList.EMPTY); } - List explosionNbt = new ArrayList<>(); - for (Fireworks.FireworkExplosion explosion : explosions) { - explosionNbt.add(translateExplosionToBedrock(explosion)); - } - fireworksNbt.putList("Explosions", NbtType.COMPOUND, explosionNbt); builder.putCompound("Fireworks", fireworksNbt.build()); } @@ -138,4 +142,20 @@ public class FireworkRocketItem extends Item { return null; } } + + @Override + public void addRequiredNbt(GeyserSession session, @Nullable DataComponents components, BedrockItemBuilder builder) { + if (components != null) { + Fireworks fireworks = components.get(DataComponentType.FIREWORKS); + if (fireworks != null) { + // Already translated + return; + } + } + + NbtMapBuilder fireworksNbt = NbtMap.builder(); + fireworksNbt.putByte("Flight", (byte) 1); + fireworksNbt.put("Explosions", NbtList.EMPTY); + builder.putCompound("Fireworks", fireworksNbt.build()); + } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index 6a781dcb8..e9527872a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -45,6 +45,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.item.type.BedrockRequiresTagItem; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -148,6 +149,12 @@ public final class ItemTranslator { if (components.get(DataComponentType.HIDE_TOOLTIP) != null) hideTooltips = true; } + // Fixes fireworks crafting recipe: they always contain a tag + // TODO remove once all items have their default components + if (javaItem instanceof BedrockRequiresTagItem requiresTagItem) { + requiresTagItem.addRequiredNbt(session, components, nbtBuilder); + } + Rarity rarity = javaItem.rarity(); boolean enchantmentGlint = javaItem.glint(); if (components != null) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index f9b840dd9..fd8981552 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -25,7 +25,11 @@ package org.geysermc.geyser.translator.protocol.java; -import it.unimi.dsi.fastutil.ints.*; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; @@ -40,7 +44,11 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescri import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; import org.cloudburstmc.protocol.bedrock.packet.TrimDataPacket; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.inventory.recipe.*; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData; +import org.geysermc.geyser.inventory.recipe.TrimRecipe; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -58,7 +66,17 @@ import org.geysermc.mcprotocollib.protocol.data.game.recipe.data.SmithingTransfo import org.geysermc.mcprotocollib.protocol.data.game.recipe.data.StoneCuttingRecipeData; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundUpdateRecipesPacket; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static org.geysermc.geyser.util.InventoryUtils.LAST_RECIPE_NET_ID; @@ -191,6 +209,9 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), context.getAndIncrementNetId())); } + case CRAFTING_SPECIAL_FIREWORK_ROCKET -> { + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("00000000-0000-0000-0000-000000000002"), context.getAndIncrementNetId())); + } default -> { List recipes = Registries.RECIPES.get(recipe.getType()); if (recipes != null) { @@ -427,7 +448,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator Date: Sat, 20 Jul 2024 23:21:36 -0400 Subject: [PATCH 34/84] Small cleanups --- README.md | 2 +- .../entity/type/living/animal/tameable/ParrotEntity.java | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 9469b1bb6..07f3df5aa 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ There are a few things Geyser is unable to support due to various differences be 3. Run `gradlew build` and locate to `bootstrap/build` folder. ## Contributing -Any contributions are appreciated. Please feel free to reach out to us on [Discord](http://discord.geysermc.org/) if +Any contributions are appreciated. Please feel free to reach out to us on [Discord](https://discord.gg/geysermc) if you're interested in helping out with Geyser. ## Libraries Used: diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java index 8baba6f00..69b19b1b9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/ParrotEntity.java @@ -31,7 +31,6 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; @@ -39,13 +38,9 @@ import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import java.util.Set; import java.util.UUID; public class ParrotEntity extends TameableEntity { - // Note: is the same as chicken. Reuse? - private static final Set TAMING_FOOD = Set.of(Items.WHEAT_SEEDS, Items.MELON_SEEDS, Items.PUMPKIN_SEEDS, Items.BEETROOT_SEEDS); - public ParrotEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } From 7d5c4a38f807c864c2784492b3aba0a7c79ef999 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 20 Jul 2024 23:22:04 -0400 Subject: [PATCH 35/84] Respect block range attribute where we can #4864 --- .../entity/attribute/GeyserAttributeType.java | 1 + .../type/player/SessionPlayerEntity.java | 8 ++ ...BedrockInventoryTransactionTranslator.java | 81 +++++++++---------- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java index a4a0df8b8..3b543a943 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java @@ -50,6 +50,7 @@ public enum GeyserAttributeType { ATTACK_SPEED("minecraft:generic.attack_speed", null, 0f, 1024f, 4f), MAX_HEALTH("minecraft:generic.max_health", null, 0f, 1024f, 20f), SCALE("minecraft:generic.scale", null, 0.0625f, 16f, 1f), + BLOCK_INTERACTION_RANGE("minecraft:player.block_interaction_range", null, 0.0f, 64f, 4.5f), // Bedrock Attributes ABSORPTION(null, "minecraft:absorption", 0f, 1024f, 0f), diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index 45fea4d48..dc0545cee 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -64,6 +64,11 @@ public class SessionPlayerEntity extends PlayerEntity { */ @Getter protected final Map attributes = new Object2ObjectOpenHashMap<>(); + /** + * Java-only attribute + */ + @Getter + private double blockInteractionRange = GeyserAttributeType.BLOCK_INTERACTION_RANGE.getDefaultValue(); /** * Used in PlayerInputTranslator for movement checks. */ @@ -232,6 +237,8 @@ public class SessionPlayerEntity extends PlayerEntity { protected void updateAttribute(Attribute javaAttribute, List newAttributes) { if (javaAttribute.getType() == AttributeType.Builtin.GENERIC_ATTACK_SPEED) { session.setAttackSpeed(AttributeUtils.calculateValue(javaAttribute)); + } else if (javaAttribute.getType() == AttributeType.Builtin.PLAYER_BLOCK_INTERACTION_RANGE) { + this.blockInteractionRange = AttributeUtils.calculateValue(javaAttribute); } else { super.updateAttribute(javaAttribute, newAttributes); } @@ -295,6 +302,7 @@ public class SessionPlayerEntity extends PlayerEntity { public void resetAttributes() { attributes.clear(); maxHealth = GeyserAttributeType.MAX_HEALTH.getDefaultValue(); + blockInteractionRange = GeyserAttributeType.BLOCK_INTERACTION_RANGE.getDefaultValue(); UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); attributesPacket.setRuntimeEntityId(geyserId); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 534a89e23..9d78d174b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -70,7 +70,11 @@ import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import org.geysermc.geyser.util.*; +import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.CooldownUtils; +import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -78,7 +82,11 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.InteractActio import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.*; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundUseItemOnPacket; import java.util.List; import java.util.concurrent.TimeUnit; @@ -90,11 +98,6 @@ import java.util.concurrent.TimeUnit; @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(GeyserSession session, InventoryTransactionPacket packet) { if (packet.getTransactionType() == InventoryTransactionType.NORMAL && packet.getActions().size() == 3) { @@ -243,17 +246,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator - (creative ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) { + if (!canInteractWithBlock(session, playerPosition, packetBlockPosition)) { restoreCorrectBlock(session, blockPos, packet); return; } @@ -262,26 +261,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator - (creative ? CREATIVE_EYE_HEIGHT_PLACE_DISTANCE : SURVIVAL_EYE_HEIGHT_PLACE_DISTANCE)) { - restoreCorrectBlock(session, blockPos, packet); - return; - } - Vector3f blockCenter = Vector3f.from(packetBlockPosition.getX() + 0.5f, packetBlockPosition.getY() + 0.5f, packetBlockPosition.getZ() + 0.5f); - // Vanilla check - if (!(session.getPlayerEntity().getPosition().sub(0, EntityDefinitions.PLAYER.offset(), 0) - .distanceSquared(blockCenter) < 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; - } - // More recent vanilla check (as of 1.18.2) double clickDistanceX = clickPositionFullX - blockCenter.getX(); double clickDistanceY = clickPositionFullY - blockCenter.getY(); double clickDistanceZ = clickPositionFullZ - blockCenter.getZ(); @@ -433,14 +414,10 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator MAXIMUM_BLOCK_DESTROYING_DISTANCE) { + playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight()); + + if (!canInteractWithBlock(session, playerPosition, packet.getBlockPosition())) { restoreCorrectBlock(session, packet.getBlockPosition(), packet); return; } @@ -550,6 +527,28 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator(BlockPos) + float minX = packetBlockPosition.getX(); + float minY = packetBlockPosition.getY(); + float minZ = packetBlockPosition.getZ(); + float maxX = packetBlockPosition.getX() + 1; + float maxY = packetBlockPosition.getY() + 1; + float maxZ = packetBlockPosition.getZ() + 1; + + // AABB#distanceToSqr + float diffX = Math.max(Math.max(minX - playerPosition.getX(), playerPosition.getX() - maxX), 0); + float diffY = Math.max(Math.max(minY - playerPosition.getY(), playerPosition.getY() - maxY), 0); + float diffZ = Math.max(Math.max(minZ - playerPosition.getZ(), playerPosition.getZ() - maxZ), 0); + return ((diffX * diffX) + (diffY * diffY) + (diffZ * diffZ)) < (additionalRangeCheck * additionalRangeCheck); + } + /** * Restore the correct block state from the server without updating the chunk cache. * @@ -696,4 +695,4 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator Date: Sun, 21 Jul 2024 13:11:27 +0200 Subject: [PATCH 36/84] Fix: Text display offset (#4883) --- .../geysermc/geyser/entity/EntityDefinitions.java | 1 + .../geyser/entity/type/TextDisplayEntity.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 11b4a32d1..9063c7421 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -327,6 +327,7 @@ public final class EntityDefinitions { TEXT_DISPLAY = EntityDefinition.inherited(TextDisplayEntity::new, displayBase) .type(EntityType.TEXT_DISPLAY) .identifier("minecraft:armor_stand") + .offset(-0.5f) .addTranslator(MetadataType.CHAT, TextDisplayEntity::setText) .addTranslator(null) // Line width .addTranslator(null) // Background color diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java index ff5604c19..8b47ce1ed 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java @@ -38,7 +38,17 @@ import java.util.UUID; // Note: 1.19.4 requires that the billboard is set to something in order to show, on Java Edition public class TextDisplayEntity extends DisplayBaseEntity { public TextDisplayEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { - super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + super(session, entityId, geyserId, uuid, definition, position.add(0, definition.offset(), 0), motion, yaw, pitch, headYaw); + } + + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, boolean isOnGround) { + super.moveRelative(relX, relY + definition.offset(), relZ, yaw, pitch, isOnGround); + } + + @Override + public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) { + super.moveAbsolute(position.add(Vector3f.from(0, definition.offset(), 0)), yaw, pitch, headYaw, isOnGround, teleported); } @Override From 1dd9ba3fb6e20a4df18580b9a4c45548ee2a3898 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 22 Jul 2024 07:24:21 +0200 Subject: [PATCH 37/84] Fix: Allow items to be worn as hats if their Java base items also allow it (#4885) --- .../registry/populator/CustomItemRegistryPopulator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java index a6fa164c1..0a9c93980 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java @@ -199,7 +199,13 @@ public class CustomItemRegistryPopulator { computeThrowableProperties(componentBuilder); } - computeRenderOffsets(false, customItemData, componentBuilder); + // Hardcoded on Java, and should extend to the custom item + boolean isHat = (javaItem.equals(Items.SKELETON_SKULL) || javaItem.equals(Items.WITHER_SKELETON_SKULL) + || javaItem.equals(Items.CARVED_PUMPKIN) || javaItem.equals(Items.ZOMBIE_HEAD) + || javaItem.equals(Items.PIGLIN_HEAD) || javaItem.equals(Items.DRAGON_HEAD) + || javaItem.equals(Items.CREEPER_HEAD) || javaItem.equals(Items.PLAYER_HEAD) + ); + computeRenderOffsets(isHat, customItemData, componentBuilder); componentBuilder.putCompound("item_properties", itemProperties.build()); builder.putCompound("components", componentBuilder.build()); From b113a6b185209bdc9718d81bed67c62a81375f2a Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:43:57 -0400 Subject: [PATCH 38/84] Mark sequence position when block placing Fixes some instances between 1.19 and 1.20.5 when block ghosting could occur --- .../java/org/geysermc/geyser/session/cache/WorldCache.java | 2 +- .../bedrock/BedrockInventoryTransactionTranslator.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java index 8eb715560..fb5137b05 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java @@ -201,4 +201,4 @@ public final class WorldCache { public String removeActiveRecord(Vector3i pos) { return this.activeRecords.remove(pos); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java index 9d78d174b..1e4c82da1 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java @@ -247,8 +247,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator Date: Tue, 23 Jul 2024 23:12:05 +0200 Subject: [PATCH 39/84] Fix: Sticky pistons not retracting on Geyser-Spigot/turning visually into normal pistons on all other platforms (#4891) --- .../java/level/JavaBlockEventTranslator.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java index f56750d12..ff861530a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java @@ -43,7 +43,15 @@ import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator; import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; -import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.*; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.BellValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.BlockValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.ChestValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.DecoratedPotValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.EndGatewayValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.MobSpawnerValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.NoteBlockValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValue; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValueType; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundBlockEventPacket; @Translator(packet = ClientboundBlockEventPacket.class) @@ -80,7 +88,7 @@ public class JavaBlockEventTranslator extends PacketTranslator { BlockState state = session.getGeyser().getWorldManager().blockAt(session, position); - boolean sticky = state.is(Blocks.STICKY_PISTON); + boolean sticky = isSticky(state); boolean extended = action != PistonValueType.PUSHING; return new PistonBlockEntity(session, pos, direction, sticky, extended); }); @@ -149,4 +157,8 @@ public class JavaBlockEventTranslator extends PacketTranslator Date: Wed, 24 Jul 2024 14:27:05 -0400 Subject: [PATCH 40/84] Map unbreakable item component Fixes #4893 --- core/src/main/java/org/geysermc/geyser/item/type/Item.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 57538565a..2417177ce 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -170,6 +170,11 @@ public class Item { builder.putInt("RepairCost", repairCost); } + // If the tag exists, it's unbreakable; the value is just weather to show the tooltip. As of Java 1.21 + if (components.getDataComponents().containsKey(DataComponentType.UNBREAKABLE)) { + builder.putByte("Unbreakable", (byte) 1); + } + // Prevents the client from trying to stack items with untranslated components // Relies on correct hash code implementation, and some luck builder.putInt("GeyserHash", components.hashCode()); // TODO: don't rely on this From a85b312b40cb7f819d9cba896463ed3bfb38e4c5 Mon Sep 17 00:00:00 2001 From: Alex <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:06:59 +0200 Subject: [PATCH 41/84] Port to MinecraftAuth (#4779) Co-authored-by: Kas-tle <26531652+Kas-tle@users.noreply.github.com> Co-authored-by: onebeastchris Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- bootstrap/mod/fabric/build.gradle.kts | 2 +- core/build.gradle.kts | 5 +- .../java/org/geysermc/geyser/Constants.java | 4 +- .../java/org/geysermc/geyser/GeyserImpl.java | 91 +++++++-- .../geyser/item/type/PlayerHeadItem.java | 2 +- .../geyser/level/block/type/SkullBlock.java | 2 +- .../geyser/network/UpstreamPacketHandler.java | 6 +- .../geyser/session/GeyserSession.java | 192 +++++++++++------- .../PendingMicrosoftAuthentication.java | 136 +++++-------- .../geyser/skin/FakeHeadProvider.java | 16 +- .../translator/item/ItemTranslator.java | 14 +- ...SetLocalPlayerAsInitializedTranslator.java | 4 +- .../java/JavaGameProfileTranslator.java | 2 +- .../JavaPlayerInfoUpdateTranslator.java | 2 +- .../geyser/util/LoginEncryptionUtils.java | 6 +- .../geyser/util/MinecraftAuthLogger.java | 49 +++++ gradle/libs.versions.toml | 6 +- 17 files changed, 320 insertions(+), 219 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index 25bd0af9d..0d083fcf7 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { shadow(libs.protocol.connection) { isTransitive = false } shadow(libs.protocol.common) { isTransitive = false } shadow(libs.protocol.codec) { isTransitive = false } - shadow(libs.mcauthlib) { isTransitive = false } + shadow(libs.minecraftauth) { isTransitive = false } shadow(libs.raknet) { isTransitive = false } // Consequences of shading + relocating mcauthlib: shadow/relocate mcpl! diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1d1794cf7..3b5cc3df9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -25,11 +25,10 @@ dependencies { api(libs.bundles.protocol) - api(libs.mcauthlib) + api(libs.minecraftauth) api(libs.mcprotocollib) { exclude("io.netty", "netty-all") - exclude("com.github.GeyserMC", "packetlib") - exclude("com.github.GeyserMC", "mcauthlib") + exclude("net.raphimc", "MinecraftAuth") } implementation(libs.raknet) { diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 588b25172..534cb30ad 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -39,7 +39,9 @@ public final class Constants { public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download"; public static final String UPDATE_PERMISSION = "geyser.update"; + @Deprecated static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; + static final String SAVED_AUTH_CHAINS_FILE = "saved-auth-chains.json"; public static final String GEYSER_CUSTOM_NAMESPACE = "geyser_custom"; @@ -54,4 +56,4 @@ public final class Constants { } GLOBAL_API_WS_URI = wsUri; } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 9ee182edd..8f88f5b6a 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; import io.netty.channel.epoll.Epoll; import io.netty.util.NettyRuntime; import io.netty.util.concurrent.DefaultThreadFactory; @@ -38,6 +39,8 @@ import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; +import net.raphimc.minecraftauth.step.msa.StepMsaToken; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -93,6 +96,7 @@ import org.geysermc.geyser.util.AssetUtils; import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.Metrics; +import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.geyser.util.NewsHandler; import org.geysermc.geyser.util.VersionCheckUtils; import org.geysermc.geyser.util.WebUtils; @@ -179,7 +183,7 @@ public class GeyserImpl implements GeyserApi { private PendingMicrosoftAuthentication pendingMicrosoftAuthentication; @Getter(AccessLevel.NONE) - private Map savedRefreshTokens; + private Map savedAuthChains; @Getter private static GeyserImpl instance; @@ -552,37 +556,84 @@ public class GeyserImpl implements GeyserApi { if (config.getRemote().authType() == AuthType.ONLINE) { // May be written/read to on multiple threads from each GeyserSession as well as writing the config - savedRefreshTokens = new ConcurrentHashMap<>(); + savedAuthChains = new ConcurrentHashMap<>(); - File tokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); - if (tokensFile.exists()) { + // TODO Remove after a while - just a migration help + //noinspection deprecation + File refreshTokensFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + if (refreshTokensFile.exists()) { + logger.info("Migrating refresh tokens to auth chains..."); + TypeReference> type = new TypeReference<>() { }; + Map refreshTokens = null; + try { + refreshTokens = JSON_MAPPER.readValue(refreshTokensFile, type); + } catch (IOException e) { + // ignored - we'll just delete this file :)) + } + + if (refreshTokens != null) { + List validUsers = config.getSavedUserLogins(); + final Gson gson = new Gson(); + for (Map.Entry entry : refreshTokens.entrySet()) { + String user = entry.getKey(); + if (!validUsers.contains(user)) { + continue; + } + + // Migrate refresh tokens to auth chains + try { + StepFullJavaSession javaSession = PendingMicrosoftAuthentication.AUTH_FLOW.apply(false, 10); + StepFullJavaSession.FullJavaSession fullJavaSession = javaSession.getFromInput( + MinecraftAuthLogger.INSTANCE, + PendingMicrosoftAuthentication.AUTH_CLIENT, + new StepMsaToken.RefreshToken(entry.getValue()) + ); + + String authChain = gson.toJson(javaSession.toJson(fullJavaSession)); + savedAuthChains.put(user, authChain); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().warning("Could not migrate " + entry.getKey() + " to an auth chain! " + + "They will need to sign in the next time they join Geyser."); + } + + // Ensure the new additions are written to the file + scheduleAuthChainsWrite(); + } + } + + // Finally: Delete it. Goodbye! + refreshTokensFile.delete(); + } + + File authChainsFile = bootstrap.getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); + if (authChainsFile.exists()) { TypeReference> type = new TypeReference<>() { }; - Map refreshTokenFile = null; + Map authChainFile = null; try { - refreshTokenFile = JSON_MAPPER.readValue(tokensFile, type); + authChainFile = JSON_MAPPER.readValue(authChainsFile, type); } catch (IOException e) { logger.error("Cannot load saved user tokens!", e); } - if (refreshTokenFile != null) { + if (authChainFile != null) { List validUsers = config.getSavedUserLogins(); boolean doWrite = false; - for (Map.Entry entry : refreshTokenFile.entrySet()) { + for (Map.Entry entry : authChainFile.entrySet()) { String user = entry.getKey(); if (!validUsers.contains(user)) { // Perform a write to this file to purge the now-unused name doWrite = true; continue; } - savedRefreshTokens.put(user, entry.getValue()); + savedAuthChains.put(user, entry.getValue()); } if (doWrite) { - scheduleRefreshTokensWrite(); + scheduleAuthChainsWrite(); } } } } else { - savedRefreshTokens = null; + savedAuthChains = null; } newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); @@ -829,11 +880,11 @@ public class GeyserImpl implements GeyserApi { } @Nullable - public String refreshTokenFor(@NonNull String bedrockName) { - return savedRefreshTokens.get(bedrockName); + public String authChainFor(@NonNull String bedrockName) { + return savedAuthChains.get(bedrockName); } - public void saveRefreshToken(@NonNull String bedrockName, @NonNull String refreshToken) { + public void saveAuthChain(@NonNull String bedrockName, @NonNull String authChain) { if (!getConfig().getSavedUserLogins().contains(bedrockName)) { // Do not save this login return; @@ -841,8 +892,8 @@ public class GeyserImpl implements GeyserApi { // We can safely overwrite old instances because MsaAuthenticationService#getLoginResponseFromRefreshToken // refreshes the token for us - if (!Objects.equals(refreshToken, savedRefreshTokens.put(bedrockName, refreshToken))) { - scheduleRefreshTokensWrite(); + if (!Objects.equals(authChain, savedAuthChains.put(bedrockName, authChain))) { + scheduleAuthChainsWrite(); } } @@ -852,15 +903,15 @@ public class GeyserImpl implements GeyserApi { } } - private void scheduleRefreshTokensWrite() { + private void scheduleAuthChainsWrite() { scheduledThread.execute(() -> { // Ensure all writes are handled on the same thread - File savedTokens = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_REFRESH_TOKEN_FILE).toFile(); + File savedAuthChains = getBootstrap().getSavedUserLoginsFolder().resolve(Constants.SAVED_AUTH_CHAINS_FILE).toFile(); TypeReference> type = new TypeReference<>() { }; - try (FileWriter writer = new FileWriter(savedTokens)) { + try (FileWriter writer = new FileWriter(savedAuthChains)) { JSON_MAPPER.writerFor(type) .withDefaultPrettyPrinter() - .writeValue(writer, savedRefreshTokens); + .writeValue(writer, this.savedAuthChains); } catch (IOException e) { getLogger().error("Unable to write saved refresh tokens!", e); } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java index 86572d60c..d4999c80d 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.item.type; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java index c4aae46a2..6e791d674 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/SkullBlock.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.level.block.type; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index c7aabb806..f56a8a43f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -274,10 +274,10 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { private boolean couldLoginUserByName(String bedrockUsername) { if (geyser.getConfig().getSavedUserLogins().contains(bedrockUsername)) { - String refreshToken = geyser.refreshTokenFor(bedrockUsername); - if (refreshToken != null) { + String authChain = geyser.authChainFor(bedrockUsername); + if (authChain != null) { geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().name())); - session.authenticateWithRefreshToken(refreshToken); + session.authenticateWithAuthChain(authChain); return true; } } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index c2f94b1c6..f7e3bd43d 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -25,9 +25,8 @@ package org.geysermc.geyser.session; -import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.exception.request.RequestException; -import com.github.steveice10.mc.auth.service.MsaAuthenticationService; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import io.netty.channel.Channel; import io.netty.channel.EventLoop; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -41,22 +40,60 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import net.kyori.adventure.key.Key; +import net.raphimc.minecraftauth.step.java.StepMCProfile; +import net.raphimc.minecraftauth.step.java.StepMCToken; +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.value.qual.IntRange; -import org.cloudburstmc.math.vector.*; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector2i; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.BedrockDisconnectReasons; import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.data.*; +import org.cloudburstmc.protocol.bedrock.data.Ability; +import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; +import org.cloudburstmc.protocol.bedrock.data.AuthoritativeMovementMode; +import org.cloudburstmc.protocol.bedrock.data.ChatRestrictionLevel; +import org.cloudburstmc.protocol.bedrock.data.ExperimentData; +import org.cloudburstmc.protocol.bedrock.data.GamePublishSetting; +import org.cloudburstmc.protocol.bedrock.data.GameRuleData; +import org.cloudburstmc.protocol.bedrock.data.GameType; +import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; +import org.cloudburstmc.protocol.bedrock.data.SoundEvent; +import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType; import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData; import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.cloudburstmc.protocol.bedrock.packet.BiomeDefinitionListPacket; +import org.cloudburstmc.protocol.bedrock.packet.CameraPresetsPacket; +import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket; +import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; +import org.cloudburstmc.protocol.bedrock.packet.CreativeContentPacket; +import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket; +import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket; +import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; +import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket; +import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket; +import org.cloudburstmc.protocol.bedrock.packet.SyncEntityPropertyPacket; +import org.cloudburstmc.protocol.bedrock.packet.TextPacket; +import org.cloudburstmc.protocol.bedrock.packet.TransferPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAbilitiesPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAdventureSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateClientInputLocksPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateSoftEnumPacket; import org.cloudburstmc.protocol.common.util.OptionalBoolean; import org.geysermc.api.util.BedrockPlatform; import org.geysermc.api.util.InputMode; @@ -106,7 +143,22 @@ import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.auth.AuthData; import org.geysermc.geyser.session.auth.BedrockClientData; -import org.geysermc.geyser.session.cache.*; +import org.geysermc.geyser.session.cache.AdvancementsCache; +import org.geysermc.geyser.session.cache.BookEditCache; +import org.geysermc.geyser.session.cache.ChunkCache; +import org.geysermc.geyser.session.cache.EntityCache; +import org.geysermc.geyser.session.cache.EntityEffectCache; +import org.geysermc.geyser.session.cache.FormCache; +import org.geysermc.geyser.session.cache.LodestoneCache; +import org.geysermc.geyser.session.cache.PistonCache; +import org.geysermc.geyser.session.cache.PreferencesCache; +import org.geysermc.geyser.session.cache.RegistryCache; +import org.geysermc.geyser.session.cache.SkullCache; +import org.geysermc.geyser.session.cache.StructureBlockCache; +import org.geysermc.geyser.session.cache.TagCache; +import org.geysermc.geyser.session.cache.TeleportCache; +import org.geysermc.geyser.session.cache.WorldBorder; +import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.skin.FloodgateSkinUploader; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; @@ -116,9 +168,15 @@ import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; +import org.geysermc.geyser.util.MinecraftAuthLogger; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.geysermc.mcprotocollib.network.BuiltinFlags; import org.geysermc.mcprotocollib.network.Session; -import org.geysermc.mcprotocollib.network.event.session.*; +import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent; +import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent; +import org.geysermc.mcprotocollib.network.event.session.PacketErrorEvent; +import org.geysermc.mcprotocollib.network.event.session.PacketSendingEvent; +import org.geysermc.mcprotocollib.network.event.session.SessionAdapter; import org.geysermc.mcprotocollib.network.packet.Packet; import org.geysermc.mcprotocollib.network.tcp.TcpClientSession; import org.geysermc.mcprotocollib.network.tcp.TcpSession; @@ -153,7 +211,16 @@ import java.net.ConnectException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledFuture; @@ -163,6 +230,8 @@ import java.util.concurrent.atomic.AtomicInteger; @Getter public class GeyserSession implements GeyserConnection, GeyserCommandSource { + private static final Gson GSON = new Gson(); + private final GeyserImpl geyser; private final UpstreamSession upstream; private DownstreamSession downstream; @@ -690,7 +759,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } } - public void authenticateWithRefreshToken(String refreshToken) { + public void authenticateWithAuthChain(String authChain) { if (loggedIn) { geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", getAuthData().name())); return; @@ -699,24 +768,23 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { loggingIn = true; CompletableFuture.supplyAsync(() -> { - MsaAuthenticationService service = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); - service.setRefreshToken(refreshToken); + StepFullJavaSession step = PendingMicrosoftAuthentication.AUTH_FLOW.apply(true, 30); + StepFullJavaSession.FullJavaSession response; try { - service.login(); - } catch (RequestException e) { - geyser.getLogger().error("Error while attempting to use refresh token for " + bedrockUsername() + "!", e); + response = step.refresh(MinecraftAuthLogger.INSTANCE, PendingMicrosoftAuthentication.AUTH_CLIENT, step.fromJson(GSON.fromJson(authChain, JsonObject.class))); + } catch (Exception e) { + geyser.getLogger().error("Error while attempting to use auth chain for " + bedrockUsername() + "!", e); return Boolean.FALSE; } - GameProfile profile = service.getSelectedProfile(); - if (profile == null) { - // Java account is offline - disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode())); - return null; - } + StepMCProfile.MCProfile mcProfile = response.getMcProfile(); + StepMCToken.MCToken mcToken = mcProfile.getMcToken(); - protocol = new MinecraftProtocol(profile, service.getAccessToken()); - geyser.saveRefreshToken(bedrockUsername(), service.getRefreshToken()); + protocol = new MinecraftProtocol( + new GameProfile(mcProfile.getId(), mcProfile.getName()), + mcToken.getAccessToken() + ); + geyser.saveAuthChain(bedrockUsername(), GSON.toJson(step.toJson(response))); return Boolean.TRUE; }).whenComplete((successful, ex) -> { if (this.closed) { @@ -761,25 +829,15 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { final PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getOrCreateTask( getAuthData().xuid() ); - task.setOnline(true); - task.resetTimer(); - - if (task.getAuthentication().isDone()) { + if (task.getAuthentication() != null && task.getAuthentication().isDone()) { onMicrosoftLoginComplete(task); } else { - task.getCode(offlineAccess).whenComplete((response, ex) -> { - boolean connected = !closed; - if (ex != null) { - if (connected) { - geyser.getLogger().error("Failed to get Microsoft auth code", ex); - disconnect(ex.toString()); - } - task.cleanup(); // error getting auth code -> clean up immediately - } else if (connected) { - LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response); - task.getAuthentication().whenComplete((r, $) -> onMicrosoftLoginComplete(task)); + task.resetRunningFlow(); + task.performLoginAttempt(offlineAccess, code -> { + if (!closed) { + LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, code); } - }); + }).handle((r, e) -> onMicrosoftLoginComplete(task)); } } @@ -791,36 +849,32 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return false; } task.cleanup(); // player is online -> remove pending authentication immediately - Throwable ex = task.getLoginException(); - if (ex != null) { - geyser.getLogger().error("Failed to log in with Microsoft code!", ex); - disconnect(ex.toString()); - } else { - MsaAuthenticationService service = task.getMsaAuthenticationService(); - GameProfile selectedProfile = service.getSelectedProfile(); - if (selectedProfile == null) { - disconnect(GeyserLocale.getPlayerLocaleString( - "geyser.network.remote.invalid_account", - clientData.getLanguageCode() - )); - } else { - this.protocol = new MinecraftProtocol( - selectedProfile, - service.getAccessToken() - ); - try { - connectDownstream(); - } catch (Throwable t) { - t.printStackTrace(); - return false; - } + return task.getAuthentication().handle((result, ex) -> { + if (ex != null) { + geyser.getLogger().error("Failed to log in with Microsoft code!", ex); + disconnect(ex.toString()); + return false; + } - // Save our refresh token for later use - geyser.saveRefreshToken(bedrockUsername(), service.getRefreshToken()); - return true; - } - } - return false; + StepMCProfile.MCProfile mcProfile = result.session().getMcProfile(); + StepMCToken.MCToken mcToken = mcProfile.getMcToken(); + + this.protocol = new MinecraftProtocol( + new GameProfile(mcProfile.getId(), mcProfile.getName()), + mcToken.getAccessToken() + ); + + try { + connectDownstream(); + } catch (Throwable t) { + t.printStackTrace(); + return false; + } + + // Save our auth chain for later use + geyser.saveAuthChain(bedrockUsername(), GSON.toJson(result.step().toJson(result.session()))); + return true; + }).getNow(false); } /** @@ -1103,7 +1157,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { if (authData != null) { PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid()); if (task != null) { - task.setOnline(false); + task.resetRunningFlow(); } } } diff --git a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java index 0651039a0..7d0e2fbf9 100644 --- a/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java +++ b/core/src/main/java/org/geysermc/geyser/session/PendingMicrosoftAuthentication.java @@ -25,27 +25,44 @@ package org.geysermc.geyser.session; -import com.github.steveice10.mc.auth.exception.request.AuthPendingException; -import com.github.steveice10.mc.auth.exception.request.RequestException; -import com.github.steveice10.mc.auth.service.MsaAuthenticationService; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; +import net.lenni0451.commons.httpclient.HttpClient; +import net.raphimc.minecraftauth.MinecraftAuth; +import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; +import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode; +import net.raphimc.minecraftauth.util.MicrosoftConstants; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.util.MinecraftAuthLogger; -import java.io.Serial; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Consumer; /** * Pending Microsoft authentication task cache. * It permits user to exit the server while they authorize Geyser to access their Microsoft account. */ public class PendingMicrosoftAuthentication { + public static final HttpClient AUTH_CLIENT = MinecraftAuth.createHttpClient(); + public static final BiFunction AUTH_FLOW = (offlineAccess, timeoutSec) -> MinecraftAuth.builder() + .withClientId(GeyserImpl.OAUTH_CLIENT_ID) + .withScope(offlineAccess ? "XboxLive.signin XboxLive.offline_access" : "XboxLive.signin") + .withTimeout(timeoutSec) + .deviceCode() + .withoutDeviceToken() + .regularAuthentication(MicrosoftConstants.JAVA_XSTS_RELYING_PARTY) + .buildMinecraftJavaProfileStep(false); /** * For GeyserConnect usage. */ @@ -57,8 +74,8 @@ public class PendingMicrosoftAuthentication { .build(new CacheLoader<>() { @Override public AuthenticationTask load(@NonNull String userKey) { - return storeServerInformation ? new ProxyAuthenticationTask(userKey, timeoutSeconds * 1000L) - : new AuthenticationTask(userKey, timeoutSeconds * 1000L); + return storeServerInformation ? new ProxyAuthenticationTask(userKey, timeoutSeconds) + : new AuthenticationTask(userKey, timeoutSeconds); } }); } @@ -80,37 +97,23 @@ public class PendingMicrosoftAuthentication { public class AuthenticationTask { private static final Executor DELAYED_BY_ONE_SECOND = CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS); - @Getter - private final MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID); private final String userKey; - private final long timeoutMs; - - private long remainingTimeMs; - - @Setter - private boolean online = true; - + private final int timeoutSec; @Getter - private final CompletableFuture authentication; + private CompletableFuture authentication; - @Getter - private volatile Throwable loginException; - - private AuthenticationTask(String userKey, long timeoutMs) { + private AuthenticationTask(String userKey, int timeoutSec) { this.userKey = userKey; - this.timeoutMs = timeoutMs; - this.remainingTimeMs = timeoutMs; - - this.authentication = new CompletableFuture<>(); - this.authentication.whenComplete((r, ex) -> { - this.loginException = ex; - // avoid memory leak, in case player doesn't connect again - CompletableFuture.delayedExecutor(timeoutMs, TimeUnit.MILLISECONDS).execute(this::cleanup); - }); + this.timeoutSec = timeoutSec; } - public void resetTimer() { - this.remainingTimeMs = this.timeoutMs; + public void resetRunningFlow() { + if (authentication == null) { + return; + } + + // Interrupt the current flow + this.authentication.cancel(true); } public void cleanup() { @@ -121,52 +124,18 @@ public class PendingMicrosoftAuthentication { authentications.invalidate(userKey); } - public CompletableFuture getCode(boolean offlineAccess) { - // Request the code - CompletableFuture code = CompletableFuture.supplyAsync( - () -> tryGetCode(offlineAccess)); - // Once the code is received, continuously try to request the access token, profile, etc - code.thenRun(() -> performLoginAttempt(System.currentTimeMillis())); - return code; - } - - /** - * @param offlineAccess whether we want a refresh token for later use. - */ - private MsaAuthenticationService.MsCodeResponse tryGetCode(boolean offlineAccess) throws CompletionException { - try { - return msaAuthenticationService.getAuthCode(offlineAccess); - } catch (RequestException e) { - throw new CompletionException(e); - } - } - - private void performLoginAttempt(long lastAttempt) { - CompletableFuture.runAsync(() -> { + public CompletableFuture performLoginAttempt(boolean offlineAccess, Consumer deviceCodeConsumer) { + return authentication = CompletableFuture.supplyAsync(() -> { try { - msaAuthenticationService.login(); - } catch (AuthPendingException e) { - long currentAttempt = System.currentTimeMillis(); - if (!online) { - // decrement timer only when player's offline - remainingTimeMs -= currentAttempt - lastAttempt; - if (remainingTimeMs <= 0L) { - // time's up - authentication.completeExceptionally(new TaskTimeoutException()); - cleanup(); - return; - } - } - // try again in 1 second - performLoginAttempt(currentAttempt); - return; + StepFullJavaSession step = AUTH_FLOW.apply(offlineAccess, timeoutSec); + return new StepChainResult(step, step.getFromInput(MinecraftAuthLogger.INSTANCE, AUTH_CLIENT, new StepMsaDeviceCode.MsaDeviceCodeCallback(deviceCodeConsumer))); } catch (Exception e) { - authentication.completeExceptionally(e); - return; + throw new CompletionException(e); } - // login successful - authentication.complete(msaAuthenticationService); - }, DELAYED_BY_ONE_SECOND); + }, DELAYED_BY_ONE_SECOND).whenComplete((r, ex) -> { + // avoid memory leak, in case player doesn't connect again + CompletableFuture.delayedExecutor(timeoutSec, TimeUnit.SECONDS).execute(this::cleanup); + }); } @Override @@ -181,22 +150,11 @@ public class PendingMicrosoftAuthentication { private String server; private int port; - private ProxyAuthenticationTask(String userKey, long timeoutMs) { - super(userKey, timeoutMs); + private ProxyAuthenticationTask(String userKey, int timeoutSec) { + super(userKey, timeoutSec); } } - /** - * @see PendingMicrosoftAuthentication - */ - public static class TaskTimeoutException extends Exception { - - @Serial - private static final long serialVersionUID = 1L; - - TaskTimeoutException() { - super("It took too long to authorize Geyser to access your Microsoft account. " + - "Please request new code and try again."); - } + public record StepChainResult(StepFullJavaSession step, StepFullJavaSession.FullJavaSession session) { } } diff --git a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java index ef3ff3293..6f6bcb0ae 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -25,11 +25,10 @@ package org.geysermc.geyser.skin; -import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.data.GameProfile.Texture; -import com.github.steveice10.mc.auth.data.GameProfile.TextureModel; -import com.github.steveice10.mc.auth.data.GameProfile.TextureType; -import com.github.steveice10.mc.auth.exception.property.PropertyException; +import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile.Texture; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureModel; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -113,12 +112,7 @@ public class FakeHeadProvider { return; } - Map textures = null; - try { - textures = profile.getTextures(false); - } catch (PropertyException e) { - session.getGeyser().getLogger().debug("Failed to get textures from GameProfile: " + e); - } + Map textures = profile.getTextures(false); if (textures == null || textures.isEmpty()) { loadHead(session, entity, profile.getName()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index e9527872a..aa0c3eb43 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -25,10 +25,9 @@ package org.geysermc.geyser.translator.item; -import com.github.steveice10.mc.auth.data.GameProfile; -import com.github.steveice10.mc.auth.data.GameProfile.Texture; -import com.github.steveice10.mc.auth.data.GameProfile.TextureType; -import com.github.steveice10.mc.auth.exception.property.PropertyException; +import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile.Texture; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.NonNull; @@ -487,12 +486,7 @@ public final class ItemTranslator { GameProfile profile = components.get(DataComponentType.PROFILE); if (profile != null) { - Map textures = null; - try { - textures = profile.getTextures(false); - } catch (PropertyException e) { - GeyserImpl.getInstance().getLogger().debug("Failed to get textures from GameProfile: " + e); - } + Map textures = profile.getTextures(false); if (textures == null || textures.isEmpty()) { return null; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java index de2df0cb7..47c5bfd35 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSetLocalPlayerAsInitializedTranslator.java @@ -46,10 +46,10 @@ public class BedrockSetLocalPlayerAsInitializedTranslator extends PacketTranslat if (session.remoteServer().authType() == AuthType.ONLINE) { if (!session.isLoggedIn()) { if (session.getGeyser().getConfig().getSavedUserLogins().contains(session.bedrockUsername())) { - if (session.getGeyser().refreshTokenFor(session.bedrockUsername()) == null) { + if (session.getGeyser().authChainFor(session.bedrockUsername()) == null) { LoginEncryptionUtils.buildAndShowConsentWindow(session); } else { - // If the refresh token is not null and we're here, then the refresh token expired + // If the auth chain is not null and we're here, then it expired // and the expiration form has been cached session.getFormCache().resendAllForms(); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java index e7bde6a9d..47d1cff08 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.translator.protocol.java; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import net.kyori.adventure.key.Key; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.PlayerEntity; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java index f5ea4c08d..19f34db74 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java @@ -25,7 +25,7 @@ package org.geysermc.geyser.translator.protocol.java.entity.player; -import com.github.steveice10.mc.auth.data.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java index 478a6ef96..d3024be65 100644 --- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java @@ -28,7 +28,7 @@ package org.geysermc.geyser.util; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.steveice10.mc.auth.service.MsaAuthenticationService; +import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode; import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket; import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; @@ -203,7 +203,7 @@ public class LoginEncryptionUtils { /** * Shows the code that a user must input into their browser */ - public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse msCode) { + public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, StepMsaDeviceCode.MsaDeviceCode msCode) { String locale = session.locale(); StringBuilder message = new StringBuilder("%xbox.signin.website\n") @@ -212,7 +212,7 @@ public class LoginEncryptionUtils { .append(ChatColor.RESET) .append("\n%xbox.signin.enterCode\n") .append(ChatColor.GREEN) - .append(msCode.user_code); + .append(msCode.getUserCode()); int timeout = session.getGeyser().getConfig().getPendingAuthenticationTimeout(); if (timeout != 0) { message.append("\n\n") diff --git a/core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java b/core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java new file mode 100644 index 000000000..4e928d47e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/util/MinecraftAuthLogger.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 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.geyser.util; + +import net.raphimc.minecraftauth.util.logging.ILogger; +import org.geysermc.geyser.GeyserImpl; + +public class MinecraftAuthLogger implements ILogger { + + public static final MinecraftAuthLogger INSTANCE = new MinecraftAuthLogger(); + + @Override + public void info(String message) { + GeyserImpl.getInstance().getLogger().debug(message); + } + + @Override + public void warn(String message) { + GeyserImpl.getInstance().getLogger().warning(message); + } + + @Override + public void error(String message) { + GeyserImpl.getInstance().getLogger().error(message); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49c02d190..845589585 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ gson = "2.3.1" # Provided by Spigot 1.8.8 websocket = "1.5.1" protocol = "3.0.0.Beta2-20240704.153116-14" raknet = "1.0.0.CR3-20240416.144209-1" -mcauthlib = "e5b0bcc" -mcprotocollib = "1.21-20240616.154144-5" +minecraftauth = "4.1.0" +mcprotocollib = "1.21-20240718.102029-13" adventure = "4.14.0" adventure-platform = "4.3.0" junit = "5.9.2" @@ -107,7 +107,7 @@ commodore = { group = "me.lucko", name = "commodore", version.ref = "commodore" guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } -mcauthlib = { group = "com.github.GeyserMC", name = "MCAuthLib", version.ref = "mcauthlib" } +minecraftauth = { group = "net.raphimc", name = "MinecraftAuth", version.ref = "minecraftauth" } mcprotocollib = { group = "org.geysermc.mcprotocollib", name = "protocol", version.ref = "mcprotocollib" } raknet = { group = "org.cloudburstmc.netty", name = "netty-transport-raknet", version.ref = "raknet" } terminalconsoleappender = { group = "net.minecrell", name = "terminalconsoleappender", version.ref = "terminalconsoleappender" } From 5ebb16a1920e75dd91dec8217ef3f06a1d2a2355 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Thu, 25 Jul 2024 10:38:12 +0000 Subject: [PATCH 42/84] Update MCPL (#4897) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 845589585..58b5310ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ websocket = "1.5.1" protocol = "3.0.0.Beta2-20240704.153116-14" raknet = "1.0.0.CR3-20240416.144209-1" minecraftauth = "4.1.0" -mcprotocollib = "1.21-20240718.102029-13" +mcprotocollib = "1.21-20240725.013034-16" adventure = "4.14.0" adventure-platform = "4.3.0" junit = "5.9.2" From e994d6e1d6929750d63a81c57d0f0d8f3497673d Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:51:22 -0400 Subject: [PATCH 43/84] Bring in #4847 change --- .../org/geysermc/geyser/util/DimensionUtils.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java index cd1a690c3..b1408b817 100644 --- a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java @@ -27,9 +27,11 @@ package org.geysermc.geyser.util; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.PlayerActionType; import org.cloudburstmc.protocol.bedrock.packet.ChangeDimensionPacket; import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.MobEffectPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket; import org.cloudburstmc.protocol.bedrock.packet.StopSoundPacket; @@ -85,6 +87,20 @@ public class DimensionUtils { // Effects are re-sent from server entityEffects.clear(); + // Always reset weather, as it sometimes suddenly starts raining. See https://github.com/GeyserMC/Geyser/issues/3679 + LevelEventPacket stopRainPacket = new LevelEventPacket(); + stopRainPacket.setType(LevelEvent.STOP_RAINING); + stopRainPacket.setData(0); + stopRainPacket.setPosition(Vector3f.ZERO); + session.sendUpstreamPacket(stopRainPacket); + session.setRaining(false); + LevelEventPacket stopThunderPacket = new LevelEventPacket(); + stopThunderPacket.setType(LevelEvent.STOP_THUNDERSTORM); + stopThunderPacket.setData(0); + stopThunderPacket.setPosition(Vector3f.ZERO); + session.sendUpstreamPacket(stopThunderPacket); + session.setThunder(false); + finalizeDimensionSwitch(session, player); // If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension, From 663e3af7c8f10e5ae9e4fc5d76ad32b8730b26c9 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:22:10 -0400 Subject: [PATCH 44/84] Fix non-block items in stonecutters Fixes #4845 --- .../geysermc/geyser/item/type/BlockItem.java | 13 ++++++++++++ .../org/geysermc/geyser/item/type/Item.java | 11 +++++++--- .../java/JavaUpdateRecipesTranslator.java | 3 ++- .../geysermc/geyser/util/StatisticsUtils.java | 20 +++++++------------ 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BlockItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BlockItem.java index 30a31a100..c57e58469 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/BlockItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/BlockItem.java @@ -28,6 +28,9 @@ package org.geysermc.geyser.item.type; import org.geysermc.geyser.level.block.type.Block; public class BlockItem extends Item { + // If item is instanceof ItemNameBlockItem + private final boolean treatLikeBlock; + public BlockItem(Builder builder, Block block, Block... otherBlocks) { super(block.javaIdentifier().value(), builder); @@ -36,6 +39,7 @@ public class BlockItem extends Item { for (Block otherBlock : otherBlocks) { registerBlock(otherBlock, this); } + treatLikeBlock = true; } // Use this constructor if the item name is not the same as its primary block @@ -46,5 +50,14 @@ public class BlockItem extends Item { for (Block otherBlock : otherBlocks) { registerBlock(otherBlock, this); } + treatLikeBlock = false; + } + + @Override + public String translationKey() { + if (!treatLikeBlock) { + return super.translationKey(); + } + return "block." + this.javaIdentifier.namespace() + "." + this.javaIdentifier.value(); } } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 2417177ce..a8a477025 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.item.type; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -59,7 +60,7 @@ import java.util.Map; public class Item { private static final Map BLOCK_TO_ITEM = new HashMap<>(); - private final String javaIdentifier; + protected final Key javaIdentifier; private int javaId = -1; private final int stackSize; private final int attackDamage; @@ -68,7 +69,7 @@ public class Item { private final boolean glint; public Item(String javaIdentifier, Builder builder) { - this.javaIdentifier = MinecraftKey.key(javaIdentifier).asString().intern(); + this.javaIdentifier = MinecraftKey.key(javaIdentifier); this.stackSize = builder.stackSize; this.maxDamage = builder.maxDamage; this.attackDamage = builder.attackDamage; @@ -77,7 +78,7 @@ public class Item { } public String javaIdentifier() { - return javaIdentifier; + return javaIdentifier.asString(); } public int javaId() { @@ -108,6 +109,10 @@ public class Item { return false; } + public String translationKey() { + return "item." + javaIdentifier.namespace() + "." + javaIdentifier.value(); + } + /* Translation methods to Bedrock and back */ public ItemData.Builder translateToBedrock(int count, DataComponents components, ItemMapping mapping, ItemMappings mappings) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index fd8981552..7c36c505b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -253,7 +253,8 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator Registries.JAVA_ITEMS.get().get(stoneCuttingRecipeData.getResult().getId()) - .javaIdentifier()))); + // See RecipeManager#getRecipesFor as of 1.21 + .translationKey()))); // Now that it's sorted, let's translate these recipes int buttonId = 0; diff --git a/core/src/main/java/org/geysermc/geyser/util/StatisticsUtils.java b/core/src/main/java/org/geysermc/geyser/util/StatisticsUtils.java index 72fcb4fa6..9847c0cfc 100644 --- a/core/src/main/java/org/geysermc/geyser/util/StatisticsUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/StatisticsUtils.java @@ -107,7 +107,7 @@ public class StatisticsUtils { for (Object2IntMap.Entry entry : session.getStatistics().object2IntEntrySet()) { if (entry.getKey() instanceof BreakItemStatistic statistic) { - String item = itemRegistry.get(statistic.getId()).javaIdentifier(); + Item item = itemRegistry.get(statistic.getId()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); } } @@ -117,7 +117,7 @@ public class StatisticsUtils { for (Object2IntMap.Entry entry : session.getStatistics().object2IntEntrySet()) { if (entry.getKey() instanceof CraftItemStatistic statistic) { - String item = itemRegistry.get(statistic.getId()).javaIdentifier(); + Item item = itemRegistry.get(statistic.getId()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); } } @@ -127,7 +127,7 @@ public class StatisticsUtils { for (Object2IntMap.Entry entry : session.getStatistics().object2IntEntrySet()) { if (entry.getKey() instanceof UseItemStatistic statistic) { - String item = itemRegistry.get(statistic.getId()).javaIdentifier(); + Item item = itemRegistry.get(statistic.getId()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); } } @@ -137,7 +137,7 @@ public class StatisticsUtils { for (Object2IntMap.Entry entry : session.getStatistics().object2IntEntrySet()) { if (entry.getKey() instanceof PickupItemStatistic statistic) { - String item = itemRegistry.get(statistic.getId()).javaIdentifier(); + Item item = itemRegistry.get(statistic.getId()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); } } @@ -147,7 +147,7 @@ public class StatisticsUtils { for (Object2IntMap.Entry entry : session.getStatistics().object2IntEntrySet()) { if (entry.getKey() instanceof DropItemStatistic statistic) { - String item = itemRegistry.get(statistic.getId()).javaIdentifier(); + Item item = itemRegistry.get(statistic.getId()); content.add(getItemTranslateKey(item, language) + ": " + entry.getIntValue()); } } @@ -208,14 +208,8 @@ public class StatisticsUtils { * @param language the language to search in * @return the full name of the item */ - private static String getItemTranslateKey(String item, String language) { - item = item.replace("minecraft:", "item.minecraft."); - String translatedItem = MinecraftLocale.getLocaleString(item, language); - if (translatedItem.equals(item)) { - // Didn't translate; must be a block - translatedItem = MinecraftLocale.getLocaleString(item.replace("item.", "block."), language); - } - return translatedItem; + private static String getItemTranslateKey(Item item, String language) { + return MinecraftLocale.getLocaleString(item.translationKey(), language); } private static String translate(String keys, String locale) { From 258d6aadb436c29b0ea969c52564b571983c9c02 Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 27 Jul 2024 00:39:45 +0200 Subject: [PATCH 45/84] Fix: Bedrock players being able to always eat food while in peaceful difficulty (#4904) --- .../BedrockServerSettingsRequestTranslator.java | 10 ++++++++++ .../java/JavaChangeDifficultyTranslator.java | 14 +++++++++++--- .../org/geysermc/geyser/util/SettingsUtils.java | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockServerSettingsRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockServerSettingsRequestTranslator.java index aa7a2e40f..c7475e5d0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockServerSettingsRequestTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockServerSettingsRequestTranslator.java @@ -27,12 +27,14 @@ package org.geysermc.geyser.translator.protocol.bedrock; import org.cloudburstmc.protocol.bedrock.packet.ServerSettingsRequestPacket; import org.cloudburstmc.protocol.bedrock.packet.ServerSettingsResponsePacket; +import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket; import org.geysermc.cumulus.form.CustomForm; import org.geysermc.cumulus.form.impl.FormDefinitions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.SettingsUtils; +import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; import java.util.concurrent.TimeUnit; @@ -47,6 +49,14 @@ public class BedrockServerSettingsRequestTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, ClientboundChangeDifficultyPacket packet) { + Difficulty difficulty = packet.getDifficulty(); + session.getWorldCache().setDifficulty(difficulty); + + // Peaceful difficulty allows always eating food - hence, we just do not send it to Bedrock. + if (difficulty == Difficulty.PEACEFUL) { + difficulty = Difficulty.EASY; + } + SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket(); - setDifficultyPacket.setDifficulty(packet.getDifficulty().ordinal()); + setDifficultyPacket.setDifficulty(difficulty.ordinal()); session.sendUpstreamPacket(setDifficultyPacket); - session.getWorldCache().setDifficulty(packet.getDifficulty()); } } diff --git a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java index ed97408b9..6f46b191c 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.util; +import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket; import org.geysermc.cumulus.component.DropdownComponent; import org.geysermc.cumulus.form.CustomForm; import org.geysermc.geyser.GeyserImpl; @@ -33,6 +34,7 @@ import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.text.MinecraftLocale; +import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; public class SettingsUtils { /** @@ -96,6 +98,7 @@ public class SettingsUtils { } builder.validResultHandler((response) -> { + applyDifficultyFix(session); if (showClientSettings) { // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. if (showCoordinates) { @@ -134,9 +137,21 @@ public class SettingsUtils { } }); + builder.closedOrInvalidResultHandler($ -> applyDifficultyFix(session)); + return builder.build(); } + private static void applyDifficultyFix(GeyserSession session) { + // Peaceful difficulty allows always eating food - hence, we just do not send it to Bedrock. + // Since we sent the real difficulty before opening the server settings form, let's restore it to our workaround here + if (session.getWorldCache().getDifficulty() == Difficulty.PEACEFUL) { + SetDifficultyPacket setDifficultyPacket = new SetDifficultyPacket(); + setDifficultyPacket.setDifficulty(Difficulty.EASY.ordinal()); + session.sendUpstreamPacket(setDifficultyPacket); + } + } + private static String translateEntry(String key, String locale) { if (key.startsWith("%")) { // Bedrock will translate From 45f96a03e79b8227f8cd661ad08b423dda237042 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sun, 28 Jul 2024 12:56:41 -0400 Subject: [PATCH 46/84] Fix online mode no auth token dimension setting on login --- .../protocol/java/JavaLoginTranslator.java | 22 +++++++++++-------- .../geysermc/geyser/util/DimensionUtils.java | 5 +++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index 641313ee4..cf4b7058b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -26,28 +26,25 @@ package org.geysermc.geyser.translator.protocol.java; import net.kyori.adventure.key.Key; -import org.geysermc.erosion.Constants; -import org.geysermc.geyser.level.JavaDimension; -import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerSpawnInfo; -import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundLoginPacket; -import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.GameRuleData; -import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket; +import org.geysermc.erosion.Constants; import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler; +import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerSpawnInfo; +import org.geysermc.mcprotocollib.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundLoginPacket; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -83,6 +80,13 @@ public class JavaLoginTranslator extends PacketTranslator Date: Mon, 29 Jul 2024 00:16:15 -0700 Subject: [PATCH 47/84] 1.21.20 Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> --- .../geyser/impl/camera/CameraDefinitions.java | 14 +- .../geyser/network/CodecProcessor.java | 50 +- .../geysermc/geyser/network/GameProtocol.java | 19 +- .../geyser/network/UpstreamPacketHandler.java | 2 +- .../populator/BlockRegistryPopulator.java | 4 +- .../registry/populator/Conversion685_671.java | 12 +- .../registry/populator/Conversion712_685.java | 436 ++ .../populator/ItemRegistryPopulator.java | 4 +- .../inventory/InventoryTranslator.java | 4 +- .../resources/bedrock/biome_definitions.dat | Bin 41676 -> 41832 bytes .../bedrock/block_palette.1_21_20.nbt | Bin 0 -> 178977 bytes .../bedrock/creative_items.1_21_20.json | 6214 +++++++++++++++ .../resources/bedrock/entity_identifiers.dat | Bin 8314 -> 8314 bytes .../bedrock/runtime_item_states.1_21_20.json | 6794 +++++++++++++++++ core/src/main/resources/mappings | 2 +- gradle/libs.versions.toml | 2 +- 16 files changed, 13521 insertions(+), 36 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/Conversion712_685.java create mode 100644 core/src/main/resources/bedrock/block_palette.1_21_20.nbt create mode 100644 core/src/main/resources/bedrock/creative_items.1_21_20.json create mode 100644 core/src/main/resources/bedrock/runtime_item_states.1_21_20.json diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java index 80564bdf3..7bb25c9ef 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java @@ -43,13 +43,13 @@ public class CameraDefinitions { static { CAMERA_PRESETS = List.of( - new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, OptionalBoolean.empty()), - new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(false)), - new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.of(true)), - new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(true))); + new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()), + new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()), + new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()), + new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()), + new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(false)), + new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.of(true)), + new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(true))); SimpleDefinitionRegistry.Builder builder = SimpleDefinitionRegistry.builder(); for (int i = 0; i < CAMERA_PRESETS.size(); i++) { diff --git a/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java b/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java index e7cf81d47..fd18c01ce 100644 --- a/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java +++ b/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java @@ -40,6 +40,9 @@ import org.cloudburstmc.protocol.bedrock.codec.v407.serializer.InventorySlotSeri import org.cloudburstmc.protocol.bedrock.codec.v486.serializer.BossEventSerializer_v486; import org.cloudburstmc.protocol.bedrock.codec.v557.serializer.SetEntityDataSerializer_v557; import org.cloudburstmc.protocol.bedrock.codec.v662.serializer.SetEntityMotionSerializer_v662; +import org.cloudburstmc.protocol.bedrock.codec.v712.serializer.InventoryContentSerializer_v712; +import org.cloudburstmc.protocol.bedrock.codec.v712.serializer.InventorySlotSerializer_v712; +import org.cloudburstmc.protocol.bedrock.codec.v712.serializer.MobArmorEquipmentSerializer_v712; import org.cloudburstmc.protocol.bedrock.packet.AnvilDamagePacket; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.cloudburstmc.protocol.bedrock.packet.BossEventPacket; @@ -119,7 +122,17 @@ class CodecProcessor { /** * Serializer that throws an exception when trying to deserialize InventoryContentPacket since server-auth inventory is used. */ - private static final BedrockPacketSerializer INVENTORY_CONTENT_SERIALIZER = new InventoryContentSerializer_v407() { + private static final BedrockPacketSerializer INVENTORY_CONTENT_SERIALIZER_V407 = new InventoryContentSerializer_v407() { + @Override + public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventoryContentPacket packet) { + throw new IllegalArgumentException("Client cannot send InventoryContentPacket in server-auth inventory environment!"); + } + }; + + /** + * Serializer that throws an exception when trying to deserialize InventoryContentPacket since server-auth inventory is used. + */ + private static final BedrockPacketSerializer INVENTORY_CONTENT_SERIALIZER_V712 = new InventoryContentSerializer_v712() { @Override public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventoryContentPacket packet) { throw new IllegalArgumentException("Client cannot send InventoryContentPacket in server-auth inventory environment!"); @@ -129,7 +142,17 @@ class CodecProcessor { /** * Serializer that throws an exception when trying to deserialize InventorySlotPacket since server-auth inventory is used. */ - private static final BedrockPacketSerializer INVENTORY_SLOT_SERIALIZER = new InventorySlotSerializer_v407() { + private static final BedrockPacketSerializer INVENTORY_SLOT_SERIALIZER_V407 = new InventorySlotSerializer_v407() { + @Override + public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventorySlotPacket packet) { + throw new IllegalArgumentException("Client cannot send InventorySlotPacket in server-auth inventory environment!"); + } + }; + + /* + * Serializer that throws an exception when trying to deserialize InventorySlotPacket since server-auth inventory is used. + */ + private static final BedrockPacketSerializer INVENTORY_SLOT_SERIALIZER_V712 = new InventorySlotSerializer_v712() { @Override public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventorySlotPacket packet) { throw new IllegalArgumentException("Client cannot send InventorySlotPacket in server-auth inventory environment!"); @@ -148,7 +171,16 @@ class CodecProcessor { /** * Serializer that does nothing when trying to deserialize MobArmorEquipmentPacket since it is not used from the client. */ - private static final BedrockPacketSerializer MOB_ARMOR_EQUIPMENT_SERIALIZER = new MobArmorEquipmentSerializer_v291() { + private static final BedrockPacketSerializer MOB_ARMOR_EQUIPMENT_SERIALIZER_V291 = new MobArmorEquipmentSerializer_v291() { + @Override + public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, MobArmorEquipmentPacket packet) { + } + }; + + /** + * Serializer that does nothing when trying to deserialize MobArmorEquipmentPacket since it is not used from the client. + */ + private static final BedrockPacketSerializer MOB_ARMOR_EQUIPMENT_SERIALIZER_V712 = new MobArmorEquipmentSerializer_v712() { @Override public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, MobArmorEquipmentPacket packet) { } @@ -193,7 +225,7 @@ class CodecProcessor { /** * Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client for codec v662. */ - private static final BedrockPacketSerializer SET_ENTITY_MOTION_SERIALIZER_V662 = new SetEntityMotionSerializer_v662() { + private static final BedrockPacketSerializer SET_ENTITY_MOTION_SERIALIZER = new SetEntityMotionSerializer_v662() { @Override public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, SetEntityMotionPacket packet) { } @@ -224,6 +256,8 @@ class CodecProcessor { @SuppressWarnings("unchecked") static BedrockCodec processCodec(BedrockCodec codec) { + boolean isPre712 = codec.getProtocolVersion() < 712; + BedrockCodec.Builder codecBuilder = codec.toBuilder() // Illegal unused serverbound EDU packets .updateSerializer(PhotoTransferPacket.class, ILLEGAL_SERIALIZER) @@ -252,15 +286,15 @@ class CodecProcessor { .updateSerializer(AnvilDamagePacket.class, IGNORED_SERIALIZER) .updateSerializer(RefreshEntitlementsPacket.class, IGNORED_SERIALIZER) // Illegal when serverbound due to Geyser specific setup - .updateSerializer(InventoryContentPacket.class, INVENTORY_CONTENT_SERIALIZER) - .updateSerializer(InventorySlotPacket.class, INVENTORY_SLOT_SERIALIZER) + .updateSerializer(InventoryContentPacket.class, isPre712 ? INVENTORY_CONTENT_SERIALIZER_V407 : INVENTORY_CONTENT_SERIALIZER_V712) + .updateSerializer(InventorySlotPacket.class, isPre712 ? INVENTORY_SLOT_SERIALIZER_V407 : INVENTORY_SLOT_SERIALIZER_V712) // Ignored only when serverbound .updateSerializer(BossEventPacket.class, BOSS_EVENT_SERIALIZER) - .updateSerializer(MobArmorEquipmentPacket.class, MOB_ARMOR_EQUIPMENT_SERIALIZER) + .updateSerializer(MobArmorEquipmentPacket.class, isPre712 ? MOB_ARMOR_EQUIPMENT_SERIALIZER_V291 : MOB_ARMOR_EQUIPMENT_SERIALIZER_V712) .updateSerializer(PlayerHotbarPacket.class, PLAYER_HOTBAR_SERIALIZER) .updateSerializer(PlayerSkinPacket.class, PLAYER_SKIN_SERIALIZER) .updateSerializer(SetEntityDataPacket.class, SET_ENTITY_DATA_SERIALIZER) - .updateSerializer(SetEntityMotionPacket.class, SET_ENTITY_MOTION_SERIALIZER_V662) + .updateSerializer(SetEntityMotionPacket.class, SET_ENTITY_MOTION_SERIALIZER) .updateSerializer(SetEntityLinkPacket.class, SET_ENTITY_LINK_SERIALIZER) // Valid serverbound packets where reading of some fields can be skipped .updateSerializer(MobEquipmentPacket.class, MOB_EQUIPMENT_SERIALIZER) diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 8f3f00021..18dee94e6 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -29,6 +29,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671; import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685; +import org.cloudburstmc.protocol.bedrock.codec.v686.Bedrock_v686; +import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712; import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec; @@ -43,17 +45,13 @@ import java.util.StringJoiner; */ public final class GameProtocol { - // Surprise protocol bump WOW - private static final BedrockCodec BEDROCK_V686 = Bedrock_v685.CODEC.toBuilder() - .protocolVersion(686) - .minecraftVersion("1.21.2") - .build(); - /** * Default Bedrock codec that should act as a fallback. Should represent the latest available * release of the game that Geyser supports. */ - public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(BEDROCK_V686); + public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder() + .minecraftVersion("1.21.20") + .build()); /** * A list of all supported Bedrock versions that can join Geyser @@ -73,9 +71,10 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v685.CODEC.toBuilder() .minecraftVersion("1.21.0/1.21.1") .build())); - SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder() - .minecraftVersion("1.21.2/1.21.3") - .build()); + SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v686.CODEC.toBuilder() + .minecraftVersion("1.21.2") + .build())); + SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } /** diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index f56a8a43f..e9c979b0c 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -209,7 +209,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { ResourcePackManifest.Header header = pack.manifest().header(); resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry( header.uuid().toString(), header.version().toString(), codec.size(), pack.contentKey(), - "", header.uuid().toString(), false, false)); + "", header.uuid().toString(), false, false, false)); } resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); session.sendUpstreamPacket(resourcePacksInfo); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index d7dc989da..33c2bc97b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -38,6 +38,7 @@ import it.unimi.dsi.fastutil.objects.*; import org.cloudburstmc.nbt.*; import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671; import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685; +import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712; import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.geysermc.geyser.GeyserImpl; @@ -108,7 +109,8 @@ public final class BlockRegistryPopulator { private static void registerBedrockBlocks() { var blockMappers = ImmutableMap., Remapper>builder() .put(ObjectIntPair.of("1_20_80", Bedrock_v671.CODEC.getProtocolVersion()), Conversion685_671::remapBlock) - .put(ObjectIntPair.of("1_21_0", Bedrock_v685.CODEC.getProtocolVersion()), tag -> tag) + .put(ObjectIntPair.of("1_21_0", Bedrock_v685.CODEC.getProtocolVersion()), Conversion712_685::remapBlock) + .put(ObjectIntPair.of("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()), tag -> tag) .build(); // We can keep this strong as nothing should be garbage collected diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion685_671.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion685_671.java index 58886ca57..0c7f540bf 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion685_671.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion685_671.java @@ -45,6 +45,8 @@ public class Conversion685_671 { private static final List NEW_MUSIC_DISCS = List.of(Items.MUSIC_DISC_CREATOR, Items.MUSIC_DISC_CREATOR_MUSIC_BOX, Items.MUSIC_DISC_PRECIPICE); static GeyserMappingItem remapItem(Item item, GeyserMappingItem mapping) { + mapping = Conversion712_685.remapItem(item, mapping); + String identifer = mapping.getBedrockIdentifier(); if (NEW_MUSIC_DISCS.contains(item)) { @@ -111,6 +113,8 @@ public class Conversion685_671 { } static NbtMap remapBlock(NbtMap tag) { + tag = Conversion712_685.remapBlock(tag); + final String name = tag.getString("name"); if (!MODIFIED_BLOCKS.contains(name)) { @@ -130,7 +134,7 @@ public class Conversion685_671 { String coralColor; boolean deadBit = name.startsWith("minecraft:dead_"); - switch(name) { + switch (name) { case "minecraft:tube_coral_block", "minecraft:dead_tube_coral_block" -> coralColor = "blue"; case "minecraft:brain_coral_block", "minecraft:dead_brain_coral_block" -> coralColor = "pink"; case "minecraft:bubble_coral_block", "minecraft:dead_bubble_coral_block" -> coralColor = "purple"; @@ -152,7 +156,7 @@ public class Conversion685_671 { replacement = "minecraft:double_plant"; String doublePlantType; - switch(name) { + switch (name) { case "minecraft:sunflower" -> doublePlantType = "sunflower"; case "minecraft:lilac" -> doublePlantType = "syringa"; case "minecraft:tall_grass" -> doublePlantType = "grass"; @@ -174,7 +178,7 @@ public class Conversion685_671 { replacement = "minecraft:stone_block_slab"; String stoneSlabType; - switch(name) { + switch (name) { case "minecraft:smooth_stone_slab" -> stoneSlabType = "smooth_stone"; case "minecraft:sandstone_slab" -> stoneSlabType = "sandstone"; case "minecraft:petrified_oak_slab" -> stoneSlabType = "wood"; @@ -198,7 +202,7 @@ public class Conversion685_671 { replacement = "minecraft:tallgrass"; String tallGrassType; - switch(name) { + switch (name) { case "minecraft:short_grass" -> tallGrassType = "tall"; case "minecraft:fern" -> tallGrassType = "fern"; default -> throw new IllegalStateException("Unexpected value: " + name); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion712_685.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion712_685.java new file mode 100644 index 000000000..557a38f1f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion712_685.java @@ -0,0 +1,436 @@ +package org.geysermc.geyser.registry.populator; + +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.type.GeyserMappingItem; + +import java.util.List; +import java.util.stream.Stream; + +public class Conversion712_685 { + private static final List NEW_STONE_BLOCK_SLABS_2 = List.of("minecraft:prismarine_slab", "minecraft:dark_prismarine_slab", "minecraft:smooth_sandstone_slab", "minecraft:purpur_slab", "minecraft:red_nether_brick_slab", "minecraft:prismarine_brick_slab", "minecraft:mossy_cobblestone_slab", "minecraft:red_sandstone_slab"); + private static final List NEW_STONE_BLOCK_SLABS_3 = List.of("minecraft:smooth_red_sandstone_slab", "minecraft:polished_granite_slab", "minecraft:granite_slab", "minecraft:polished_diorite_slab", "minecraft:andesite_slab", "minecraft:polished_andesite_slab", "minecraft:diorite_slab", "minecraft:end_stone_brick_slab"); + private static final List NEW_STONE_BLOCK_SLABS_4 = List.of("minecraft:smooth_quartz_slab", "minecraft:cut_sandstone_slab", "minecraft:cut_red_sandstone_slab", "minecraft:normal_stone_slab", "minecraft:mossy_stone_brick_slab"); + private static final List NEW_DOUBLE_STONE_BLOCK_SLABS = List.of("minecraft:quartz_double_slab", "minecraft:petrified_oak_double_slab", "minecraft:stone_brick_double_slab", "minecraft:brick_double_slab", "minecraft:sandstone_double_slab", "minecraft:nether_brick_double_slab", "minecraft:cobblestone_double_slab", "minecraft:smooth_stone_double_slab"); + private static final List NEW_DOUBLE_STONE_BLOCK_SLABS_2 = List.of("minecraft:prismarine_double_slab", "minecraft:dark_prismarine_double_slab", "minecraft:smooth_sandstone_double_slab", "minecraft:purpur_double_slab", "minecraft:red_nether_brick_double_slab", "minecraft:prismarine_brick_double_slab", "minecraft:mossy_cobblestone_double_slab", "minecraft:red_sandstone_double_slab"); + private static final List NEW_DOUBLE_STONE_BLOCK_SLABS_3 = List.of("minecraft:smooth_red_sandstone_double_slab", "minecraft:polished_granite_double_slab", "minecraft:granite_double_slab", "minecraft:polished_diorite_double_slab", "minecraft:andesite_double_slab", "minecraft:polished_andesite_double_slab", "minecraft:diorite_double_slab", "minecraft:end_stone_brick_double_slab"); + private static final List NEW_DOUBLE_STONE_BLOCK_SLABS_4 = List.of("minecraft:smooth_quartz_double_slab", "minecraft:cut_sandstone_double_slab", "minecraft:cut_red_sandstone_double_slab", "minecraft:normal_stone_double_slab", "minecraft:mossy_stone_brick_double_slab"); + private static final List NEW_PRISMARINE_BLOCKS = List.of("minecraft:prismarine_bricks", "minecraft:dark_prismarine", "minecraft:prismarine"); + private static final List NEW_CORAL_FAN_HANGS = List.of("minecraft:tube_coral_wall_fan", "minecraft:brain_coral_wall_fan", "minecraft:dead_tube_coral_wall_fan", "minecraft:dead_brain_coral_wall_fan"); + private static final List NEW_CORAL_FAN_HANGS_2 = List.of("minecraft:bubble_coral_wall_fan", "minecraft:fire_coral_wall_fan", "minecraft:dead_bubble_coral_wall_fan", "minecraft:dead_fire_coral_wall_fan"); + private static final List NEW_CORAL_FAN_HANGS_3 = List.of("minecraft:horn_coral_wall_fan", "minecraft:dead_horn_coral_wall_fan"); + private static final List NEW_MONSTER_EGGS = List.of("minecraft:infested_cobblestone", "minecraft:infested_stone_bricks", "minecraft:infested_mossy_stone_bricks", "minecraft:infested_cracked_stone_bricks", "minecraft:infested_chiseled_stone_bricks", "minecraft:infested_stone"); + private static final List NEW_STONEBRICK_BLOCKS = List.of("minecraft:mossy_stone_bricks", "minecraft:cracked_stone_bricks", "minecraft:chiseled_stone_bricks", "minecraft:smooth_stone_bricks", "minecraft:stone_bricks"); + private static final List NEW_LIGHT_BLOCKS = List.of("minecraft:light_block_0", "minecraft:light_block_1", "minecraft:light_block_2", "minecraft:light_block_3", "minecraft:light_block_4", "minecraft:light_block_5", "minecraft:light_block_6", "minecraft:light_block_7", "minecraft:light_block_8", "minecraft:light_block_9", "minecraft:light_block_10", "minecraft:light_block_11", "minecraft:light_block_12", "minecraft:light_block_13", "minecraft:light_block_14", "minecraft:light_block_15"); + private static final List NEW_SANDSTONE_BLOCKS = List.of("minecraft:cut_sandstone", "minecraft:chiseled_sandstone", "minecraft:smooth_sandstone", "minecraft:sandstone"); + private static final List NEW_QUARTZ_BLOCKS = List.of("minecraft:chiseled_quartz_block", "minecraft:quartz_pillar", "minecraft:smooth_quartz", "minecraft:quartz_block"); + private static final List NEW_RED_SANDSTONE_BLOCKS = List.of("minecraft:cut_red_sandstone", "minecraft:chiseled_red_sandstone", "minecraft:smooth_red_sandstone", "minecraft:red_sandstone"); + private static final List NEW_SAND_BLOCKS = List.of("minecraft:red_sand", "minecraft:sand"); + private static final List NEW_DIRT_BLOCKS = List.of("minecraft:coarse_dirt", "minecraft:dirt"); + private static final List NEW_ANVILS = List.of("minecraft:damaged_anvil", "minecraft:chipped_anvil", "minecraft:deprecated_anvil", "minecraft:anvil"); + private static final List NEW_YELLOW_FLOWERS = List.of("minecraft:dandelion"); + private static final List NEW_BLOCKS = Stream.of(NEW_STONE_BLOCK_SLABS_2, NEW_STONE_BLOCK_SLABS_3, NEW_STONE_BLOCK_SLABS_4, NEW_DOUBLE_STONE_BLOCK_SLABS, NEW_DOUBLE_STONE_BLOCK_SLABS_2, NEW_DOUBLE_STONE_BLOCK_SLABS_3, NEW_DOUBLE_STONE_BLOCK_SLABS_4, NEW_PRISMARINE_BLOCKS, NEW_CORAL_FAN_HANGS, NEW_CORAL_FAN_HANGS_2, NEW_CORAL_FAN_HANGS_3, NEW_MONSTER_EGGS, NEW_STONEBRICK_BLOCKS, NEW_LIGHT_BLOCKS, NEW_SANDSTONE_BLOCKS, NEW_QUARTZ_BLOCKS, NEW_RED_SANDSTONE_BLOCKS, NEW_SAND_BLOCKS, NEW_DIRT_BLOCKS, NEW_ANVILS, NEW_YELLOW_FLOWERS).flatMap(List::stream).toList(); + + static GeyserMappingItem remapItem(Item item, GeyserMappingItem mapping) { + String identifer = mapping.getBedrockIdentifier(); + + if (!NEW_BLOCKS.contains(identifer)) { + return mapping; + } + + if (identifer.equals("minecraft:coarse_dirt")) { + return mapping.withBedrockIdentifier("minecraft:dirt").withBedrockData(1); + } + + if (identifer.equals("minecraft:dandelion")) { + return mapping.withBedrockIdentifier("minecraft:yellow_flower").withBedrockData(0); + } + + if (identifer.equals("minecraft:red_sand")) { + return mapping.withBedrockIdentifier("minecraft:sand").withBedrockData(1); + } + + if (NEW_PRISMARINE_BLOCKS.contains(identifer)) { + switch (identifer) { + case "minecraft:prismarine" -> { return mapping.withBedrockIdentifier("minecraft:prismarine").withBedrockData(0); } + case "minecraft:dark_prismarine" -> { return mapping.withBedrockIdentifier("minecraft:prismarine").withBedrockData(1); } + case "minecraft:prismarine_bricks" -> { return mapping.withBedrockIdentifier("minecraft:prismarine").withBedrockData(2); } + } + } + + if (NEW_SANDSTONE_BLOCKS.contains(identifer)) { + switch (identifer) { + case "minecraft:sandstone" -> { return mapping.withBedrockIdentifier("minecraft:sandstone").withBedrockData(0); } + case "minecraft:chiseled_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:sandstone").withBedrockData(1); } + case "minecraft:cut_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:sandstone").withBedrockData(2); } + case "minecraft:smooth_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:sandstone").withBedrockData(3); } + } + } + + if (NEW_RED_SANDSTONE_BLOCKS.contains(identifer)) { + switch (identifer) { + case "minecraft:red_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:red_sandstone").withBedrockData(0); } + case "minecraft:chiseled_red_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:red_sandstone").withBedrockData(1); } + case "minecraft:cut_red_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:red_sandstone").withBedrockData(2); } + case "minecraft:smooth_red_sandstone" -> { return mapping.withBedrockIdentifier("minecraft:red_sandstone").withBedrockData(3); } + } + } + + if (NEW_QUARTZ_BLOCKS.contains(identifer)) { + switch (identifer) { + case "minecraft:quartz_block" -> { return mapping.withBedrockIdentifier("minecraft:quartz_block").withBedrockData(0); } + case "minecraft:chiseled_quartz_block" -> { return mapping.withBedrockIdentifier("minecraft:quartz_block").withBedrockData(1); } + case "minecraft:quartz_pillar" -> { return mapping.withBedrockIdentifier("minecraft:quartz_block").withBedrockData(2); } + case "minecraft:smooth_quartz" -> { return mapping.withBedrockIdentifier("minecraft:quartz_block").withBedrockData(3); } + } + } + + if (NEW_STONE_BLOCK_SLABS_2.contains(identifer)) { + switch (identifer) { + case "minecraft:red_sandstone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(0); } + case "minecraft:purpur_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(1); } + case "minecraft:prismarine_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(2); } + case "minecraft:dark_prismarine_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(3); } + case "minecraft:prismarine_brick_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(4); } + case "minecraft:mossy_cobblestone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(5); } + case "minecraft:smooth_sandstone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(6); } + case "minecraft:red_nether_brick_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab2").withBedrockData(7); } + } + } + + if (NEW_STONE_BLOCK_SLABS_3.contains(identifer)) { + switch (identifer) { + case "minecraft:end_stone_brick_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(0); } + case "minecraft:smooth_red_sandstone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(1); } + case "minecraft:polished_andesite_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(2); } + case "minecraft:andesite_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(3); } + case "minecraft:diorite_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(4); } + case "minecraft:polished_diorite_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(5); } + case "minecraft:granite_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(6); } + case "minecraft:polished_granite_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab3").withBedrockData(7); } + } + } + + if (NEW_STONE_BLOCK_SLABS_4.contains(identifer)) { + switch (identifer) { + case "minecraft:mossy_stone_brick_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab4").withBedrockData(0); } + case "minecraft:smooth_quartz_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab4").withBedrockData(1); } + case "minecraft:normal_stone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab4").withBedrockData(2); } + case "minecraft:cut_sandstone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab4").withBedrockData(3); } + case "minecraft:cut_red_sandstone_slab" -> { return mapping.withBedrockIdentifier("minecraft:stone_block_slab4").withBedrockData(4); } + } + } + + if (NEW_MONSTER_EGGS.contains(identifer)) { + switch (identifer) { + case "minecraft:infested_stone" -> { return mapping.withBedrockIdentifier("minecraft:monster_egg").withBedrockData(0); } + case "minecraft:infested_cobblestone" -> { return mapping.withBedrockIdentifier("minecraft:monster_egg").withBedrockData(1); } + case "minecraft:infested_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:monster_egg").withBedrockData(2); } + case "minecraft:infested_mossy_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:monster_egg").withBedrockData(3); } + case "minecraft:infested_cracked_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:monster_egg").withBedrockData(4); } + case "minecraft:infested_chiseled_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:monster_egg").withBedrockData(5); } + } + } + + if (NEW_STONEBRICK_BLOCKS.contains(identifer)) { + switch (identifer) { + case "minecraft:stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:stonebrick").withBedrockData(0); } + case "minecraft:mossy_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:stonebrick").withBedrockData(1); } + case "minecraft:cracked_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:stonebrick").withBedrockData(2); } + case "minecraft:chiseled_stone_bricks" -> { return mapping.withBedrockIdentifier("minecraft:stonebrick").withBedrockData(3); } + } + } + + if (NEW_ANVILS.contains(identifer)) { + switch (identifer) { + case "minecraft:anvil" -> { return mapping.withBedrockIdentifier("minecraft:anvil").withBedrockData(0); } + case "minecraft:chipped_anvil" -> { return mapping.withBedrockIdentifier("minecraft:anvil").withBedrockData(4); } + case "minecraft:damaged_anvil" -> { return mapping.withBedrockIdentifier("minecraft:anvil").withBedrockData(8); } + } + } + + return mapping; + } + + static NbtMap remapBlock(NbtMap tag) { + final String name = tag.getString("name"); + + if (!NEW_BLOCKS.contains(name)) { + return tag; + } + + String replacement; + + if (NEW_DOUBLE_STONE_BLOCK_SLABS.contains(name)) { + replacement = "minecraft:double_stone_block_slab"; + String stoneSlabType; + + switch (name) { + case "minecraft:quartz_double_slab" -> stoneSlabType = "quartz"; + case "minecraft:petrified_oak_double_slab" -> stoneSlabType = "wood"; + case "minecraft:stone_brick_double_slab" -> stoneSlabType = "stone_brick"; + case "minecraft:brick_double_slab" -> stoneSlabType = "brick"; + case "minecraft:sandstone_double_slab" -> stoneSlabType = "sandstone"; + case "minecraft:nether_brick_double_slab" -> stoneSlabType = "nether_brick"; + case "minecraft:cobblestone_double_slab" -> stoneSlabType = "cobblestone"; + case "minecraft:smooth_stone_double_slab" -> stoneSlabType = "smooth_stone"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("stone_slab_type", stoneSlabType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_STONE_BLOCK_SLABS_2.contains(name) || NEW_DOUBLE_STONE_BLOCK_SLABS_2.contains(name)) { + replacement = NEW_STONE_BLOCK_SLABS_2.contains(name) ? "minecraft:stone_block_slab2" : "minecraft:double_stone_block_slab2"; + String stoneSlabType2; + + switch (name) { + case "minecraft:prismarine_slab", "minecraft:prismarine_double_slab" -> stoneSlabType2 = "prismarine_rough"; + case "minecraft:dark_prismarine_slab", "minecraft:dark_prismarine_double_slab" -> stoneSlabType2 = "prismarine_dark"; + case "minecraft:smooth_sandstone_slab", "minecraft:smooth_sandstone_double_slab" -> stoneSlabType2 = "smooth_sandstone"; + case "minecraft:purpur_slab", "minecraft:purpur_double_slab" -> stoneSlabType2 = "purpur"; + case "minecraft:red_nether_brick_slab", "minecraft:red_nether_brick_double_slab" -> stoneSlabType2 = "red_nether_brick"; + case "minecraft:prismarine_brick_slab", "minecraft:prismarine_brick_double_slab" -> stoneSlabType2 = "prismarine_brick"; + case "minecraft:mossy_cobblestone_slab", "minecraft:mossy_cobblestone_double_slab" -> stoneSlabType2 = "mossy_cobblestone"; + case "minecraft:red_sandstone_slab", "minecraft:red_sandstone_double_slab" -> stoneSlabType2 = "red_sandstone"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("stone_slab_type_2", stoneSlabType2) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_STONE_BLOCK_SLABS_3.contains(name) || NEW_DOUBLE_STONE_BLOCK_SLABS_3.contains(name)) { + replacement = NEW_STONE_BLOCK_SLABS_3.contains(name) ? "minecraft:stone_block_slab3" : "minecraft:double_stone_block_slab3"; + String stoneSlabType3; + + switch (name) { + case "minecraft:smooth_red_sandstone_slab", "minecraft:smooth_red_sandstone_double_slab" -> stoneSlabType3 = "smooth_red_sandstone"; + case "minecraft:polished_granite_slab", "minecraft:polished_granite_double_slab" -> stoneSlabType3 = "polished_granite"; + case "minecraft:granite_slab", "minecraft:granite_double_slab" -> stoneSlabType3 = "granite"; + case "minecraft:polished_diorite_slab", "minecraft:polished_diorite_double_slab" -> stoneSlabType3 = "polished_diorite"; + case "minecraft:andesite_slab", "minecraft:andesite_double_slab" -> stoneSlabType3 = "andesite"; + case "minecraft:polished_andesite_slab", "minecraft:polished_andesite_double_slab" -> stoneSlabType3 = "polished_andesite"; + case "minecraft:diorite_slab", "minecraft:diorite_double_slab" -> stoneSlabType3 = "diorite"; + case "minecraft:end_stone_brick_slab", "minecraft:end_stone_brick_double_slab" -> stoneSlabType3 = "end_stone_brick"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("stone_slab_type_3", stoneSlabType3) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_STONE_BLOCK_SLABS_4.contains(name) || NEW_DOUBLE_STONE_BLOCK_SLABS_4.contains(name)) { + replacement = NEW_STONE_BLOCK_SLABS_4.contains(name) ? "minecraft:stone_block_slab4" : "minecraft:double_stone_block_slab4"; + String stoneSlabType4; + + switch (name) { + case "minecraft:smooth_quartz_slab", "minecraft:smooth_quartz_double_slab" -> stoneSlabType4 = "smooth_quartz"; + case "minecraft:cut_sandstone_slab", "minecraft:cut_sandstone_double_slab" -> stoneSlabType4 = "cut_sandstone"; + case "minecraft:cut_red_sandstone_slab", "minecraft:cut_red_sandstone_double_slab" -> stoneSlabType4 = "cut_red_sandstone"; + case "minecraft:normal_stone_slab", "minecraft:normal_stone_double_slab" -> stoneSlabType4 = "stone"; + case "minecraft:mossy_stone_brick_slab", "minecraft:mossy_stone_brick_double_slab" -> stoneSlabType4 = "mossy_stone_brick"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("stone_slab_type_4", stoneSlabType4) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_PRISMARINE_BLOCKS.contains(name)) { + replacement = "minecraft:prismarine"; + String prismarineBlockType; + + switch (name) { + case "minecraft:prismarine_bricks" -> prismarineBlockType = "bricks"; + case "minecraft:dark_prismarine" -> prismarineBlockType = "dark"; + case "minecraft:prismarine" -> prismarineBlockType = "default"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("prismarine_block_type", prismarineBlockType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_CORAL_FAN_HANGS.contains(name) || NEW_CORAL_FAN_HANGS_2.contains(name) || NEW_CORAL_FAN_HANGS_3.contains(name)) { + replacement = NEW_CORAL_FAN_HANGS.contains(name) ? "minecraft:coral_fan_hang" : NEW_CORAL_FAN_HANGS_2.contains(name) ? "minecraft:coral_fan_hang2" : "minecraft:coral_fan_hang3"; + boolean deadBit = name.startsWith("minecraft:dead_"); + boolean coralHangTypeBit = name.contains("brain") || name.contains("fire"); + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putBoolean("coral_hang_type_bit", coralHangTypeBit) + .putBoolean("dead_bit", deadBit) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_MONSTER_EGGS.contains(name)) { + replacement = "minecraft:monster_egg"; + String monsterEggStoneType; + + switch (name) { + case "minecraft:infested_cobblestone" -> monsterEggStoneType = "cobblestone"; + case "minecraft:infested_stone_bricks" -> monsterEggStoneType = "stone_brick"; + case "minecraft:infested_mossy_stone_bricks" -> monsterEggStoneType = "mossy_stone_brick"; + case "minecraft:infested_cracked_stone_bricks" -> monsterEggStoneType = "cracked_stone_brick"; + case "minecraft:infested_chiseled_stone_bricks" -> monsterEggStoneType = "chiseled_stone_brick"; + case "minecraft:infested_stone" -> monsterEggStoneType = "stone"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("monster_egg_stone_type", monsterEggStoneType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_STONEBRICK_BLOCKS.contains(name)) { + replacement = "minecraft:stonebrick"; + String stoneBrickType; + + switch (name) { + case "minecraft:mossy_stone_bricks" -> stoneBrickType = "mossy"; + case "minecraft:cracked_stone_bricks" -> stoneBrickType = "cracked"; + case "minecraft:chiseled_stone_bricks" -> stoneBrickType = "chiseled"; + case "minecraft:smooth_stone_bricks" -> stoneBrickType = "smooth"; + case "minecraft:stone_bricks" -> stoneBrickType = "default"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("stone_brick_type", stoneBrickType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_LIGHT_BLOCKS.contains(name)) { + replacement = "minecraft:light_block"; + int blockLightLevel = Integer.parseInt(name.split("_")[2]); + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putInt("block_light_level", blockLightLevel) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_SANDSTONE_BLOCKS.contains(name) || NEW_RED_SANDSTONE_BLOCKS.contains(name)) { + replacement = NEW_SANDSTONE_BLOCKS.contains(name) ? "minecraft:sandstone" : "minecraft:red_sandstone"; + String sandStoneType; + + switch (name) { + case "minecraft:cut_sandstone", "minecraft:cut_red_sandstone" -> sandStoneType = "cut"; + case "minecraft:chiseled_sandstone", "minecraft:chiseled_red_sandstone" -> sandStoneType = "heiroglyphs"; + case "minecraft:smooth_sandstone", "minecraft:smooth_red_sandstone" -> sandStoneType = "smooth"; + case "minecraft:sandstone", "minecraft:red_sandstone" -> sandStoneType = "default"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("sand_stone_type", sandStoneType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_QUARTZ_BLOCKS.contains(name)) { + replacement = "minecraft:quartz_block"; + String chiselType; + + switch (name) { + case "minecraft:chiseled_quartz_block" -> chiselType = "chiseled"; + case "minecraft:quartz_pillar" -> chiselType = "lines"; + case "minecraft:smooth_quartz" -> chiselType = "smooth"; + case "minecraft:quartz_block" -> chiselType = "default"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("chisel_type", chiselType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_SAND_BLOCKS.contains(name)) { + replacement = "minecraft:sand"; + String sandType = name.equals("minecraft:red_sand") ? "red" : "normal"; + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("sand_type", sandType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_DIRT_BLOCKS.contains(name)) { + replacement = "minecraft:dirt"; + String dirtType = name.equals("minecraft:coarse_dirt") ? "coarse" : "normal"; + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("dirt_type", dirtType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_ANVILS.contains(name)) { + replacement = "minecraft:anvil"; + String damage; + + switch (name) { + case "minecraft:damaged_anvil" -> damage = "broken"; + case "minecraft:chipped_anvil" -> damage = "slightly_damaged"; + case "minecraft:deprecated_anvil" -> damage = "very_damaged"; + case "minecraft:anvil" -> damage = "undamaged"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("damage", damage) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_YELLOW_FLOWERS.contains(name)) { + replacement = "minecraft:yellow_flower"; + return tag.toBuilder().putString("name", replacement).build(); + } + + return tag; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 2c97fe13c..f11b58bfe 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -41,6 +41,7 @@ import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.nbt.NbtUtils; import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671; import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685; +import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition; @@ -90,7 +91,8 @@ public class ItemRegistryPopulator { public static void populate() { List paletteVersions = new ArrayList<>(3); paletteVersions.add(new PaletteVersion("1_20_80", Bedrock_v671.CODEC.getProtocolVersion(), Collections.emptyMap(), Conversion685_671::remapItem)); - paletteVersions.add(new PaletteVersion("1_21_0", Bedrock_v685.CODEC.getProtocolVersion())); + paletteVersions.add(new PaletteVersion("1_21_0", Bedrock_v685.CODEC.getProtocolVersion(), Collections.emptyMap(), Conversion712_685::remapItem)); + paletteVersions.add(new PaletteVersion("1_21_20", Bedrock_v712.CODEC.getProtocolVersion())); GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap(); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 4c426b410..ce1022936 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -894,11 +894,11 @@ public abstract class InventoryTranslator { List containerEntries = new ArrayList<>(); for (Map.Entry> entry : containerMap.entrySet()) { - containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue())); + containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), null)); } ItemStackResponseSlot cursorEntry = makeItemEntry(0, session.getPlayerInventory().getCursor()); - containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry))); + containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), null)); return containerEntries; } diff --git a/core/src/main/resources/bedrock/biome_definitions.dat b/core/src/main/resources/bedrock/biome_definitions.dat index dfee570e48866c87a60fce4f93041093f7696468..3ae94a5c85c6f13d4de2feb8a5d24d2ab2f11efe 100644 GIT binary patch delta 283 zcmX?elC?gS)l wVXngD*bKkT)0^f?g3Op~DLMIErtsu-bCo7@&ljA`Yb!XJr%`9~mpLVD0G@a}7ytkO diff --git a/core/src/main/resources/bedrock/block_palette.1_21_20.nbt b/core/src/main/resources/bedrock/block_palette.1_21_20.nbt new file mode 100644 index 0000000000000000000000000000000000000000..521ea3cc6fdded8adda60e2a1ad1ff35ab826dc5 GIT binary patch literal 178977 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�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 q0AJ1OG*VR}TG|eCk Date: Mon, 29 Jul 2024 00:17:56 -0700 Subject: [PATCH 48/84] Indicate 1.21.20 support Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07f3df5aa..ecf991cdb 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,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 joined us here! -### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.3 and Minecraft Java 1.21 +### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.20 and Minecraft Java 1.21 ## Setting Up Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. From efe2736635b523564fa961ebbf3ae17011c3a8a4 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 30 Jul 2024 10:26:02 +0200 Subject: [PATCH 49/84] Fix: Piston listener on Fabric/NeoForge (#4899) * Fix: Sticky pistons not retracting on Geyser-Spigot/turning visually into normal pistons on all other platforms * Initial attempt: Mod piston listener * fix piston retracting --- bootstrap/mod/build.gradle.kts | 3 +- .../mixin/server/PistonBaseBlockMixin.java | 127 ++++++++++++++++++ .../mod/src/main/resources/geyser.mixins.json | 1 + .../populator/BlockRegistryPopulator.java | 2 +- .../level/block/entity/PistonBlockEntity.java | 5 +- .../java/level/JavaBlockEventTranslator.java | 20 ++- gradle/libs.versions.toml | 2 + 7 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java diff --git a/bootstrap/mod/build.gradle.kts b/bootstrap/mod/build.gradle.kts index 32224d00b..57f11b2c7 100644 --- a/bootstrap/mod/build.gradle.kts +++ b/bootstrap/mod/build.gradle.kts @@ -16,7 +16,8 @@ afterEvaluate { dependencies { api(projects.core) compileOnly(libs.mixin) + compileOnly(libs.mixinextras) // Only here to suppress "unknown enum constant EnvType.CLIENT" warnings. DO NOT USE! compileOnly(libs.fabric.loader) -} \ No newline at end of file +} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java new file mode 100644 index 000000000..6ac51ba52 --- /dev/null +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/PistonBaseBlockMixin.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024 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.geyser.platform.mod.mixin.server; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.cloudburstmc.math.vector.Vector3i; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.PistonCache; +import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity; +import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValueType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Mixin(PistonBaseBlock.class) +public class PistonBaseBlockMixin { + + @Shadow + @Final + private boolean isSticky; + + @ModifyExpressionValue(method = "moveBlocks", + at = @At(value = "INVOKE", target = "Lcom/google/common/collect/Maps;newHashMap()Ljava/util/HashMap;") + ) + private HashMap geyser$onMapCreate(HashMap original, @Share("pushBlocks") LocalRef> localRef) { + localRef.set(original); + return original; + } + + @Inject(method = "moveBlocks", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/piston/PistonStructureResolver;getToDestroy()Ljava/util/List;") + ) + private void geyser$onBlocksMove(Level level, BlockPos blockPos, Direction direction, boolean isExtending, CallbackInfoReturnable cir, @Share("pushBlocks") LocalRef> localRef) { + PistonValueType type = isExtending ? PistonValueType.PUSHING : PistonValueType.PULLING; + boolean sticky = this.isSticky; + + Object2ObjectMap attachedBlocks = new Object2ObjectArrayMap<>(); + boolean blocksFilled = false; + + for (Map.Entry entry : GeyserImpl.getInstance().getSessionManager().getSessions().entrySet()) { + Player player = level.getPlayerByUUID(entry.getKey()); + //noinspection resource + if (player == null || !player.level().equals(level)) { + continue; + } + GeyserSession session = entry.getValue(); + + int dX = Math.abs(blockPos.getX() - player.getBlockX()) >> 4; + int dZ = Math.abs(blockPos.getZ() - player.getBlockZ()) >> 4; + if ((dX * dX + dZ * dZ) > session.getServerRenderDistance() * session.getServerRenderDistance()) { + // Ignore pistons outside the player's render distance + continue; + } + + // Trying to grab the blocks from the world like other platforms would result in the moving piston block + // being returned instead. + if (!blocksFilled) { + Map blocks = localRef.get(); + for (Map.Entry blockStateEntry : blocks.entrySet()) { + int blockStateId = Block.BLOCK_STATE_REGISTRY.getId(blockStateEntry.getValue()); + org.geysermc.geyser.level.block.type.BlockState state = org.geysermc.geyser.level.block.type.BlockState.of(blockStateId); + attachedBlocks.put(geyser$fromBlockPos(blockStateEntry.getKey()), state); + } + blocksFilled = true; + } + + org.geysermc.geyser.level.physics.Direction orientation = org.geysermc.geyser.level.physics.Direction.VALUES[direction.ordinal()]; + + Vector3i position = geyser$fromBlockPos(blockPos); + session.executeInEventLoop(() -> { + PistonCache pistonCache = session.getPistonCache(); + PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> + new PistonBlockEntity(session, position, orientation, sticky, !isExtending)); + blockEntity.setAction(type, attachedBlocks); + }); + } + } + + @Unique + private static Vector3i geyser$fromBlockPos(BlockPos pos) { + return Vector3i.from(pos.getX(), pos.getY(), pos.getZ()); + } + +} diff --git a/bootstrap/mod/src/main/resources/geyser.mixins.json b/bootstrap/mod/src/main/resources/geyser.mixins.json index 2576e1ce6..e820e654d 100644 --- a/bootstrap/mod/src/main/resources/geyser.mixins.json +++ b/bootstrap/mod/src/main/resources/geyser.mixins.json @@ -5,6 +5,7 @@ "compatibilityLevel": "JAVA_17", "mixins": [ "server.BlockPlaceMixin", + "server.PistonBaseBlockMixin", "server.ServerConnectionListenerMixin" ], "server": [ diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java index d7dc989da..f539e52ec 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java @@ -129,7 +129,7 @@ public final class BlockRegistryPopulator { NbtMapBuilder builder = vanillaBlockStates.get(i).toBuilder(); builder.remove("version"); // Remove all nbt tags which are not needed for differentiating states builder.remove("name_hash"); // Quick workaround - was added in 1.19.20 - builder.remove("network_id"); // Added in 1.19.80 - ???? + builder.remove("network_id"); // Added in 1.19.80 builder.remove("block_id"); // Added in 1.20.60 //noinspection UnstableApiUsage builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states"))); diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java index 350ce8c3e..d1dd24855 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java @@ -37,7 +37,6 @@ import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.property.Properties; @@ -230,8 +229,8 @@ public class PistonBlockEntity { BlockState state = session.getGeyser().getWorldManager().blockAt(session, blockInFront); if (state.is(Blocks.PISTON_HEAD)) { ChunkUtils.updateBlock(session, Block.JAVA_AIR_ID, blockInFront); - } else if ((session.getGeyser().getPlatformType() == PlatformType.SPIGOT || session.getErosionHandler().isActive()) && state.is(Blocks.AIR)) { - // Spigot removes the piston head from the cache, but we need to send the block update ourselves + } else if ((session.getGeyser().getWorldManager().hasOwnChunkCache() || session.getErosionHandler().isActive()) && state.is(Blocks.AIR)) { + // The platform removes the piston head from the cache, but we need to send the block update ourselves ChunkUtils.updateBlock(session, Block.JAVA_AIR_ID, blockInFront); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java index ff861530a..c94468c17 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaBlockEventTranslator.java @@ -82,16 +82,22 @@ public class JavaBlockEventTranslator extends PacketTranslator new PistonBlockEntity(session, pos, direction, true, true)); + PistonBlockEntity blockEntity = pistonCache.getPistons().computeIfAbsent(position, pos -> new PistonBlockEntity(session, pos, direction, isSticky, true)); if (blockEntity.getAction() != action) { blockEntity.setAction(action, Object2ObjectMaps.emptyMap()); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 58b5310ac..e50756ef1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ fabric-api = "0.100.1+1.21" fabric-permissions = "0.2-SNAPSHOT" neoforge-minecraft = "21.0.0-beta" mixin = "0.8.5" +mixinextras = "0.3.5" minecraft = "1.21" # plugin versions @@ -89,6 +90,7 @@ folia-api = { group = "dev.folia", name = "folia-api", version.ref = "folia" } paper-mojangapi = { group = "io.papermc.paper", name = "paper-mojangapi", version.ref = "folia" } mixin = { group = "org.spongepowered", name = "mixin", version.ref = "mixin" } +mixinextras = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinextras" } minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } From ca0f3775a22d39617f8d9a32b983c0d187ead8ea Mon Sep 17 00:00:00 2001 From: rtm516 Date: Tue, 30 Jul 2024 13:49:42 +0100 Subject: [PATCH 50/84] Update links in README (#4917) * Update links in README * Update README.md * Update README.md --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 07f3df5aa..8eac49a24 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,15 @@ 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 joined us here! -### Currently supporting Minecraft Bedrock 1.20.80 - 1.21.3 and Minecraft Java 1.21 +## Supported Versions +Geyser is currently supporting Minecraft Bedrock 1.20.80 - 1.21.3 and Minecraft Java Server 1.21. For more info please see [here](https://geysermc.org/wiki/geyser/supported-versions/). ## Setting Up -Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser. - -[![YouTube Video](https://img.youtube.com/vi/U7dZZ8w7Gi4/0.jpg)](https://www.youtube.com/watch?v=U7dZZ8w7Gi4) +Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser. ## Links: - Website: https://geysermc.org -- Docs: https://wiki.geysermc.org/geyser/ +- Docs: https://geysermc.org/wiki/geyser/ - Download: https://geysermc.org/download - Discord: https://discord.gg/geysermc - Donate: https://opencollective.com/geysermc @@ -34,7 +33,7 @@ Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Ge - Some Entity Flags ## What can't be fixed -There are a few things Geyser is unable to support due to various differences between Minecraft Bedrock and Java. For a list of these limitations, see the [Current Limitations](https://wiki.geysermc.org/geyser/current-limitations/) page. +There are a few things Geyser is unable to support due to various differences between Minecraft Bedrock and Java. For a list of these limitations, see the [Current Limitations](https://geysermc.org/wiki/geyser/current-limitations/) page. ## Compiling 1. Clone the repo to your computer @@ -47,7 +46,7 @@ you're interested in helping out with Geyser. ## Libraries Used: - [Adventure Text Library](https://github.com/KyoriPowered/adventure) -- [NukkitX Bedrock Protocol Library](https://github.com/NukkitX/Protocol) -- [Steveice10's Java Protocol Library](https://github.com/Steveice10/MCProtocolLib) +- [CloudburstMC Bedrock Protocol Library](https://github.com/CloudburstMC/Protocol) +- [GeyserMC's Java Protocol Library](https://github.com/GeyserMC/MCProtocolLib) - [TerminalConsoleAppender](https://github.com/Minecrell/TerminalConsoleAppender) - [Simple Logging Facade for Java (slf4j)](https://github.com/qos-ch/slf4j) From 13dfc7c173550c49ff6070176f8d0c4f3d270c8a Mon Sep 17 00:00:00 2001 From: rtm516 Date: Wed, 31 Jul 2024 01:06:26 +0100 Subject: [PATCH 51/84] Allow commands with xbox achievements enabled (#4894) * Allow commands with xbox achievements enabled * Don't enable by default * Add null check to paramData * Update comment --- .../translator/protocol/java/JavaCommandsTranslator.java | 6 +++--- core/src/main/resources/config.yml | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java index ecfb2d220..c0e3f5716 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java @@ -169,8 +169,8 @@ public class JavaCommandsTranslator extends PacketTranslator flags = Set.of(); + // The command flags, set to NOT_CHEAT so known commands can be used while achievements are enabled. + Set flags = Set.of(CommandData.Flag.NOT_CHEAT); // Loop through all the found commands for (Map.Entry> entry : commands.entrySet()) { @@ -449,7 +449,7 @@ public class JavaCommandsTranslator extends PacketTranslator Date: Wed, 31 Jul 2024 19:21:29 +0200 Subject: [PATCH 52/84] Fix: Geyser-NeoForge not booting due to duplicate module (#4922) --- bootstrap/mod/fabric/build.gradle.kts | 5 +---- bootstrap/mod/neoforge/build.gradle.kts | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index 0d083fcf7..9215c575e 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -25,10 +25,7 @@ dependencies { shadow(libs.protocol.connection) { isTransitive = false } shadow(libs.protocol.common) { isTransitive = false } shadow(libs.protocol.codec) { isTransitive = false } - shadow(libs.minecraftauth) { isTransitive = false } shadow(libs.raknet) { isTransitive = false } - - // Consequences of shading + relocating mcauthlib: shadow/relocate mcpl! shadow(libs.mcprotocollib) { isTransitive = false } // Since we also relocate cloudburst protocol: shade erosion common @@ -67,4 +64,4 @@ modrinth { dependencies { required.project("fabric-api") } -} \ No newline at end of file +} diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts index e0e7c2dfa..741e2fd11 100644 --- a/bootstrap/mod/neoforge/build.gradle.kts +++ b/bootstrap/mod/neoforge/build.gradle.kts @@ -5,6 +5,7 @@ plugins { // This is provided by "org.cloudburstmc.math.mutable" too, so yeet. // NeoForge's class loader is *really* annoying. provided("org.cloudburstmc.math", "api") +provided("com.google.errorprone", "error_prone_annotations") architectury { platformSetupLoomIde() @@ -56,4 +57,4 @@ tasks { modrinth { loaders.add("neoforge") uploadFile.set(tasks.getByPath("remapModrinthJar")) -} \ No newline at end of file +} From 6002c9c7a167df137fb802bbbe7a38bc84de7fdb Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 31 Jul 2024 21:22:22 +0200 Subject: [PATCH 53/84] Only add a tag to the bedrock item if it is needed (#4925) --- .../java/JavaUpdateRecipesTranslator.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index 7c36c505b..689e0448a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -49,6 +49,8 @@ import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData; import org.geysermc.geyser.inventory.recipe.TrimRecipe; +import org.geysermc.geyser.item.type.BedrockRequiresTagItem; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -443,13 +445,18 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator translateShulkerBoxRecipe(GeyserShapelessRecipe recipe) { - ItemData output = ItemTranslator.translateToBedrock(session, recipe.result()); + ItemStack result = recipe.result(); + ItemData output = ItemTranslator.translateToBedrock(session, result); if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists return null; } - // Strip NBT - tools won't appear in the recipe book otherwise - // output = output.toBuilder().tag(null).build(); // TODO confirm??? + + Item javaItem = Registries.JAVA_ITEMS.get(result.getId()); + if (!(javaItem instanceof BedrockRequiresTagItem)) { + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + } ItemDescriptorWithCount[][] inputCombinations = combinations(session, recipe.ingredients()); if (inputCombinations == null) { return null; @@ -467,13 +474,18 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator translateShapelessRecipe(GeyserShapelessRecipe recipe) { - ItemData output = ItemTranslator.translateToBedrock(session, recipe.result()); + ItemStack result = recipe.result(); + ItemData output = ItemTranslator.translateToBedrock(session, result); if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists return null; } - // Strip NBT - tools won't appear in the recipe book otherwise - //output = output.toBuilder().tag(null).build(); // TODO confirm this is still true??? + + Item javaItem = Registries.JAVA_ITEMS.get(result.getId()); + if (!(javaItem instanceof BedrockRequiresTagItem)) { + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + } ItemDescriptorWithCount[][] inputCombinations = combinations(session, recipe.ingredients()); if (inputCombinations == null) { return null; @@ -491,13 +503,18 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator translateShapedRecipe(GeyserShapedRecipe recipe) { - ItemData output = ItemTranslator.translateToBedrock(session, recipe.result()); + ItemStack result = recipe.result(); + ItemData output = ItemTranslator.translateToBedrock(session, result); if (!output.isValid()) { // Likely modded item that Bedrock will complain about if it persists return null; } - // See above - //output = output.toBuilder().tag(null).build(); + + Item javaItem = Registries.JAVA_ITEMS.get(result.getId()); + if (!(javaItem instanceof BedrockRequiresTagItem)) { + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + } ItemDescriptorWithCount[][] inputCombinations = combinations(session, recipe.ingredients()); if (inputCombinations == null) { return null; From 87ab51cb28f059dc815be0c9804346d4d88535d8 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Thu, 11 Jul 2024 23:56:42 -0500 Subject: [PATCH 54/84] Cloud for commands (#3808) Co-authored-by: onebeastchris --- .../geysermc/geyser/api/command/Command.java | 133 ++++--- .../geyser/api/command/CommandSource.java | 15 + .../lifecycle/GeyserDefineCommandsEvent.java | 2 +- ...GeyserRegisterPermissionCheckersEvent.java | 42 +++ .../GeyserRegisterPermissionsEvent.java | 51 +++ .../geyser/api/extension/Extension.java | 9 + .../api/permission/PermissionChecker.java | 49 +++ bootstrap/bungeecord/build.gradle.kts | 8 +- .../bungeecord/GeyserBungeePlugin.java | 56 +-- .../GeyserBungeeUpdateListener.java | 4 +- .../command/BungeeCommandSource.java | 25 +- .../command/GeyserBungeeCommandExecutor.java | 89 ----- bootstrap/mod/fabric/build.gradle.kts | 13 +- .../fabric/GeyserFabricBootstrap.java | 33 +- bootstrap/mod/neoforge/build.gradle.kts | 11 +- .../neoforge/GeyserNeoForgeBootstrap.java | 50 ++- .../GeyserNeoForgeCommandRegistry.java | 101 ++++++ .../GeyserNeoForgePermissionHandler.java | 149 -------- .../platform/neoforge/PermissionUtils.java | 79 +++++ .../neoforge/mixin/PermissionNodeMixin.java | 48 +++ .../resources/META-INF/neoforge.mods.toml | 2 + .../resources/geyser_neoforge.mixins.json | 12 + .../platform/mod/GeyserModBootstrap.java | 75 +--- .../platform/mod/GeyserModUpdateListener.java | 13 +- .../mod/command/GeyserModCommandExecutor.java | 75 ---- ...mmandSender.java => ModCommandSource.java} | 26 +- .../mod/world/GeyserModWorldManager.java | 7 - bootstrap/spigot/build.gradle.kts | 8 +- .../platform/spigot/GeyserSpigotPlugin.java | 172 ++++----- .../spigot/GeyserSpigotUpdateListener.java | 4 +- .../command/GeyserBrigadierSupport.java | 61 ---- .../command/GeyserPaperCommandListener.java | 87 ----- .../command/GeyserSpigotCommandExecutor.java | 88 ----- ...anager.java => SpigotCommandRegistry.java} | 45 ++- .../spigot/command/SpigotCommandSource.java | 26 +- .../manager/GeyserSpigotWorldManager.java | 9 - .../spigot/src/main/resources/plugin.yml | 8 - bootstrap/standalone/build.gradle.kts | 4 + .../standalone/GeyserStandaloneBootstrap.java | 29 +- .../standalone/GeyserStandaloneLogger.java | 4 +- .../standalone/gui/GeyserStandaloneGUI.java | 20 +- bootstrap/velocity/build.gradle.kts | 9 +- .../velocity/GeyserVelocityPlugin.java | 62 ++-- .../GeyserVelocityUpdateListener.java | 4 +- .../GeyserVelocityCommandExecutor.java | 83 ----- .../command/VelocityCommandSource.java | 18 +- bootstrap/viaproxy/build.gradle.kts | 6 +- .../viaproxy/GeyserViaProxyPlugin.java | 35 +- .../geyser.modded-conventions.gradle.kts | 6 +- .../geyser.platform-conventions.gradle.kts | 1 - core/build.gradle.kts | 3 + .../java/org/geysermc/geyser/Constants.java | 2 - .../org/geysermc/geyser/GeyserBootstrap.java | 8 +- .../java/org/geysermc/geyser/GeyserImpl.java | 19 +- .../org/geysermc/geyser/GeyserLogger.java | 6 + .../java/org/geysermc/geyser/Permissions.java | 63 ++++ .../geyser/command/CommandRegistry.java | 300 ++++++++++++++++ .../command/CommandSourceConverter.java | 113 ++++++ .../geyser/command/ExceptionHandlers.java | 129 +++++++ .../geyser/command/GeyserCommand.java | 204 ++++++++--- .../geyser/command/GeyserCommandExecutor.java | 98 ------ .../geyser/command/GeyserCommandManager.java | 330 ------------------ .../geyser/command/GeyserCommandSource.java | 30 ++ .../geyser/command/GeyserPermission.java | 136 ++++++++ .../defaults/AdvancedTooltipsCommand.java | 33 +- .../command/defaults/AdvancementsCommand.java | 24 +- .../defaults/ConnectionTestCommand.java | 117 +++---- .../geyser/command/defaults/DumpCommand.java | 84 +++-- .../command/defaults/ExtensionsCommand.java | 17 +- .../geyser/command/defaults/HelpCommand.java | 76 ++-- .../geyser/command/defaults/ListCommand.java | 20 +- .../command/defaults/OffhandCommand.java | 26 +- .../command/defaults/ReloadCommand.java | 22 +- .../command/defaults/SettingsCommand.java | 27 +- .../command/defaults/StatisticsCommand.java | 27 +- .../geyser/command/defaults/StopCommand.java | 22 +- .../command/defaults/VersionCommand.java | 34 +- .../standalone/PermissionConfiguration.java | 42 +++ .../StandaloneCloudCommandManager.java | 126 +++++++ .../type/GeyserDefineCommandsEventImpl.java | 6 +- .../command/GeyserExtensionCommand.java | 195 ++++++++++- .../geyser/level/GeyserWorldManager.java | 5 - .../geysermc/geyser/level/WorldManager.java | 9 - .../loader/ProviderRegistryLoader.java | 4 +- .../geyser/session/GeyserSession.java | 28 +- .../BedrockCommandRequestTranslator.java | 26 +- .../BedrockSetDefaultGameTypeTranslator.java | 3 +- .../BedrockSetDifficultyTranslator.java | 3 +- .../BedrockSetPlayerGameTypeTranslator.java | 3 +- .../protocol/java/JavaCommandsTranslator.java | 10 +- .../org/geysermc/geyser/util/FileUtils.java | 12 + .../geysermc/geyser/util/SettingsUtils.java | 3 +- core/src/main/resources/languages | 2 +- core/src/main/resources/permissions.yml | 9 + gradle/libs.versions.toml | 13 +- 95 files changed, 2556 insertions(+), 1879 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java delete mode 100644 bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java create mode 100644 bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java delete mode 100644 bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java create mode 100644 bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java create mode 100644 bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java create mode 100644 bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json delete mode 100644 bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java rename bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/{ModCommandSender.java => ModCommandSource.java} (77%) delete mode 100644 bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java delete mode 100644 bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java delete mode 100644 bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java rename bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/{GeyserSpigotCommandManager.java => SpigotCommandRegistry.java} (61%) delete mode 100644 bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java create mode 100644 core/src/main/java/org/geysermc/geyser/Permissions.java create mode 100644 core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java create mode 100644 core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java create mode 100644 core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java delete mode 100644 core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java delete mode 100644 core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java create mode 100644 core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java create mode 100644 core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java create mode 100644 core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java create mode 100644 core/src/main/resources/permissions.yml diff --git a/api/src/main/java/org/geysermc/geyser/api/command/Command.java b/api/src/main/java/org/geysermc/geyser/api/command/Command.java index 2f1f2b24d..29922ae1e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/command/Command.java +++ b/api/src/main/java/org/geysermc/geyser/api/command/Command.java @@ -28,7 +28,9 @@ package org.geysermc.geyser.api.command; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.TriState; import java.util.Collections; import java.util.List; @@ -58,15 +60,15 @@ public interface Command { * Gets the permission node associated with * this command. * - * @return the permission node for this command + * @return the permission node for this command if defined, otherwise an empty string */ @NonNull String permission(); /** - * Gets the aliases for this command. + * Gets the aliases for this command, as an unmodifiable list * - * @return the aliases for this command + * @return the aliases for this command as an unmodifiable list */ @NonNull List aliases(); @@ -75,35 +77,39 @@ public interface Command { * Gets if this command is designed to be used only by server operators. * * @return if this command is designated to be used only by server operators. + * @deprecated this method is not guaranteed to provide meaningful or expected results. */ - boolean isSuggestedOpOnly(); - - /** - * Gets if this command is executable on console. - * - * @return if this command is executable on console - */ - boolean isExecutableOnConsole(); - - /** - * Gets the subcommands associated with this - * command. Mainly used within the Geyser Standalone - * GUI to know what subcommands are supported. - * - * @return the subcommands associated with this command - */ - @NonNull - default List subCommands() { - return Collections.emptyList(); + @Deprecated(forRemoval = true) + default boolean isSuggestedOpOnly() { + return false; } /** - * Used to send a deny message to Java players if this command can only be used by Bedrock players. - * - * @return true if this command can only be used by Bedrock players. + * @return true if this command is executable on console + * @deprecated use {@link #isPlayerOnly()} instead (inverted) */ - default boolean isBedrockOnly() { - return false; + @Deprecated(forRemoval = true) + default boolean isExecutableOnConsole() { + return !isPlayerOnly(); + } + + /** + * @return true if this command can only be used by players + */ + boolean isPlayerOnly(); + + /** + * @return true if this command can only be used by Bedrock players + */ + boolean isBedrockOnly(); + + /** + * @deprecated this method will always return an empty immutable list + */ + @Deprecated(forRemoval = true) + @NonNull + default List subCommands() { + return Collections.emptyList(); } /** @@ -128,7 +134,7 @@ public interface Command { * is an instance of this source. * * @param sourceType the source type - * @return the builder + * @return this builder */ Builder source(@NonNull Class sourceType); @@ -136,7 +142,7 @@ public interface Command { * Sets the command name. * * @param name the command name - * @return the builder + * @return this builder */ Builder name(@NonNull String name); @@ -144,23 +150,40 @@ public interface Command { * Sets the command description. * * @param description the command description - * @return the builder + * @return this builder */ Builder description(@NonNull String description); /** - * Sets the permission node. + * Sets the permission node required to run this command.
    + * It will not be registered with any permission registries, such as an underlying server, + * or a permissions Extension (unlike {@link #permission(String, TriState)}). * * @param permission the permission node - * @return the builder + * @return this builder */ Builder permission(@NonNull String permission); + /** + * Sets the permission node and its default value. The usage of the default value is platform dependant + * and may or may not be used. For example, it may be registered to an underlying server. + *

    + * Extensions may instead listen for {@link GeyserRegisterPermissionsEvent} to register permissions, + * especially if the same permission is required by multiple commands. Also see this event for TriState meanings. + * + * @param permission the permission node + * @param defaultValue the node's default value + * @return this builder + * @deprecated this method is experimental and may be removed in the future + */ + @Deprecated + Builder permission(@NonNull String permission, @NonNull TriState defaultValue); + /** * Sets the aliases. * * @param aliases the aliases - * @return the builder + * @return this builder */ Builder aliases(@NonNull List aliases); @@ -168,46 +191,62 @@ public interface Command { * Sets if this command is designed to be used only by server operators. * * @param suggestedOpOnly if this command is designed to be used only by server operators - * @return the builder + * @return this builder + * @deprecated this method is not guaranteed to produce meaningful or expected results */ + @Deprecated(forRemoval = true) Builder suggestedOpOnly(boolean suggestedOpOnly); /** * Sets if this command is executable on console. * * @param executableOnConsole if this command is executable on console - * @return the builder + * @return this builder + * @deprecated use {@link #isPlayerOnly()} instead (inverted) */ + @Deprecated(forRemoval = true) Builder executableOnConsole(boolean executableOnConsole); + /** + * Sets if this command can only be executed by players. + * + * @param playerOnly if this command is player only + * @return this builder + */ + Builder playerOnly(boolean playerOnly); + + /** + * Sets if this command can only be executed by bedrock players. + * + * @param bedrockOnly if this command is bedrock only + * @return this builder + */ + Builder bedrockOnly(boolean bedrockOnly); + /** * Sets the subcommands. * * @param subCommands the subcommands - * @return the builder + * @return this builder + * @deprecated this method has no effect */ - Builder subCommands(@NonNull List subCommands); - - /** - * Sets if this command is bedrock only. - * - * @param bedrockOnly if this command is bedrock only - * @return the builder - */ - Builder bedrockOnly(boolean bedrockOnly); + @Deprecated(forRemoval = true) + default Builder subCommands(@NonNull List subCommands) { + return this; + } /** * Sets the {@link CommandExecutor} for this command. * * @param executor the command executor - * @return the builder + * @return this builder */ Builder executor(@NonNull CommandExecutor executor); /** * Builds the command. * - * @return the command + * @return a new command from this builder */ @NonNull Command build(); diff --git a/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java b/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java index 45276e2c4..c1453f579 100644 --- a/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java +++ b/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java @@ -26,6 +26,10 @@ package org.geysermc.geyser.api.command; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.connection.GeyserConnection; + +import java.util.UUID; /** * Represents an instance capable of sending commands. @@ -64,6 +68,17 @@ public interface CommandSource { */ boolean isConsole(); + /** + * @return a Java UUID if this source represents a player, otherwise null + */ + @Nullable UUID playerUuid(); + + /** + * @return a GeyserConnection if this source represents a Bedrock player that is connected + * to this Geyser instance, otherwise null + */ + @Nullable GeyserConnection connection(); + /** * Returns the locale of the command source. * diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java index 994373752..d136202bd 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java @@ -50,7 +50,7 @@ public interface GeyserDefineCommandsEvent extends Event { /** * Gets all the registered built-in {@link Command}s. * - * @return all the registered built-in commands + * @return all the registered built-in commands as an unmodifiable map */ @NonNull Map commands(); diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java new file mode 100644 index 000000000..43ebc2c50 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2023 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.geyser.api.event.lifecycle; + +import org.geysermc.event.Event; +import org.geysermc.event.PostOrder; +import org.geysermc.geyser.api.permission.PermissionChecker; + +/** + * Fired by any permission manager implementations that wish to add support for custom permission checking. + * This event is not guaranteed to be fired - it is currently only fired on Geyser-Standalone and ViaProxy. + *

    + * Subscribing to this event with an earlier {@link PostOrder} and registering a {@link PermissionChecker} + * will result in that checker having a higher priority than others. + */ +public interface GeyserRegisterPermissionCheckersEvent extends Event { + + void register(PermissionChecker checker); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java new file mode 100644 index 000000000..4f06c4e5f --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2023 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.geyser.api.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.util.TriState; + +/** + * Fired by anything that wishes to gather permission nodes and defaults. + *

    + * This event is not guaranteed to be fired, as certain Geyser platforms do not have a native permission system. + * It can be expected to fire on Geyser-Spigot, Geyser-NeoForge, Geyser-Standalone, and Geyser-ViaProxy + * It may be fired by a 3rd party regardless of the platform. + */ +public interface GeyserRegisterPermissionsEvent extends Event { + + /** + * Registers a permission node and its default value with the firer.

    + * {@link TriState#TRUE} corresponds to all players having the permission by default.
    + * {@link TriState#NOT_SET} corresponds to only server operators having the permission by default (if such a concept exists on the platform).
    + * {@link TriState#FALSE} corresponds to no players having the permission by default.
    + * + * @param permission the permission node to register + * @param defaultValue the default value of the node + */ + void register(@NonNull String permission, @NonNull TriState defaultValue); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java b/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java index 993bdee44..1eacfea9a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java +++ b/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java @@ -107,6 +107,15 @@ public interface Extension extends EventRegistrar { return this.extensionLoader().description(this); } + /** + * @return the root command that all of this extension's commands will stem from. + * By default, this is the extension's id. + */ + @NonNull + default String rootCommand() { + return this.description().id(); + } + /** * Gets the extension's logger * diff --git a/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java new file mode 100644 index 000000000..c0d4af2f4 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2023 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.geyser.api.permission; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.command.CommandSource; +import org.geysermc.geyser.api.util.TriState; + +/** + * Something capable of checking if a {@link CommandSource} has a permission + */ +@FunctionalInterface +public interface PermissionChecker { + + /** + * Checks if the given source has a permission + * + * @param source the {@link CommandSource} whose permissions should be queried + * @param permission the permission node to check + * @return a {@link TriState} as the value of the node. {@link TriState#NOT_SET} generally means that the permission + * node itself was not found, and the source does not have such permission. + * {@link TriState#TRUE} and {@link TriState#FALSE} represent explicitly set values. + */ + @NonNull + TriState hasPermission(@NonNull CommandSource source, @NonNull String permission); +} diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 910e50723..5fe7ea3d1 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -1,5 +1,7 @@ dependencies { api(projects.core) + + implementation(libs.cloud.bungee) implementation(libs.adventure.text.serializer.bungeecord) compileOnlyApi(libs.bungeecord.proxy) } @@ -8,13 +10,15 @@ platformRelocate("net.md_5.bungee.jni") platformRelocate("com.fasterxml.jackson") platformRelocate("io.netty.channel.kqueue") // This is not used because relocating breaks natives, but we must include it or else we get ClassDefNotFound platformRelocate("net.kyori") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated platformRelocate("org.yaml") // Broken as of 1.20 // These dependencies are already present on the platform provided(libs.bungeecord.proxy) -application { - mainClass.set("org.geysermc.geyser.platform.bungeecord.GeyserBungeeMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.bungeecord.GeyserBungeeMain" } tasks.withType { diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java index cd6b59f64..1c0049231 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.platform.bungeecord; import io.netty.channel.Channel; import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.protocol.ProtocolConstants; @@ -34,17 +35,20 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.bungeecord.command.GeyserBungeeCommandExecutor; +import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.bungee.BungeeCommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; import java.io.File; import java.io.IOException; @@ -54,21 +58,22 @@ import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; -import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { - private GeyserCommandManager geyserCommandManager; + private CommandRegistry commandRegistry; private GeyserBungeeConfiguration geyserConfig; private GeyserBungeeInjector geyserInjector; private final GeyserBungeeLogger geyserLogger = new GeyserBungeeLogger(getLogger()); private IGeyserPingPassthrough geyserBungeePingPassthrough; - private GeyserImpl geyser; + // We can't disable the plugin; hence we need to keep track of it manually + private boolean disabled; + @Override public void onLoad() { onGeyserInitialize(); @@ -93,16 +98,23 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } if (!this.loadConfig()) { + disabled = true; return; } this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); this.geyser = GeyserImpl.load(PlatformType.BUNGEECORD, this); this.geyserInjector = new GeyserBungeeInjector(this); + + // Registration of listeners occurs only once + this.getProxy().getPluginManager().registerListener(this, new GeyserBungeeUpdateListener()); } @Override public void onEnable() { + if (disabled) { + return; // Config did not load properly! + } // Big hack - Bungee does not provide us an event to listen to, so schedule a repeating // task that waits for a field to be filled which is set after the plugin enable // process is complete @@ -143,10 +155,18 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } else { - // For consistency with other platforms - create command manager before GeyserImpl#start() - // This ensures the command events are called before the item/block ones are - this.geyserCommandManager = new GeyserCommandManager(geyser); - this.geyserCommandManager.init(); + var sourceConverter = new CommandSourceConverter<>( + CommandSender.class, + id -> getProxy().getPlayer(id), + () -> getProxy().getConsole(), + BungeeCommandSource::new + ); + CommandManager cloud = new BungeeCommandManager<>( + this, + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + this.commandRegistry = new CommandRegistry(geyser, cloud, false); // applying root permission would be a breaking change because we can't register permission defaults } // Force-disable query if enabled, or else Geyser won't enable @@ -181,16 +201,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } this.geyserInjector.initializeLocalChannel(this); - - this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor("geyser", this.geyser, this.geyserCommandManager.getCommands())); - for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { - Map commands = entry.getValue(); - if (commands.isEmpty()) { - continue; - } - - this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor(entry.getKey().description().id(), this.geyser, commands)); - } } @Override @@ -226,8 +236,8 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java index c68839b20..0a89b5421 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java @@ -29,8 +29,8 @@ import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; @@ -40,7 +40,7 @@ public final class GeyserBungeeUpdateListener implements Listener { public void onPlayerJoin(final PostLoginEvent event) { if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { final ProxiedPlayer player = event.getPlayer(); - if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new BungeeCommandSource(player)); } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java index e3099f170..10ccc5bac 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java @@ -27,19 +27,22 @@ package org.geysermc.geyser.platform.bungeecord.command; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.GeyserLocale; import java.util.Locale; +import java.util.UUID; public class BungeeCommandSource implements GeyserCommandSource { - private final net.md_5.bungee.api.CommandSender handle; + private final CommandSender handle; - public BungeeCommandSource(net.md_5.bungee.api.CommandSender handle) { + public BungeeCommandSource(CommandSender handle) { this.handle = handle; // Ensure even Java players' languages are loaded GeyserLocale.loadGeyserLocale(this.locale()); @@ -72,12 +75,20 @@ public class BungeeCommandSource implements GeyserCommandSource { return !(handle instanceof ProxiedPlayer); } + @Override + public @Nullable UUID playerUuid() { + if (handle instanceof ProxiedPlayer player) { + return player.getUniqueId(); + } + return null; + } + @Override public String locale() { if (handle instanceof ProxiedPlayer player) { Locale locale = player.getLocale(); if (locale != null) { - // Locale can be null early on in the conneciton + // Locale can be null early on in the connection return GeyserLocale.formatLocale(locale.getLanguage() + "_" + locale.getCountry()); } } @@ -86,6 +97,12 @@ public class BungeeCommandSource implements GeyserCommandSource { @Override public boolean hasPermission(String permission) { - return handle.hasPermission(permission); + // Handle blank permissions ourselves, as bungeecord only handles empty ones + return permission.isBlank() || handle.hasPermission(permission); + } + + @Override + public Object handle() { + return handle; } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java deleted file mode 100644 index 2d02c9950..000000000 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.bungeecord.command; - -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.plugin.Command; -import net.md_5.bungee.api.plugin.TabExecutor; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -public class GeyserBungeeCommandExecutor extends Command implements TabExecutor { - private final GeyserCommandExecutor commandExecutor; - - public GeyserBungeeCommandExecutor(String name, GeyserImpl geyser, Map commands) { - super(name); - - this.commandExecutor = new GeyserCommandExecutor(geyser, commands); - } - - @Override - public void execute(CommandSender sender, String[] args) { - BungeeCommandSource commandSender = new BungeeCommandSource(sender); - GeyserSession session = this.commandExecutor.getGeyserSession(commandSender); - - if (args.length > 0) { - GeyserCommand command = this.commandExecutor.getCommand(args[0]); - if (command != null) { - if (!sender.hasPermission(command.permission())) { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.locale()); - - commandSender.sendMessage(ChatColor.RED + message); - return; - } - if (command.isBedrockOnly() && session == null) { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.locale()); - - commandSender.sendMessage(ChatColor.RED + message); - return; - } - command.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); - } else { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", commandSender.locale()); - commandSender.sendMessage(ChatColor.RED + message); - } - } else { - this.commandExecutor.getCommand("help").execute(session, commandSender, new String[0]); - } - } - - @Override - public Iterable onTabComplete(CommandSender sender, String[] args) { - if (args.length == 1) { - return commandExecutor.tabComplete(new BungeeCommandSource(sender)); - } else { - return Collections.emptyList(); - } - } -} diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index 9215c575e..fd9d7e99d 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -1,7 +1,3 @@ -plugins { - application -} - architectury { platformSetupLoomIde() fabric() @@ -35,13 +31,12 @@ dependencies { shadow(projects.api) { isTransitive = false } shadow(projects.common) { isTransitive = false } - // Permissions - modImplementation(libs.fabric.permissions) - include(libs.fabric.permissions) + modImplementation(libs.cloud.fabric) + include(libs.cloud.fabric) } -application { - mainClass.set("org.geysermc.geyser.platform.fabric.GeyserFabricMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.fabric.GeyserFabricMain" } relocate("org.cloudburstmc.netty") diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java index c363ade8f..149246d59 100644 --- a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java +++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.platform.fabric; -import me.lucko.fabric.api.permissions.v0.Permissions; import net.fabricmc.api.EnvType; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; @@ -34,9 +33,16 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.commands.CommandSourceStack; import net.minecraft.world.entity.player.Player; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.platform.mod.GeyserModUpdateListener; +import org.geysermc.geyser.platform.mod.command.ModCommandSource; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.fabric.FabricServerCommandManager; public class GeyserFabricBootstrap extends GeyserModBootstrap implements ModInitializer { @@ -70,20 +76,23 @@ public class GeyserFabricBootstrap extends GeyserModBootstrap implements ModInit ServerPlayConnectionEvents.JOIN.register((handler, $, $$) -> GeyserModUpdateListener.onPlayReady(handler.getPlayer())); this.onGeyserInitialize(); + + var sourceConverter = CommandSourceConverter.layered( + CommandSourceStack.class, + id -> getServer().getPlayerList().getPlayer(id), + Player::createCommandSourceStack, + () -> getServer().createCommandSourceStack(), // NPE if method reference is used, since server is not available yet + ModCommandSource::new + ); + CommandManager cloud = new FabricServerCommandManager<>( + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + this.setCommandRegistry(new CommandRegistry(GeyserImpl.getInstance(), cloud, false)); // applying root permission would be a breaking change because we can't register permission defaults } @Override public boolean isServer() { return FabricLoader.getInstance().getEnvironmentType().equals(EnvType.SERVER); } - - @Override - public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) { - return Permissions.check(source, permissionNode); - } - - @Override - public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) { - return Permissions.check(source, permissionNode, permissionLevel); - } } diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts index 741e2fd11..81a35a58b 100644 --- a/bootstrap/mod/neoforge/build.gradle.kts +++ b/bootstrap/mod/neoforge/build.gradle.kts @@ -1,7 +1,3 @@ -plugins { - application -} - // This is provided by "org.cloudburstmc.math.mutable" too, so yeet. // NeoForge's class loader is *really* annoying. provided("org.cloudburstmc.math", "api") @@ -38,10 +34,13 @@ dependencies { // Include all transitive deps of core via JiJ includeTransitive(projects.core) + + modImplementation(libs.cloud.neoforge) + include(libs.cloud.neoforge) } -application { - mainClass.set("org.geysermc.geyser.platform.forge.GeyserNeoForgeMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.neoforge.GeyserNeoForgeMain" } tasks { diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java index b97e42389..7d3b9dc5f 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.platform.neoforge; import net.minecraft.commands.CommandSourceStack; import net.minecraft.world.entity.player.Player; +import net.neoforged.bus.api.EventPriority; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLLoader; @@ -35,15 +36,22 @@ import net.neoforged.neoforge.event.GameShuttingDownEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent; import net.neoforged.neoforge.event.server.ServerStoppingEvent; -import org.checkerframework.checker.nullness.qual.NonNull; +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.platform.mod.GeyserModUpdateListener; +import org.geysermc.geyser.platform.mod.command.ModCommandSource; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.neoforge.NeoForgeServerCommandManager; + +import java.util.Objects; @Mod(ModConstants.MOD_ID) public class GeyserNeoForgeBootstrap extends GeyserModBootstrap { - private final GeyserNeoForgePermissionHandler permissionHandler = new GeyserNeoForgePermissionHandler(); - public GeyserNeoForgeBootstrap(ModContainer container) { super(new GeyserNeoForgePlatform(container)); @@ -56,9 +64,25 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap { NeoForge.EVENT_BUS.addListener(this::onServerStopping); NeoForge.EVENT_BUS.addListener(this::onPlayerJoin); - NeoForge.EVENT_BUS.addListener(this.permissionHandler::onPermissionGather); + + NeoForge.EVENT_BUS.addListener(EventPriority.HIGHEST, this::onPermissionGather); this.onGeyserInitialize(); + + var sourceConverter = CommandSourceConverter.layered( + CommandSourceStack.class, + id -> getServer().getPlayerList().getPlayer(id), + Player::createCommandSourceStack, + () -> getServer().createCommandSourceStack(), + ModCommandSource::new + ); + CommandManager cloud = new NeoForgeServerCommandManager<>( + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + GeyserNeoForgeCommandRegistry registry = new GeyserNeoForgeCommandRegistry(getGeyser(), cloud); + this.setCommandRegistry(registry); + NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, registry::onPermissionGatherForUndefined); } private void onServerStarted(ServerStartedEvent event) { @@ -87,13 +111,17 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap { return FMLLoader.getDist().isDedicatedServer(); } - @Override - public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) { - return this.permissionHandler.hasPermission(source, permissionNode); - } + private void onPermissionGather(PermissionGatherEvent.Nodes event) { + getGeyser().eventBus().fire( + (GeyserRegisterPermissionsEvent) (permission, defaultValue) -> { + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(defaultValue, "permission default for " + permission); - @Override - public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) { - return this.permissionHandler.hasPermission(source, permissionNode, permissionLevel); + if (permission.isBlank()) { + return; + } + PermissionUtils.register(permission, defaultValue, event); + } + ); } } diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java new file mode 100644 index 000000000..a8854d5d9 --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019-2024 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.geyser.platform.neoforge; + +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.neoforge.PermissionNotRegisteredException; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class GeyserNeoForgeCommandRegistry extends CommandRegistry { + + /** + * Permissions with an undefined permission default. Use Set to not register the same fallback more than once. + * NeoForge requires that all permissions are registered, and cloud-neoforge follows that. + * This is unlike most platforms, on which we wouldn't register a permission if no default was provided. + */ + private final Set undefinedPermissions = new HashSet<>(); + + public GeyserNeoForgeCommandRegistry(GeyserImpl geyser, CommandManager cloud) { + super(geyser, cloud); + } + + @Override + protected void register(GeyserCommand command, Map commands) { + super.register(command, commands); + + // FIRST STAGE: Collect all permissions that may have undefined defaults. + if (!command.permission().isBlank() && command.permissionDefault() == null) { + // Permission requirement exists but no default value specified. + undefinedPermissions.add(command.permission()); + } + } + + @Override + protected void onRegisterPermissions(GeyserRegisterPermissionsEvent event) { + super.onRegisterPermissions(event); + + // SECOND STAGE + // Now that we are aware of all commands, we can eliminate some incorrect assumptions. + // Example: two commands may have the same permission, but only of them defines a permission default. + undefinedPermissions.removeAll(permissionDefaults.keySet()); + } + + /** + * Registers permissions with possibly undefined defaults. + * Should be subscribed late to allow extensions and mods to register a desired permission default first. + */ + void onPermissionGatherForUndefined(PermissionGatherEvent.Nodes event) { + // THIRD STAGE + for (String permission : undefinedPermissions) { + if (PermissionUtils.register(permission, TriState.NOT_SET, event)) { + // The permission was not already registered + geyser.getLogger().debug("Registered permission " + permission + " with fallback default value of NOT_SET"); + } + } + } + + @Override + public boolean hasPermission(GeyserCommandSource source, String permission) { + // NeoForgeServerCommandManager will throw this exception if the permission is not registered to the server. + // We can't realistically ensure that every permission is registered (calls by API users), so we catch this. + // This works for our calls, but not for cloud's internal usage. For that case, see above. + try { + return super.hasPermission(source, permission); + } catch (PermissionNotRegisteredException e) { + return false; + } + } +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java deleted file mode 100644 index 0a5f8f052..000000000 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2019-2023 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.geyser.platform.neoforge; - -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.entity.player.Player; -import net.neoforged.neoforge.server.permission.PermissionAPI; -import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; -import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKey; -import net.neoforged.neoforge.server.permission.nodes.PermissionNode; -import net.neoforged.neoforge.server.permission.nodes.PermissionType; -import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.Constants; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.command.GeyserCommandManager; - -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Map; - -public class GeyserNeoForgePermissionHandler { - - private static final Constructor PERMISSION_NODE_CONSTRUCTOR; - - static { - try { - @SuppressWarnings("rawtypes") - Constructor constructor = PermissionNode.class.getDeclaredConstructor( - String.class, - PermissionType.class, - PermissionNode.PermissionResolver.class, - PermissionDynamicContextKey[].class - ); - constructor.setAccessible(true); - PERMISSION_NODE_CONSTRUCTOR = constructor; - } catch (NoSuchMethodException e) { - throw new RuntimeException("Unable to construct PermissionNode!", e); - } - } - - private final Map> permissionNodes = new HashMap<>(); - - public void onPermissionGather(PermissionGatherEvent.Nodes event) { - this.registerNode(Constants.UPDATE_PERMISSION, event); - - GeyserCommandManager commandManager = GeyserImpl.getInstance().commandManager(); - for (Map.Entry entry : commandManager.commands().entrySet()) { - Command command = entry.getValue(); - - // Don't register aliases - if (!command.name().equals(entry.getKey())) { - continue; - } - - this.registerNode(command.permission(), event); - } - - for (Map commands : commandManager.extensionCommands().values()) { - for (Map.Entry entry : commands.entrySet()) { - Command command = entry.getValue(); - - // Don't register aliases - if (!command.name().equals(entry.getKey())) { - continue; - } - - this.registerNode(command.permission(), event); - } - } - } - - public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) { - PermissionNode node = this.permissionNodes.get(permissionNode); - if (node == null) { - GeyserImpl.getInstance().getLogger().warning("Unable to find permission node " + permissionNode); - return false; - } - - return PermissionAPI.getPermission((ServerPlayer) source, node); - } - - public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) { - if (!source.isPlayer()) { - return true; - } - assert source.getPlayer() != null; - boolean permission = this.hasPermission(source.getPlayer(), permissionNode); - if (!permission) { - return source.getPlayer().hasPermissions(permissionLevel); - } - - return true; - } - - private void registerNode(String node, PermissionGatherEvent.Nodes event) { - PermissionNode permissionNode = this.createNode(node); - - // NeoForge likes to crash if you try and register a duplicate node - if (!event.getNodes().contains(permissionNode)) { - event.addNodes(permissionNode); - this.permissionNodes.put(node, permissionNode); - } - } - - @SuppressWarnings("unchecked") - private PermissionNode createNode(String node) { - // The typical constructors in PermissionNode require a - // mod id, which means our permission nodes end up becoming - // geyser_neoforge. instead of just . We work around - // this by using reflection to access the constructor that - // doesn't require a mod id or ResourceLocation. - try { - return (PermissionNode) PERMISSION_NODE_CONSTRUCTOR.newInstance( - node, - PermissionTypes.BOOLEAN, - (PermissionNode.PermissionResolver) (player, playerUUID, context) -> false, - new PermissionDynamicContextKey[0] - ); - } catch (Exception e) { - throw new RuntimeException("Unable to create permission node " + node, e); - } - } -} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java new file mode 100644 index 000000000..c57dc9a6c --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 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.geyser.platform.neoforge; + +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.platform.neoforge.mixin.PermissionNodeMixin; + +/** + * Common logic for handling the more complicated way we have to register permission on NeoForge + */ +public class PermissionUtils { + + private PermissionUtils() { + //no + } + + /** + * Registers the given permission and its default value to the event. If the permission has the same name as one + * that has already been registered to the event, it will not be registered. In other words, it will not override. + * + * @param permission the permission to register + * @param permissionDefault the permission's default value. See {@link GeyserRegisterPermissionsEvent#register(String, TriState)} for TriState meanings. + * @param event the registration event + * @return true if the permission was registered + */ + public static boolean register(String permission, TriState permissionDefault, PermissionGatherEvent.Nodes event) { + // NeoForge likes to crash if you try and register a duplicate node + if (event.getNodes().stream().noneMatch(n -> n.getNodeName().equals(permission))) { + PermissionNode node = createNode(permission, permissionDefault); + event.addNodes(node); + return true; + } + return false; + } + + private static PermissionNode createNode(String node, TriState permissionDefault) { + return PermissionNodeMixin.geyser$construct( + node, + PermissionTypes.BOOLEAN, + (player, playerUUID, context) -> switch (permissionDefault) { + case TRUE -> true; + case FALSE -> false; + case NOT_SET -> { + if (player != null) { + yield player.createCommandSourceStack().hasPermission(player.server.getOperatorUserPermissionLevel()); + } + yield false; // NeoForge javadocs say player is null in the case of an offline player. + } + } + ); + } +} diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java new file mode 100644 index 000000000..a43acd58a --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2024 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.geyser.platform.neoforge.mixin; + +import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKey; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(value = PermissionNode.class, remap = false) // this is API - do not remap +public interface PermissionNodeMixin { + + /** + * Invokes the matching private constructor in {@link PermissionNode}. + *

    + * The typical constructors in PermissionNode require a mod id, which means our permission nodes + * would end up becoming {@code geyser_neoforge.} instead of just {@code }. + */ + @SuppressWarnings("rawtypes") // the varargs + @Invoker("") + static PermissionNode geyser$construct(String nodeName, PermissionType type, PermissionNode.PermissionResolver defaultResolver, PermissionDynamicContextKey... dynamics) { + throw new IllegalStateException(); + } +} diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml index fa01bb6ec..56b7d68e1 100644 --- a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -11,6 +11,8 @@ authors="GeyserMC" description="${description}" [[mixins]] config = "geyser.mixins.json" +[[mixins]] +config = "geyser_neoforge.mixins.json" [[dependencies.geyser_neoforge]] modId="neoforge" type="required" diff --git a/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json b/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json new file mode 100644 index 000000000..f1653051c --- /dev/null +++ b/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.geysermc.geyser.platform.neoforge.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "PermissionNodeMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java index d7373f0a9..f11b5fbd6 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java @@ -25,30 +25,21 @@ package org.geysermc.geyser.platform.mod; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.Commands; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.entity.player.Player; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.mod.command.GeyserModCommandExecutor; import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform; import org.geysermc.geyser.platform.mod.world.GeyserModWorldManager; import org.geysermc.geyser.text.GeyserLocale; @@ -59,7 +50,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.SocketAddress; import java.nio.file.Path; -import java.util.Map; import java.util.UUID; @RequiredArgsConstructor @@ -70,13 +60,15 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { private final GeyserModPlatform platform; + @Getter private GeyserImpl geyser; private Path dataFolder; - @Setter + @Setter @Getter private MinecraftServer server; - private GeyserCommandManager geyserCommandManager; + @Setter + private CommandRegistry commandRegistry; private GeyserModConfiguration geyserConfig; private GeyserModInjector geyserInjector; private final GeyserModLogger geyserLogger = new GeyserModLogger(); @@ -94,10 +86,6 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); this.geyser = GeyserImpl.load(this.platform.platformType(), this); - - // Create command manager here, since the permission handler on neo needs it - this.geyserCommandManager = new GeyserCommandManager(geyser); - this.geyserCommandManager.init(); } public void onGeyserEnable() { @@ -130,50 +118,6 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { if (isServer()) { this.geyserInjector.initializeLocalChannel(this); } - - // Start command building - // Set just "geyser" as the help command - GeyserModCommandExecutor helpExecutor = new GeyserModCommandExecutor(geyser, - (GeyserCommand) geyser.commandManager().getCommands().get("help")); - LiteralArgumentBuilder builder = Commands.literal("geyser").executes(helpExecutor); - - // Register all subcommands as valid - for (Map.Entry command : geyser.commandManager().getCommands().entrySet()) { - GeyserModCommandExecutor executor = new GeyserModCommandExecutor(geyser, (GeyserCommand) command.getValue()); - builder.then(Commands.literal(command.getKey()) - .executes(executor) - // Could also test for Bedrock but depending on when this is called it may backfire - .requires(executor::testPermission) - // Allows parsing of arguments; e.g. for /geyser dump logs or the connectiontest command - .then(Commands.argument("args", StringArgumentType.greedyString()) - .executes(context -> executor.runWithArgs(context, StringArgumentType.getString(context, "args"))) - .requires(executor::testPermission))); - } - server.getCommands().getDispatcher().register(builder); - - // Register extension commands - for (Map.Entry> extensionMapEntry : geyser.commandManager().extensionCommands().entrySet()) { - Map extensionCommands = extensionMapEntry.getValue(); - if (extensionCommands.isEmpty()) { - continue; - } - - // Register help command for just "/" - GeyserModCommandExecutor extensionHelpExecutor = new GeyserModCommandExecutor(geyser, - (GeyserCommand) extensionCommands.get("help")); - LiteralArgumentBuilder extCmdBuilder = Commands.literal(extensionMapEntry.getKey().description().id()).executes(extensionHelpExecutor); - - for (Map.Entry command : extensionCommands.entrySet()) { - GeyserModCommandExecutor executor = new GeyserModCommandExecutor(geyser, (GeyserCommand) command.getValue()); - extCmdBuilder.then(Commands.literal(command.getKey()) - .executes(executor) - .requires(executor::testPermission) - .then(Commands.argument("args", StringArgumentType.greedyString()) - .executes(context -> executor.runWithArgs(context, StringArgumentType.getString(context, "args"))) - .requires(executor::testPermission))); - } - server.getCommands().getDispatcher().register(extCmdBuilder); - } } @Override @@ -206,8 +150,8 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return commandRegistry; } @Override @@ -235,6 +179,7 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { return this.server.getServerVersion(); } + @SuppressWarnings("ConstantConditions") // Certain IDEA installations think that ip cannot be null @NonNull @Override public String getServerBindAddress() { @@ -270,10 +215,6 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { return this.platform.resolveResource(resource); } - public abstract boolean hasPermission(@NonNull Player source, @NonNull String permissionNode); - - public abstract boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel); - @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { try { diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java index 11ca0bc4f..6a724155f 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java @@ -25,17 +25,18 @@ package org.geysermc.geyser.platform.mod; -import net.minecraft.commands.CommandSourceStack; import net.minecraft.world.entity.player.Player; -import org.geysermc.geyser.Constants; -import org.geysermc.geyser.platform.mod.command.ModCommandSender; +import org.geysermc.geyser.Permissions; +import org.geysermc.geyser.platform.mod.command.ModCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; public final class GeyserModUpdateListener { public static void onPlayReady(Player player) { - CommandSourceStack stack = player.createCommandSourceStack(); - if (GeyserModBootstrap.getInstance().hasPermission(stack, Constants.UPDATE_PERMISSION, 2)) { - VersionCheckUtils.checkForGeyserUpdate(() -> new ModCommandSender(stack)); + // Should be creating this in the supplier, but we need it for the permission check. + // Not a big deal currently because ModCommandSource doesn't load locale, so don't need to try to wait for it. + ModCommandSource source = new ModCommandSource(player.createCommandSourceStack()); + if (source.hasPermission(Permissions.CHECK_UPDATE)) { + VersionCheckUtils.checkForGeyserUpdate(() -> source); } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java deleted file mode 100644 index 694dc732e..000000000 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.mod.command; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.context.CommandContext; -import net.minecraft.commands.CommandSourceStack; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.platform.mod.GeyserModBootstrap; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.ChatColor; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Collections; - -public class GeyserModCommandExecutor extends GeyserCommandExecutor implements Command { - private final GeyserCommand command; - - public GeyserModCommandExecutor(GeyserImpl geyser, GeyserCommand command) { - super(geyser, Collections.singletonMap(command.name(), command)); - this.command = command; - } - - public boolean testPermission(CommandSourceStack source) { - return GeyserModBootstrap.getInstance().hasPermission(source, command.permission(), command.isSuggestedOpOnly() ? 2 : 0); - } - - @Override - public int run(CommandContext context) { - return runWithArgs(context, ""); - } - - public int runWithArgs(CommandContext context, String args) { - CommandSourceStack source = context.getSource(); - ModCommandSender sender = new ModCommandSender(source); - GeyserSession session = getGeyserSession(sender); - if (!testPermission(source)) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return 0; - } - - if (command.isBedrockOnly() && session == null) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.locale())); - return 0; - } - - command.execute(session, sender, args.split(" ")); - return 0; - } -} diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java similarity index 77% rename from bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java index 5bebfae93..af1f368b3 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java @@ -31,19 +31,21 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.text.ChatColor; import java.util.Objects; +import java.util.UUID; -public class ModCommandSender implements GeyserCommandSource { +public class ModCommandSource implements GeyserCommandSource { private final CommandSourceStack source; - public ModCommandSender(CommandSourceStack source) { + public ModCommandSource(CommandSourceStack source) { this.source = source; + // todo find locale? } @Override @@ -75,8 +77,24 @@ public class ModCommandSender implements GeyserCommandSource { return !(source.getEntity() instanceof ServerPlayer); } + @Override + public @Nullable UUID playerUuid() { + if (source.getEntity() instanceof ServerPlayer player) { + return player.getUUID(); + } + return null; + } + @Override public boolean hasPermission(String permission) { - return GeyserModBootstrap.getInstance().hasPermission(source, permission, source.getServer().getOperatorUserPermissionLevel()); + // Unlike other bootstraps; we delegate to cloud here too: + // On NeoForge; we'd have to keep track of all PermissionNodes - cloud already does that + // For Fabric, we won't need to include the Fabric Permissions API anymore - cloud already does that too :p + return GeyserImpl.getInstance().commandRegistry().hasPermission(this, permission); + } + + @Override + public Object handle() { + return source; } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java index db1768737..89452eba3 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java @@ -48,7 +48,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3i; import org.geysermc.geyser.level.GeyserWorldManager; import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.platform.mod.GeyserModBootstrap; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.Holder; @@ -111,12 +110,6 @@ public class GeyserModWorldManager extends GeyserWorldManager { return SharedConstants.getCurrentVersion().getProtocolVersion() == GameProtocol.getJavaProtocolVersion(); } - @Override - public boolean hasPermission(GeyserSession session, String permission) { - ServerPlayer player = getPlayer(session); - return GeyserModBootstrap.getInstance().hasPermission(player, permission); - } - @Override public GameMode getDefaultGameMode(GeyserSession session) { return GameMode.byId(server.getDefaultGameType().getId()); diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index fcb85f100..0a1271145 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -17,12 +17,12 @@ dependencies { classifier("all") // otherwise the unshaded jar is used without the shaded NMS implementations }) + implementation(libs.cloud.paper) implementation(libs.commodore) implementation(libs.adventure.text.serializer.bungeecord) compileOnly(libs.folia.api) - compileOnly(libs.paper.mojangapi) compileOnlyApi(libs.viaversion) } @@ -33,13 +33,15 @@ platformRelocate("com.fasterxml.jackson") platformRelocate("net.kyori", "net.kyori.adventure.text.logger.slf4j.ComponentLogger") platformRelocate("org.objectweb.asm") platformRelocate("me.lucko.commodore") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated platformRelocate("org.yaml") // Broken as of 1.20 // These dependencies are already present on the platform provided(libs.viaversion) -application { - mainClass.set("org.geysermc.geyser.platform.spigot.GeyserSpigotMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.spigot.GeyserSpigotMain" } tasks.withType { diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index 2d13155f2..3bb44a4bc 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -30,37 +30,34 @@ import com.viaversion.viaversion.api.data.MappingData; import com.viaversion.viaversion.api.protocol.ProtocolPathEntry; import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import io.netty.buffer.ByteBuf; -import me.lucko.commodore.CommodoreProvider; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; -import org.bukkit.command.CommandMap; -import org.bukkit.command.PluginCommand; +import org.bukkit.command.CommandSender; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; -import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.adapters.paper.PaperAdapters; import org.geysermc.geyser.adapters.spigot.SpigotAdapters; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.spigot.command.GeyserBrigadierSupport; -import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandExecutor; -import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandManager; +import org.geysermc.geyser.platform.spigot.command.SpigotCommandRegistry; +import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource; import org.geysermc.geyser.platform.spigot.world.GeyserPistonListener; import org.geysermc.geyser.platform.spigot.world.GeyserSpigotBlockPlaceListener; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotLegacyNativeWorldManager; @@ -68,21 +65,21 @@ import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotNativeWorld import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.bukkit.BukkitCommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.paper.LegacyPaperCommandManager; import java.io.File; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.SocketAddress; import java.nio.file.Path; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.UUID; public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { - private GeyserSpigotCommandManager geyserCommandManager; + private CommandRegistry commandRegistry; private GeyserSpigotConfiguration geyserConfig; private GeyserSpigotInjector geyserInjector; private final GeyserSpigotLogger geyserLogger = GeyserPaperLogger.supported() ? @@ -165,31 +162,37 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { @Override public void onEnable() { - this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); - this.geyserCommandManager.init(); - - // Because Bukkit locks its command map upon startup, we need to - // add our plugin commands in onEnable, but populating the executor - // can happen at any time (later in #onGeyserEnable()) - CommandMap commandMap = GeyserSpigotCommandManager.getCommandMap(); - for (Extension extension : this.geyserCommandManager.extensionCommands().keySet()) { - // Thanks again, Bukkit - try { - Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); - constructor.setAccessible(true); - - PluginCommand pluginCommand = constructor.newInstance(extension.description().id(), this); - pluginCommand.setDescription("The main command for the " + extension.name() + " Geyser extension!"); - - commandMap.register(extension.description().id(), "geyserext", pluginCommand); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { - this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex); - } + // Create command manager early so we can add Geyser extension commands + var sourceConverter = new CommandSourceConverter<>( + CommandSender.class, + Bukkit::getPlayer, + Bukkit::getConsoleSender, + SpigotCommandSource::new + ); + LegacyPaperCommandManager cloud; + try { + // LegacyPaperCommandManager works for spigot too, see https://cloud.incendo.org/minecraft/paper + cloud = new LegacyPaperCommandManager<>( + this, + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + } catch (Exception e) { + throw new RuntimeException(e); } + try { + // Commodore brigadier on Spigot/Paper 1.13 - 1.18.2 + // Paper-only brigadier on 1.19+ + cloud.registerBrigadier(); + } catch (BukkitCommandManager.BrigadierInitializationException e) { + geyserLogger.debug("Failed to initialize Brigadier support: " + e.getMessage()); + } + + this.commandRegistry = new SpigotCommandRegistry(geyser, cloud); + // Needs to be an anonymous inner class otherwise Bukkit complains about missing classes Bukkit.getPluginManager().registerEvents(new Listener() { - @EventHandler public void onServerLoaded(ServerLoadEvent event) { if (event.getType() == ServerLoadEvent.LoadType.RELOAD) { @@ -227,7 +230,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { } geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); - // Don't need to re-create the world manager/re-register commands/reinject when reloading + // Don't need to re-create the world manager/reinject when reloading if (GeyserImpl.getInstance().isReloading()) { return; } @@ -282,79 +285,40 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.debug("Using default world manager."); } - PluginCommand geyserCommand = this.getCommand("geyser"); - Objects.requireNonNull(geyserCommand, "base command cannot be null"); - geyserCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands())); - - for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { - Map commands = entry.getValue(); - if (commands.isEmpty()) { - continue; - } - - PluginCommand command = this.getCommand(entry.getKey().description().id()); - if (command == null) { - continue; - } - - command.setExecutor(new GeyserSpigotCommandExecutor(this.geyser, commands)); - } - // Register permissions so they appear in, for example, LuckPerms' UI - // Re-registering permissions throws an error - for (Map.Entry entry : geyserCommandManager.commands().entrySet()) { - Command command = entry.getValue(); - if (command.aliases().contains(entry.getKey())) { - // Don't register aliases - continue; + // Re-registering permissions without removing it throws an error + PluginManager pluginManager = Bukkit.getPluginManager(); + geyser.eventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> { + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(def, "permission default for " + permission); + + if (permission.isBlank()) { + return; + } + PermissionDefault permissionDefault = switch (def) { + case TRUE -> PermissionDefault.TRUE; + case FALSE -> PermissionDefault.FALSE; + case NOT_SET -> PermissionDefault.OP; + }; + + Permission existingPermission = pluginManager.getPermission(permission); + if (existingPermission != null) { + geyserLogger.debug("permission " + permission + " with default " + + existingPermission.getDefault() + " is being overridden by " + permissionDefault); + + pluginManager.removePermission(permission); } - Bukkit.getPluginManager().addPermission(new Permission(command.permission(), - GeyserLocale.getLocaleStringLog(command.description()), - command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); - } - - // Register permissions for extension commands - for (Map.Entry> commandEntry : this.geyserCommandManager.extensionCommands().entrySet()) { - for (Map.Entry entry : commandEntry.getValue().entrySet()) { - Command command = entry.getValue(); - if (command.aliases().contains(entry.getKey())) { - // Don't register aliases - continue; - } - - if (command.permission().isBlank()) { - continue; - } - - // Avoid registering the same permission twice, e.g. for the extension help commands - if (Bukkit.getPluginManager().getPermission(command.permission()) != null) { - GeyserImpl.getInstance().getLogger().debug("Skipping permission " + command.permission() + " as it is already registered"); - continue; - } - - Bukkit.getPluginManager().addPermission(new Permission(command.permission(), - GeyserLocale.getLocaleStringLog(command.description()), - command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); - } - } - - Bukkit.getPluginManager().addPermission(new Permission(Constants.UPDATE_PERMISSION, - "Whether update notifications can be seen", PermissionDefault.OP)); + pluginManager.addPermission(new Permission(permission, permissionDefault)); + }); // Events cannot be unregistered - re-registering results in duplicate firings GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager); - Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); + pluginManager.registerEvents(blockPlaceListener, this); - Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); + pluginManager.registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); - Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigotUpdateListener(), this); - - boolean brigadierSupported = CommodoreProvider.isSupported(); - geyserLogger.debug("Brigadier supported? " + brigadierSupported); - if (brigadierSupported) { - GeyserBrigadierSupport.loadBrigadier(this, geyserCommand); - } + pluginManager.registerEvents(new GeyserSpigotUpdateListener(), this); } @Override @@ -390,8 +354,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java index 5e3c4def8..8a8a43460 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java @@ -29,8 +29,8 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; @@ -40,7 +40,7 @@ public final class GeyserSpigotUpdateListener implements Listener { public void onPlayerJoin(final PlayerJoinEvent event) { if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { final Player player = event.getPlayer(); - if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new SpigotCommandSource(player)); } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java deleted file mode 100644 index 61900174c..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.spigot.command; - -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import me.lucko.commodore.Commodore; -import me.lucko.commodore.CommodoreProvider; -import org.bukkit.Bukkit; -import org.bukkit.command.PluginCommand; -import org.geysermc.geyser.platform.spigot.GeyserSpigotPlugin; - -/** - * Needs to be a separate class so pre-1.13 loads correctly. - */ -public final class GeyserBrigadierSupport { - - public static void loadBrigadier(GeyserSpigotPlugin plugin, PluginCommand pluginCommand) { - // Enable command completions if supported - // This is beneficial because this is sent over the network and Bedrock can see it - Commodore commodore = CommodoreProvider.getCommodore(plugin); - LiteralArgumentBuilder builder = LiteralArgumentBuilder.literal("geyser"); - for (String command : plugin.getGeyserCommandManager().getCommands().keySet()) { - builder.then(LiteralArgumentBuilder.literal(command)); - } - commodore.register(pluginCommand, builder); - - try { - Class.forName("com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent"); - Bukkit.getServer().getPluginManager().registerEvents(new GeyserPaperCommandListener(), plugin); - plugin.getGeyserLogger().debug("Successfully registered AsyncPlayerSendCommandsEvent listener."); - } catch (ClassNotFoundException e) { - plugin.getGeyserLogger().debug("Not registering AsyncPlayerSendCommandsEvent listener."); - } - } - - private GeyserBrigadierSupport() { - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java deleted file mode 100644 index dcec045ab..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.spigot.command; - -import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; -import com.mojang.brigadier.tree.CommandNode; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; - -import java.net.InetSocketAddress; -import java.util.Iterator; -import java.util.Map; - -public final class GeyserPaperCommandListener implements Listener { - - @SuppressWarnings("UnstableApiUsage") - @EventHandler - public void onCommandSend(AsyncPlayerSendCommandsEvent event) { - // Documentation says to check (event.isAsynchronous() || !event.hasFiredAsync()), but as of Paper 1.18.2 - // event.hasFiredAsync is never true - if (event.isAsynchronous()) { - CommandNode geyserBrigadier = event.getCommandNode().getChild("geyser"); - if (geyserBrigadier != null) { - Player player = event.getPlayer(); - boolean isJavaPlayer = isProbablyJavaPlayer(player); - Map commands = GeyserImpl.getInstance().commandManager().getCommands(); - Iterator> it = geyserBrigadier.getChildren().iterator(); - - while (it.hasNext()) { - CommandNode subnode = it.next(); - Command command = commands.get(subnode.getName()); - if (command != null) { - if ((command.isBedrockOnly() && isJavaPlayer) || !player.hasPermission(command.permission())) { - // Remove this from the node as we don't have permission to use it - it.remove(); - } - } - } - } - } - } - - /** - * This early on, there is a rare chance that Geyser has yet to process the connection. We'll try to minimize that - * chance, though. - */ - private boolean isProbablyJavaPlayer(Player player) { - if (GeyserImpl.getInstance().connectionByUuid(player.getUniqueId()) != null) { - // For sure this is a Bedrock player - return false; - } - - if (GeyserImpl.getInstance().getConfig().isUseDirectConnection()) { - InetSocketAddress address = player.getAddress(); - if (address != null) { - return address.getPort() != 0; - } - } - return true; - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java deleted file mode 100644 index 6780bde17..000000000 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.spigot.command; - -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class GeyserSpigotCommandExecutor extends GeyserCommandExecutor implements TabExecutor { - - public GeyserSpigotCommandExecutor(GeyserImpl geyser, Map commands) { - super(geyser, commands); - } - - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String[] args) { - SpigotCommandSource commandSender = new SpigotCommandSource(sender); - GeyserSession session = getGeyserSession(commandSender); - - if (args.length > 0) { - GeyserCommand geyserCommand = getCommand(args[0]); - if (geyserCommand != null) { - if (!sender.hasPermission(geyserCommand.permission())) { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.locale()); - - commandSender.sendMessage(ChatColor.RED + message); - return true; - } - if (geyserCommand.isBedrockOnly() && session == null) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.locale())); - return true; - } - geyserCommand.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]); - return true; - } else { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", commandSender.locale()); - commandSender.sendMessage(ChatColor.RED + message); - } - } else { - getCommand("help").execute(session, commandSender, new String[0]); - return true; - } - return true; - } - - @Override - public List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String[] args) { - if (args.length == 1) { - return tabComplete(new SpigotCommandSource(sender)); - } - return Collections.emptyList(); - } -} diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java similarity index 61% rename from bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java rename to bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java index 655d3be23..39496d2c6 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2023 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 @@ -29,16 +29,21 @@ import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.command.Command; import org.bukkit.command.CommandMap; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.incendo.cloud.CommandManager; import java.lang.reflect.Field; -public class GeyserSpigotCommandManager extends GeyserCommandManager { +public class SpigotCommandRegistry extends CommandRegistry { - private static final CommandMap COMMAND_MAP; + private final CommandMap commandMap; + + public SpigotCommandRegistry(GeyserImpl geyser, CommandManager cloud) { + super(geyser, cloud); - static { CommandMap commandMap = null; try { // Paper-only @@ -49,24 +54,28 @@ public class GeyserSpigotCommandManager extends GeyserCommandManager { Field cmdMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); cmdMapField.setAccessible(true); commandMap = (CommandMap) cmdMapField.get(Bukkit.getServer()); - } catch (NoSuchFieldException | IllegalAccessException ex) { - ex.printStackTrace(); + } catch (Exception ex) { + geyser.getLogger().error("Failed to get Spigot's CommandMap", ex); } } - COMMAND_MAP = commandMap; - } - - public GeyserSpigotCommandManager(GeyserImpl geyser) { - super(geyser); + this.commandMap = commandMap; } + @NonNull @Override - public String description(String command) { - Command cmd = COMMAND_MAP.getCommand(command.replace("/", "")); - return cmd != null ? cmd.getDescription() : ""; - } + public String description(@NonNull String command, @NonNull String locale) { + // check if the command is /geyser or an extension command so that we can localize the description + String description = super.description(command, locale); + if (!description.isBlank()) { + return description; + } - public static CommandMap getCommandMap() { - return COMMAND_MAP; + if (commandMap != null) { + Command cmd = commandMap.getCommand(command); + if (cmd != null) { + return cmd.getDescription(); + } + } + return ""; } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java index 365e9ad17..c1fb837c2 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java @@ -27,17 +27,21 @@ package org.geysermc.geyser.platform.spigot.command; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.platform.spigot.PaperAdventure; import org.geysermc.geyser.text.GeyserLocale; -public class SpigotCommandSource implements GeyserCommandSource { - private final org.bukkit.command.CommandSender handle; +import java.util.UUID; - public SpigotCommandSource(org.bukkit.command.CommandSender handle) { +public class SpigotCommandSource implements GeyserCommandSource { + private final CommandSender handle; + + public SpigotCommandSource(CommandSender handle) { this.handle = handle; // Ensure even Java players' languages are loaded GeyserLocale.loadGeyserLocale(locale()); @@ -65,11 +69,24 @@ public class SpigotCommandSource implements GeyserCommandSource { handle.spigot().sendMessage(BungeeComponentSerializer.get().serialize(message)); } + @Override + public Object handle() { + return handle; + } + @Override public boolean isConsole() { return handle instanceof ConsoleCommandSender; } + @Override + public @Nullable UUID playerUuid() { + if (handle instanceof Player player) { + return player.getUniqueId(); + } + return null; + } + @SuppressWarnings("deprecation") @Override public String locale() { @@ -83,6 +100,7 @@ public class SpigotCommandSource implements GeyserCommandSource { @Override public boolean hasPermission(String permission) { - return handle.hasPermission(permission); + // Don't trust Spigot to handle blank permissions + return permission.isBlank() || handle.hasPermission(permission); } } diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java index 73356c4e7..6588a22a3 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java @@ -128,15 +128,6 @@ public class GeyserSpigotWorldManager extends WorldManager { return GameMode.byId(Bukkit.getDefaultGameMode().ordinal()); } - @Override - public boolean hasPermission(GeyserSession session, String permission) { - Player player = Bukkit.getPlayer(session.javaUuid()); - if (player != null) { - return player.hasPermission(permission); - } - return false; - } - @Override public @NonNull CompletableFuture<@Nullable DataComponents> getPickItemComponents(GeyserSession session, int x, int y, int z, boolean addNbtData) { Player bukkitPlayer; diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml index 6e81ccdb6..14e98f577 100644 --- a/bootstrap/spigot/src/main/resources/plugin.yml +++ b/bootstrap/spigot/src/main/resources/plugin.yml @@ -6,11 +6,3 @@ version: ${version} softdepend: ["ViaVersion", "floodgate"] api-version: 1.13 folia-supported: true -commands: - geyser: - description: The main command for Geyser. - usage: /geyser - permission: geyser.command -permissions: - geyser.command: - default: true diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts index eaf895108..fd81dad63 100644 --- a/bootstrap/standalone/build.gradle.kts +++ b/bootstrap/standalone/build.gradle.kts @@ -1,5 +1,9 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer +plugins { + application +} + val terminalConsoleVersion = "1.2.0" val jlineVersion = "3.21.0" diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index f289fa2ba..87fbbf0aa 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -42,7 +42,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.configuration.GeyserJacksonConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; @@ -69,7 +70,8 @@ import java.util.stream.Collectors; public class GeyserStandaloneBootstrap implements GeyserBootstrap { - private GeyserCommandManager geyserCommandManager; + private StandaloneCloudCommandManager cloud; + private CommandRegistry commandRegistry; private GeyserStandaloneConfiguration geyserConfig; private final GeyserStandaloneLogger geyserLogger = new GeyserStandaloneLogger(); private IGeyserPingPassthrough geyserPingPassthrough; @@ -222,13 +224,24 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { geyser = GeyserImpl.load(PlatformType.STANDALONE, this); - geyserCommandManager = new GeyserCommandManager(geyser); - geyserCommandManager.init(); + boolean reloading = geyser.isReloading(); + if (!reloading) { + // Currently there would be no significant benefit of re-initializing commands. Also, we would have to unsubscribe CommandRegistry. + // Fire GeyserDefineCommandsEvent after PreInitEvent, before PostInitEvent, for consistency with other bootstraps. + cloud = new StandaloneCloudCommandManager(geyser); + commandRegistry = new CommandRegistry(geyser, cloud); + } GeyserImpl.start(); + if (!reloading) { + // Event must be fired after CommandRegistry has subscribed its listener. + // Also, the subscription for the Permissions class is created when Geyser is initialized. + cloud.fireRegisterPermissionsEvent(); + } + if (gui != null) { - gui.enableCommands(geyser.getScheduledThread(), geyserCommandManager); + gui.enableCommands(geyser.getScheduledThread(), commandRegistry); } geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); @@ -255,8 +268,6 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { @Override public void onGeyserDisable() { - // We can re-register commands on standalone, so why not - GeyserImpl.getInstance().commandManager().getCommands().clear(); geyser.disable(); } @@ -277,8 +288,8 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return commandRegistry; } @Override diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java index 3a34920ce..21e6a5e82 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java @@ -44,7 +44,9 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey @Override protected void runCommand(String line) { - GeyserImpl.getInstance().commandManager().runCommand(this, line); + // don't block the terminal! + GeyserImpl geyser = GeyserImpl.getInstance(); + geyser.getScheduledThread().execute(() -> geyser.commandRegistry().runCommand(this, line)); } @Override diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java index b82d8cc94..4cbd178af 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java @@ -28,7 +28,7 @@ package org.geysermc.geyser.platform.standalone.gui; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; @@ -271,15 +271,14 @@ public class GeyserStandaloneGUI { } /** - * Enable the command input box. + * Enables the command input box. * - * @param executor the executor for running commands off the GUI thread - * @param commandManager the command manager to delegate commands to + * @param executor the executor that commands will be run on + * @param registry the command registry containing all current commands */ - public void enableCommands(ScheduledExecutorService executor, GeyserCommandManager commandManager) { + public void enableCommands(ScheduledExecutorService executor, CommandRegistry registry) { // we don't want to block the GUI thread with the command execution - // todo: once cloud is used, an AsynchronousCommandExecutionCoordinator can be used to avoid this scheduler - commandListener.handler = cmd -> executor.schedule(() -> commandManager.runCommand(logger, cmd), 0, TimeUnit.SECONDS); + commandListener.dispatcher = cmd -> executor.execute(() -> registry.runCommand(logger, cmd)); commandInput.setEnabled(true); commandInput.requestFocusInWindow(); } @@ -344,13 +343,14 @@ public class GeyserStandaloneGUI { private class CommandListener implements ActionListener { - private Consumer handler; + private Consumer dispatcher; @Override public void actionPerformed(ActionEvent e) { - String command = commandInput.getText(); + // the headless variant of Standalone strips trailing whitespace for us - we need to manually + String command = commandInput.getText().stripTrailing(); appendConsole(command + "\n"); // show what was run in the console - handler.accept(command); // run the command + dispatcher.accept(command); // run the command commandInput.setText(""); // clear the input } } diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts index 4daad9784..93e0c9c93 100644 --- a/bootstrap/velocity/build.gradle.kts +++ b/bootstrap/velocity/build.gradle.kts @@ -3,12 +3,15 @@ dependencies { api(projects.core) compileOnlyApi(libs.velocity.api) + api(libs.cloud.velocity) } platformRelocate("com.fasterxml.jackson") platformRelocate("it.unimi.dsi.fastutil") platformRelocate("net.kyori.adventure.text.serializer.gson.legacyimpl") platformRelocate("org.yaml") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated exclude("com.google.*:*") @@ -38,8 +41,8 @@ exclude("net.kyori:adventure-nbt:*") // These dependencies are already present on the platform provided(libs.velocity.api) -application { - mainClass.set("org.geysermc.geyser.platform.velocity.GeyserVelocityMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.velocity.GeyserVelocityMain" } tasks.withType { @@ -74,4 +77,4 @@ tasks.withType { modrinth { uploadFile.set(tasks.getByPath("shadowJar")) loaders.addAll("velocity") -} \ No newline at end of file +} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java index 539bdadbf..868cdbf8e 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java @@ -26,7 +26,7 @@ package org.geysermc.geyser.platform.velocity; import com.google.inject.Inject; -import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ListenerBoundEvent; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; @@ -34,24 +34,28 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.network.ListenerType; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.CommandSourceConverter; +import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; -import org.geysermc.geyser.platform.velocity.command.GeyserVelocityCommandExecutor; +import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.velocity.VelocityCommandManager; import org.slf4j.Logger; import java.io.File; @@ -59,29 +63,28 @@ import java.io.IOException; import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Map; import java.util.UUID; @Plugin(id = "geyser", name = GeyserImpl.NAME + "-Velocity", version = GeyserImpl.VERSION, url = "https://geysermc.org", authors = "GeyserMC") public class GeyserVelocityPlugin implements GeyserBootstrap { private final ProxyServer proxyServer; - private final CommandManager commandManager; + private final PluginContainer container; private final GeyserVelocityLogger geyserLogger; - private GeyserCommandManager geyserCommandManager; private GeyserVelocityConfiguration geyserConfig; private GeyserVelocityInjector geyserInjector; private IGeyserPingPassthrough geyserPingPassthrough; + private CommandRegistry commandRegistry; private GeyserImpl geyser; @Getter private final Path configFolder = Paths.get("plugins/" + GeyserImpl.NAME + "-Velocity/"); @Inject - public GeyserVelocityPlugin(ProxyServer server, Logger logger, CommandManager manager) { - this.geyserLogger = new GeyserVelocityLogger(logger); + public GeyserVelocityPlugin(ProxyServer server, PluginContainer container, Logger logger) { this.proxyServer = server; - this.commandManager = manager; + this.container = container; + this.geyserLogger = new GeyserVelocityLogger(logger); } @Override @@ -117,8 +120,19 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } else { - this.geyserCommandManager = new GeyserCommandManager(geyser); - this.geyserCommandManager.init(); + var sourceConverter = new CommandSourceConverter<>( + CommandSource.class, + id -> proxyServer.getPlayer(id).orElse(null), + proxyServer::getConsoleCommandSource, + VelocityCommandSource::new + ); + CommandManager cloud = new VelocityCommandManager<>( + container, + proxyServer, + ExecutionCoordinator.simpleCoordinator(), + sourceConverter + ); + this.commandRegistry = new CommandRegistry(geyser, cloud, false); // applying root permission would be a breaking change because we can't register permission defaults } GeyserImpl.start(); @@ -129,22 +143,10 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer); } - // No need to re-register commands when reloading - if (GeyserImpl.getInstance().isReloading()) { - return; + // No need to re-register events + if (!GeyserImpl.getInstance().isReloading()) { + proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener()); } - - this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(geyser, geyserCommandManager.getCommands())); - for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { - Map commands = entry.getValue(); - if (commands.isEmpty()) { - continue; - } - - this.commandManager.register(entry.getKey().description().id(), new GeyserVelocityCommandExecutor(this.geyser, commands)); - } - - proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener()); } @Override @@ -175,8 +177,8 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.geyserCommandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java index 31e584612..c1c88b70d 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java @@ -28,8 +28,8 @@ package org.geysermc.geyser.platform.velocity; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.proxy.Player; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource; import org.geysermc.geyser.util.VersionCheckUtils; @@ -39,7 +39,7 @@ public final class GeyserVelocityUpdateListener { public void onPlayerJoin(PostLoginEvent event) { if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) { final Player player = event.getPlayer(); - if (player.hasPermission(Constants.UPDATE_PERMISSION)) { + if (player.hasPermission(Permissions.CHECK_UPDATE)) { VersionCheckUtils.checkForGeyserUpdate(() -> new VelocityCommandSource(player)); } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java deleted file mode 100644 index c89c35b06..000000000 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.platform.velocity.command; - -import com.velocitypowered.api.command.SimpleCommand; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.command.GeyserCommand; -import org.geysermc.geyser.command.GeyserCommandExecutor; -import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.ChatColor; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class GeyserVelocityCommandExecutor extends GeyserCommandExecutor implements SimpleCommand { - - public GeyserVelocityCommandExecutor(GeyserImpl geyser, Map commands) { - super(geyser, commands); - } - - @Override - public void execute(Invocation invocation) { - GeyserCommandSource sender = new VelocityCommandSource(invocation.source()); - GeyserSession session = getGeyserSession(sender); - - if (invocation.arguments().length > 0) { - GeyserCommand command = getCommand(invocation.arguments()[0]); - if (command != null) { - if (!invocation.source().hasPermission(getCommand(invocation.arguments()[0]).permission())) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } - if (command.isBedrockOnly() && session == null) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.locale())); - return; - } - command.execute(session, sender, invocation.arguments().length > 1 ? Arrays.copyOfRange(invocation.arguments(), 1, invocation.arguments().length) : new String[0]); - } else { - String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", sender.locale()); - sender.sendMessage(ChatColor.RED + message); - } - } else { - getCommand("help").execute(session, sender, new String[0]); - } - } - - @Override - public List suggest(Invocation invocation) { - // Velocity seems to do the splitting a bit differently. This results in the same behaviour in bungeecord/spigot. - if (invocation.arguments().length == 0 || invocation.arguments().length == 1) { - return tabComplete(new VelocityCommandSource(invocation.source())); - } - return Collections.emptyList(); - } -} diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java index 403e4cb20..2240f9988 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java @@ -31,10 +31,12 @@ import com.velocitypowered.api.proxy.Player; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.GeyserLocale; import java.util.Locale; +import java.util.UUID; public class VelocityCommandSource implements GeyserCommandSource { @@ -72,6 +74,14 @@ public class VelocityCommandSource implements GeyserCommandSource { return handle instanceof ConsoleCommandSource; } + @Override + public @Nullable UUID playerUuid() { + if (handle instanceof Player player) { + return player.getUniqueId(); + } + return null; + } + @Override public String locale() { if (handle instanceof Player) { @@ -83,6 +93,12 @@ public class VelocityCommandSource implements GeyserCommandSource { @Override public boolean hasPermission(String permission) { - return handle.hasPermission(permission); + // Handle blank permissions ourselves, as velocity only handles empty ones + return permission.isBlank() || handle.hasPermission(permission); + } + + @Override + public Object handle() { + return handle; } } diff --git a/bootstrap/viaproxy/build.gradle.kts b/bootstrap/viaproxy/build.gradle.kts index 6eadc790f..254787743 100644 --- a/bootstrap/viaproxy/build.gradle.kts +++ b/bootstrap/viaproxy/build.gradle.kts @@ -8,12 +8,14 @@ platformRelocate("net.kyori") platformRelocate("org.yaml") platformRelocate("it.unimi.dsi.fastutil") platformRelocate("org.cloudburstmc.netty") +platformRelocate("org.incendo") +platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated // These dependencies are already present on the platform provided(libs.viaproxy) -application { - mainClass.set("org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain") +tasks.withType { + manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain" } tasks.withType { diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java index bdc80335a..1eed778f2 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java @@ -34,13 +34,15 @@ import net.raphimc.viaproxy.plugins.events.ProxyStartEvent; import net.raphimc.viaproxy.plugins.events.ProxyStopEvent; import net.raphimc.viaproxy.plugins.events.ShouldVerifyOnlineModeEvent; import org.apache.logging.log4j.LogManager; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; @@ -50,7 +52,6 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; import org.geysermc.geyser.util.LoopbackUtil; -import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; @@ -66,7 +67,8 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst private final GeyserViaProxyLogger logger = new GeyserViaProxyLogger(LogManager.getLogger("Geyser")); private GeyserViaProxyConfiguration config; private GeyserImpl geyser; - private GeyserCommandManager commandManager; + private StandaloneCloudCommandManager cloud; + private CommandRegistry commandRegistry; private IGeyserPingPassthrough pingPassthrough; @Override @@ -87,7 +89,9 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst @EventHandler private void onConsoleCommand(final ConsoleCommandEvent event) { final String command = event.getCommand().startsWith("/") ? event.getCommand().substring(1) : event.getCommand(); - if (this.getGeyserCommandManager().runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs()))) { + CommandRegistry registry = this.getCommandRegistry(); + if (registry.rootCommands().contains(command)) { + registry.runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs())); event.setCancelled(true); } } @@ -128,17 +132,25 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst @Override public void onGeyserEnable() { - if (GeyserImpl.getInstance().isReloading()) { + boolean reloading = geyser.isReloading(); + if (reloading) { if (!this.loadConfig()) { return; } + } else { + // Only initialized once - documented in the Geyser-Standalone bootstrap + this.cloud = new StandaloneCloudCommandManager(geyser); + this.commandRegistry = new CommandRegistry(geyser, cloud); } - this.commandManager = new GeyserCommandManager(this.geyser); - this.commandManager.init(); - GeyserImpl.start(); + if (!reloading) { + // Event must be fired after CommandRegistry has subscribed its listener. + // Also, the subscription for the Permissions class is created when Geyser is initialized (by GeyserImpl#start) + this.cloud.fireRegisterPermissionsEvent(); + } + if (ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().newerThanOrEqualTo(LegacyProtocolVersion.b1_8tob1_8_1)) { // Only initialize the ping passthrough if the protocol version is above beta 1.7.3, as that's when the status protocol was added this.pingPassthrough = GeyserLegacyPingPassthrough.init(this.geyser); @@ -166,8 +178,8 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst } @Override - public GeyserCommandManager getGeyserCommandManager() { - return this.commandManager; + public CommandRegistry getCommandRegistry() { + return this.commandRegistry; } @Override @@ -185,7 +197,7 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst return new GeyserViaProxyDumpInfo(); } - @NotNull + @NonNull @Override public String getServerBindAddress() { if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress socketAddress) { @@ -209,6 +221,7 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst return false; } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { try { final File configFile = FileUtils.fileOrCopiedFromResource(new File(ROOT_FOLDER, "config.yml"), "config.yml", s -> s.replaceAll("generateduuid", UUID.randomUUID().toString()), this); diff --git a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts index 7952bcf14..20d14c443 100644 --- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts @@ -37,6 +37,10 @@ provided("io.netty", "netty-resolver-dns") provided("io.netty", "netty-resolver-dns-native-macos") provided("org.ow2.asm", "asm") +// cloud-fabric/cloud-neoforge jij's all cloud depends already +provided("org.incendo", ".*") +provided("io.leangen.geantyref", "geantyref") + architectury { minecraft = libs.minecraft.get().version as String } @@ -120,4 +124,4 @@ repositories { maven("https://oss.sonatype.org/content/repositories/snapshots/") maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") maven("https://maven.neoforged.net/releases") -} \ No newline at end of file +} diff --git a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts index 81d224906..410e67404 100644 --- a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts @@ -1,4 +1,3 @@ plugins { - application id("geyser.publish-conventions") } \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 3b5cc3df9..acd6c5147 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -51,6 +51,9 @@ dependencies { // Adventure text serialization api(libs.bundles.adventure) + // command library + api(libs.cloud.core) + api(libs.erosion.common) { isTransitive = false } diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java index 534cb30ad..7f00075d8 100644 --- a/core/src/main/java/org/geysermc/geyser/Constants.java +++ b/core/src/main/java/org/geysermc/geyser/Constants.java @@ -35,9 +35,7 @@ public final class Constants { public static final String NEWS_PROJECT_NAME = "geyser"; public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://geysermc.org/download#floodgate"; - public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download"; - public static final String UPDATE_PERMISSION = "geyser.update"; @Deprecated static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json"; diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java index a9414d9d0..3063fa4f6 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java @@ -27,7 +27,7 @@ package org.geysermc.geyser; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.GeyserWorldManager; @@ -82,11 +82,11 @@ public interface GeyserBootstrap { GeyserLogger getGeyserLogger(); /** - * Returns the current CommandManager + * Returns the current CommandRegistry * - * @return The current CommandManager + * @return The current CommandRegistry */ - GeyserCommandManager getGeyserCommandManager(); + CommandRegistry getCommandRegistry(); /** * Returns the current PingPassthrough manager diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 8f88f5b6a..464ebda96 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -68,7 +68,7 @@ import org.geysermc.geyser.api.network.BedrockListener; import org.geysermc.geyser.api.network.RemoteServer; import org.geysermc.geyser.api.util.MinecraftVersion; import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.erosion.UnixSocketClientListener; @@ -128,7 +128,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; @Getter -public class GeyserImpl implements GeyserApi { +public class GeyserImpl implements GeyserApi, EventRegistrar { public static final ObjectMapper JSON_MAPPER = new ObjectMapper() .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) @@ -231,9 +231,7 @@ public class GeyserImpl implements GeyserApi { logger.info(GeyserLocale.getLocaleStringLog("geyser.core.load", NAME, VERSION)); logger.info(""); if (IS_DEV) { - // TODO cloud use language string - //logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc")); - logger.info("You are running a development build of Geyser! Please report any bugs you find on our Discord server: %s".formatted("https://discord.gg/geysermc")); + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc")); logger.info(""); } logger.info("******************************************"); @@ -266,6 +264,9 @@ public class GeyserImpl implements GeyserApi { CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks); }); + // Register our general permissions when possible + eventBus.subscribe(this, GeyserRegisterPermissionsEvent.class, Permissions::register); + startInstance(); GeyserConfiguration config = bootstrap.getGeyserConfig(); @@ -730,7 +731,6 @@ public class GeyserImpl implements GeyserApi { if (isEnabled) { this.disable(); } - this.commandManager().getCommands().clear(); // Disable extensions, fire the shutdown event this.eventBus.fire(new GeyserShutdownEvent(this.extensionManager, this.eventBus)); @@ -768,9 +768,12 @@ public class GeyserImpl implements GeyserApi { return this.extensionManager; } + /** + * @return the current CommandRegistry in use. The instance may change over the lifecycle of the Geyser runtime. + */ @NonNull - public GeyserCommandManager commandManager() { - return this.bootstrap.getGeyserCommandManager(); + public CommandRegistry commandRegistry() { + return this.bootstrap.getCommandRegistry(); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index aa79e3630..f408de29c 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.command.GeyserCommandSource; +import java.util.UUID; public interface GeyserLogger extends GeyserCommandSource { @@ -129,6 +130,11 @@ public interface GeyserLogger extends GeyserCommandSource { return true; } + @Override + default @Nullable UUID playerUuid() { + return null; + } + @Override default boolean hasPermission(String permission) { return true; diff --git a/core/src/main/java/org/geysermc/geyser/Permissions.java b/core/src/main/java/org/geysermc/geyser/Permissions.java new file mode 100644 index 000000000..b65a5af7a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/Permissions.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 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.geyser; + +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; + +import java.util.HashMap; +import java.util.Map; + +/** + * Permissions related to Geyser + */ +public final class Permissions { + private static final Map PERMISSIONS = new HashMap<>(); + + public static final String CHECK_UPDATE = register("geyser.update"); + public static final String SERVER_SETTINGS = register("geyser.settings.server"); + public static final String SETTINGS_GAMERULES = register("geyser.settings.gamerules"); + + private Permissions() { + //no + } + + private static String register(String permission) { + return register(permission, TriState.NOT_SET); + } + + @SuppressWarnings("SameParameterValue") + private static String register(String permission, TriState permissionDefault) { + PERMISSIONS.put(permission, permissionDefault); + return permission; + } + + public static void register(GeyserRegisterPermissionsEvent event) { + for (Map.Entry permission : PERMISSIONS.entrySet()) { + event.register(permission.getKey(), permission.getValue()); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java new file mode 100644 index 000000000..f07092afd --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2019-2022 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.geyser.command; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.event.EventRegistrar; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand; +import org.geysermc.geyser.command.defaults.AdvancementsCommand; +import org.geysermc.geyser.command.defaults.ConnectionTestCommand; +import org.geysermc.geyser.command.defaults.DumpCommand; +import org.geysermc.geyser.command.defaults.ExtensionsCommand; +import org.geysermc.geyser.command.defaults.HelpCommand; +import org.geysermc.geyser.command.defaults.ListCommand; +import org.geysermc.geyser.command.defaults.OffhandCommand; +import org.geysermc.geyser.command.defaults.ReloadCommand; +import org.geysermc.geyser.command.defaults.SettingsCommand; +import org.geysermc.geyser.command.defaults.StatisticsCommand; +import org.geysermc.geyser.command.defaults.StopCommand; +import org.geysermc.geyser.command.defaults.VersionCommand; +import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl; +import org.geysermc.geyser.extension.command.GeyserExtensionCommand; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.Command.Builder; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.geysermc.geyser.command.GeyserCommand.DEFAULT_ROOT_COMMAND; + +/** + * Registers all built-in and extension commands to the given Cloud CommandManager. + *

    + * Fires {@link GeyserDefineCommandsEvent} upon construction. + *

    + * Subscribes to {@link GeyserRegisterPermissionsEvent} upon construction. + * A new instance of this class (that registers the same permissions) shouldn't be created until the previous + * instance is unsubscribed from the event. + */ +public class CommandRegistry implements EventRegistrar { + + private static final String GEYSER_ROOT_PERMISSION = "geyser.command"; + + protected final GeyserImpl geyser; + private final CommandManager cloud; + private final boolean applyRootPermission; + + /** + * Map of Geyser subcommands to their Commands + */ + private final Map commands = new Object2ObjectOpenHashMap<>(13); + + /** + * Map of Extensions to maps of their subcommands + */ + private final Map> extensionCommands = new Object2ObjectOpenHashMap<>(0); + + /** + * Map of root commands (that are for extensions) to Extensions + */ + private final Map extensionRootCommands = new Object2ObjectOpenHashMap<>(0); + + /** + * Map containing only permissions that have been registered with a default value + */ + protected final Map permissionDefaults = new Object2ObjectOpenHashMap<>(13); + + /** + * Creates a new CommandRegistry. Does apply a root permission. If undesired, use the other constructor. + */ + public CommandRegistry(GeyserImpl geyser, CommandManager cloud) { + this(geyser, cloud, true); + } + + /** + * Creates a new CommandRegistry + * + * @param geyser the Geyser instance + * @param cloud the cloud command manager to register commands to + * @param applyRootPermission true if this registry should apply a permission to Geyser and Extension root commands. + * This currently exists because we want to retain the root command permission for Spigot, + * but don't want to add it yet to platforms like Velocity where we cannot natively + * specify a permission default. Doing so will break setups as players would suddenly not + * have the required permission to execute any Geyser commands. + */ + public CommandRegistry(GeyserImpl geyser, CommandManager cloud, boolean applyRootPermission) { + this.geyser = geyser; + this.cloud = cloud; + this.applyRootPermission = applyRootPermission; + + // register our custom exception handlers + ExceptionHandlers.register(cloud); + + // begin command registration + HelpCommand help = new HelpCommand(DEFAULT_ROOT_COMMAND, "help", "geyser.commands.help.desc", "geyser.command.help", this.commands); + registerBuiltInCommand(help); + buildRootCommand(GEYSER_ROOT_PERMISSION, help); // build root and delegate to help + + registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list")); + registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload")); + registerBuiltInCommand(new OffhandCommand("offhand", "geyser.commands.offhand.desc", "geyser.command.offhand")); + registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump")); + registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version")); + registerBuiltInCommand(new SettingsCommand("settings", "geyser.commands.settings.desc", "geyser.command.settings")); + registerBuiltInCommand(new StatisticsCommand("statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); + registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); + registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); + registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); + if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { + registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); + } + + if (!this.geyser.extensionManager().extensions().isEmpty()) { + registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions")); + } + + GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) { + + @Override + public void register(@NonNull Command command) { + if (!(command instanceof GeyserExtensionCommand extensionCommand)) { + throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?"); + } + + registerExtensionCommand(extensionCommand.extension(), extensionCommand); + } + }; + this.geyser.eventBus().fire(defineCommandsEvent); + + // Stuff that needs to be done on a per-extension basis + for (Map.Entry> entry : this.extensionCommands.entrySet()) { + Extension extension = entry.getKey(); + + // Register this extension's root command + extensionRootCommands.put(extension.rootCommand(), extension); + + // Register help commands for all extensions with commands + String id = extension.description().id(); + HelpCommand extensionHelp = new HelpCommand( + extension.rootCommand(), + "help", + "geyser.commands.exthelp.desc", + "geyser.command.exthelp." + id, + entry.getValue()); // commands it provides help for + + registerExtensionCommand(extension, extensionHelp); + buildRootCommand("geyser.extension." + id + ".command", extensionHelp); + } + + // Wait for the right moment (depends on the platform) to register permissions. + geyser.eventBus().subscribe(this, GeyserRegisterPermissionsEvent.class, this::onRegisterPermissions); + } + + /** + * @return an immutable view of the root commands registered to this command registry + */ + @NonNull + public Collection rootCommands() { + return cloud.rootCommands(); + } + + /** + * For internal Geyser commands + */ + private void registerBuiltInCommand(GeyserCommand command) { + register(command, this.commands); + } + + private void registerExtensionCommand(@NonNull Extension extension, @NonNull GeyserCommand command) { + register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>())); + } + + protected void register(GeyserCommand command, Map commands) { + String root = command.rootCommand(); + String name = command.name(); + if (commands.containsKey(name)) { + throw new IllegalArgumentException("Command with root=%s, name=%s already registered".formatted(root, name)); + } + + command.register(cloud); + commands.put(name, command); + geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", root + " " + name)); + + for (String alias : command.aliases()) { + commands.put(alias, command); + } + + String permission = command.permission(); + TriState defaultValue = command.permissionDefault(); + if (!permission.isBlank() && defaultValue != null) { + + TriState existingDefault = permissionDefaults.get(permission); + // Extensions might be using the same permission for two different commands + if (existingDefault != null && existingDefault != defaultValue) { + geyser.getLogger().debug("Overriding permission default %s:%s with %s".formatted(permission, existingDefault, defaultValue)); + } + + permissionDefaults.put(permission, defaultValue); + } + } + + /** + * Registers a root command to cloud that delegates to the given help command. + * The name of this root command is the root of the given help command. + * + * @param permission the permission of the root command. currently, it may or may not be + * applied depending on the platform. see below. + * @param help the help command to delegate to + */ + private void buildRootCommand(String permission, HelpCommand help) { + Builder builder = cloud.commandBuilder(help.rootCommand()); + + if (applyRootPermission) { + builder = builder.permission(permission); + permissionDefaults.put(permission, TriState.TRUE); + } + + cloud.command(builder.handler(context -> { + GeyserCommandSource source = context.sender(); + if (!source.hasPermission(help.permission())) { + // delegate if possible - otherwise we have nothing else to offer the user. + source.sendLocaleString(ExceptionHandlers.PERMISSION_FAIL_LANG_KEY); + return; + } + help.execute(source); + })); + } + + protected void onRegisterPermissions(GeyserRegisterPermissionsEvent event) { + for (Map.Entry permission : permissionDefaults.entrySet()) { + event.register(permission.getKey(), permission.getValue()); + } + } + + public boolean hasPermission(GeyserCommandSource source, String permission) { + // Handle blank permissions ourselves, as cloud only handles empty ones + return permission.isBlank() || cloud.hasPermission(source, permission); + } + + /** + * Returns the description of the given command + * + * @param command the root command node + * @param locale the ideal locale that the description should be in + * @return a description if found, otherwise an empty string. The locale is not guaranteed. + */ + @NonNull + public String description(@NonNull String command, @NonNull String locale) { + if (command.equals(DEFAULT_ROOT_COMMAND)) { + return GeyserLocale.getPlayerLocaleString("geyser.command.root.geyser", locale); + } + + Extension extension = extensionRootCommands.get(command); + if (extension != null) { + return GeyserLocale.getPlayerLocaleString("geyser.command.root.extension", locale, extension.name()); + } + return ""; + } + + /** + * Dispatches a command into cloud and handles any thrown exceptions. + * This method may or may not be blocking, depending on the {@link ExecutionCoordinator} in use by cloud. + */ + public void runCommand(@NonNull GeyserCommandSource source, @NonNull String command) { + cloud.commandExecutor().executeCommand(source, command); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java b/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java new file mode 100644 index 000000000..1fa5871e0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019-2023 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.geyser.command; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.SenderMapper; + +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Converts {@link GeyserCommandSource}s to the server's command sender type (and back) in a lenient manner. + * + * @param senderType class of the server command sender type + * @param playerLookup function for looking up a player command sender by UUID + * @param consoleProvider supplier of the console command sender + * @param commandSourceLookup supplier of the platform implementation of the {@link GeyserCommandSource} + * @param server command sender type + */ +public record CommandSourceConverter(Class senderType, + Function playerLookup, + Supplier consoleProvider, + Function commandSourceLookup +) implements SenderMapper { + + /** + * Creates a new CommandSourceConverter for a server platform + * in which the player type is not a command sender type, and must be mapped. + * + * @param senderType class of the command sender type + * @param playerLookup function for looking up a player by UUID + * @param senderLookup function for converting a player to a command sender + * @param consoleProvider supplier of the console command sender + * @param commandSourceLookup supplier of the platform implementation of {@link GeyserCommandSource} + * @return a new CommandSourceConverter + * @param

    server player type + * @param server command sender type + */ + public static CommandSourceConverter layered(Class senderType, + Function playerLookup, + Function senderLookup, + Supplier consoleProvider, + Function commandSourceLookup) { + Function lookup = uuid -> { + P player = playerLookup.apply(uuid); + if (player == null) { + return null; + } + return senderLookup.apply(player); + }; + return new CommandSourceConverter<>(senderType, lookup, consoleProvider, commandSourceLookup); + } + + @Override + public @NonNull GeyserCommandSource map(@NonNull S base) { + return commandSourceLookup.apply(base); + } + + @Override + public @NonNull S reverse(GeyserCommandSource source) throws IllegalArgumentException { + Object handle = source.handle(); + if (senderType.isInstance(handle)) { + return senderType.cast(handle); // one of the server platform implementations + } + + if (source.isConsole()) { + return consoleProvider.get(); // one of the loggers + } + + if (!(source instanceof GeyserSession)) { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); + if (logger.isDebug()) { + logger.debug("Falling back to UUID for command sender lookup for a command source that is not a GeyserSession: " + source); + Thread.dumpStack(); + } + } + + // Ideally lookup should only be necessary for GeyserSession + UUID uuid = source.playerUuid(); + if (uuid != null) { + return playerLookup.apply(uuid); + } + + throw new IllegalArgumentException("failed to find sender for name=%s, uuid=%s".formatted(source.name(), source.playerUuid())); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java new file mode 100644 index 000000000..45657a596 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2019-2024 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.geyser.command; + +import io.leangen.geantyref.GenericTypeReflector; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.text.ChatColor; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.text.MinecraftLocale; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.exception.ArgumentParseException; +import org.incendo.cloud.exception.CommandExecutionException; +import org.incendo.cloud.exception.InvalidCommandSenderException; +import org.incendo.cloud.exception.InvalidSyntaxException; +import org.incendo.cloud.exception.NoPermissionException; +import org.incendo.cloud.exception.NoSuchCommandException; +import org.incendo.cloud.exception.handling.ExceptionController; + +import java.lang.reflect.Type; +import java.util.function.BiConsumer; + +/** + * Geyser's exception handlers for command execution with Cloud. + * Overrides Cloud's defaults so that messages can be customized to our liking: localization, etc. + */ +final class ExceptionHandlers { + + final static String PERMISSION_FAIL_LANG_KEY = "geyser.command.permission_fail"; + + private final ExceptionController controller; + + private ExceptionHandlers(ExceptionController controller) { + this.controller = controller; + } + + /** + * Clears the existing handlers that are registered to the given command manager, and repopulates them. + * + * @param manager the manager whose exception handlers will be modified + */ + static void register(CommandManager manager) { + new ExceptionHandlers(manager.exceptionController()).register(); + } + + private void register() { + // Yeet the default exception handlers that cloud provides so that we can perform localization. + controller.clearHandlers(); + + registerExceptionHandler(InvalidSyntaxException.class, + (src, e) -> src.sendLocaleString("geyser.command.invalid_syntax", e.correctSyntax())); + + registerExceptionHandler(InvalidCommandSenderException.class, (src, e) -> { + // We currently don't use cloud sender type requirements anywhere. + // This can be implemented better in the future if necessary. + Type type = e.requiredSenderTypes().iterator().next(); // just grab the first + String typeString = GenericTypeReflector.getTypeName(type); + src.sendLocaleString("geyser.command.invalid_sender", e.commandSender().getClass().getSimpleName(), typeString); + }); + + registerExceptionHandler(NoPermissionException.class, ExceptionHandlers::handleNoPermission); + + registerExceptionHandler(NoSuchCommandException.class, + (src, e) -> src.sendLocaleString("geyser.command.not_found")); + + registerExceptionHandler(ArgumentParseException.class, + (src, e) -> src.sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage())); + + registerExceptionHandler(CommandExecutionException.class, + (src, e) -> handleUnexpectedThrowable(src, e.getCause())); + + registerExceptionHandler(Throwable.class, + (src, e) -> handleUnexpectedThrowable(src, e.getCause())); + } + + private void registerExceptionHandler(Class type, BiConsumer handler) { + controller.registerHandler(type, context -> handler.accept(context.context().sender(), context.exception())); + } + + private static void handleNoPermission(GeyserCommandSource source, NoPermissionException exception) { + // custom handling if the source can't use the command because of additional requirements + if (exception.permissionResult() instanceof GeyserPermission.Result result) { + if (result.meta() == GeyserPermission.Result.Meta.NOT_BEDROCK) { + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.bedrock_only", source.locale())); + return; + } + if (result.meta() == GeyserPermission.Result.Meta.NOT_PLAYER) { + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.player_only", source.locale())); + return; + } + } else { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); + if (logger.isDebug()) { + logger.debug("Expected a GeyserPermission.Result for %s but instead got %s from %s".formatted(exception.currentChain(), exception.permissionResult(), exception.missingPermission())); + } + } + + // Result.NO_PERMISSION or generic permission failure + source.sendLocaleString(PERMISSION_FAIL_LANG_KEY); + } + + private static void handleUnexpectedThrowable(GeyserCommandSource source, Throwable throwable) { + source.sendMessage(MinecraftLocale.getLocaleString("command.failed", source.locale())); // java edition translation key + GeyserImpl.getInstance().getLogger().error("Exception while executing command handler", throwable); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java index 47d57e73f..3cc05ca0c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java @@ -25,65 +25,187 @@ package org.geysermc.geyser.command; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Accessors; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.CommandDescription; +import org.jetbrains.annotations.Contract; import java.util.Collections; import java.util.List; -@Accessors(fluent = true) -@Getter -@RequiredArgsConstructor -public abstract class GeyserCommand implements Command { +public abstract class GeyserCommand implements org.geysermc.geyser.api.command.Command { + public static final String DEFAULT_ROOT_COMMAND = "geyser"; + + /** + * The second literal of the command. Note: the first literal is {@link #rootCommand()}. + */ + @NonNull + private final String name; - protected final String name; /** * The description of the command - will attempt to be translated. */ - protected final String description; - protected final String permission; - - private List aliases = Collections.emptyList(); - - public abstract void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args); + @NonNull + private final String description; /** - * If false, hides the command from being shown on the Geyser Standalone GUI. - * - * @return true if the command can be run on the server console - */ - @Override - public boolean isExecutableOnConsole() { - return true; - } - - /** - * Used in the GUI to know what subcommands can be run - * - * @return a list of all possible subcommands, or empty if none. + * The permission node required to run the command, or blank if not required. */ @NonNull - @Override - public List subCommands() { - return Collections.emptyList(); + private final String permission; + + /** + * The default value of the permission node. + * A null value indicates that the permission node should not be registered whatsoever. + * See {@link GeyserRegisterPermissionsEvent#register(String, TriState)} for TriState meanings. + */ + @Nullable + private final TriState permissionDefault; + + /** + * True if this command can be executed by players + */ + private final boolean playerOnly; + + /** + * True if this command can only be run by bedrock players + */ + private final boolean bedrockOnly; + + /** + * The aliases of the command {@link #name}. This should not be modified after construction. + */ + protected List aliases = Collections.emptyList(); + + public GeyserCommand(@NonNull String name, @NonNull String description, + @NonNull String permission, @Nullable TriState permissionDefault, + boolean playerOnly, boolean bedrockOnly) { + + if (name.isBlank()) { + throw new IllegalArgumentException("Command cannot be null or blank!"); + } + if (permission.isBlank()) { + // Cloud treats empty permissions as available to everyone, but not blank permissions. + // When registering commands, we must convert ALL whitespace permissions into empty ones, + // because we cannot override permission checks that Cloud itself performs + permission = ""; + permissionDefault = null; + } + + this.name = name; + this.description = description; + this.permission = permission; + this.permissionDefault = permissionDefault; + + if (bedrockOnly && !playerOnly) { + throw new IllegalArgumentException("Command cannot be bedrockOnly if it is not playerOnly"); + } + + this.playerOnly = playerOnly; + this.bedrockOnly = bedrockOnly; } - public void setAliases(List aliases) { - this.aliases = aliases; + public GeyserCommand(@NonNull String name, @NonNull String description, @NonNull String permission, @Nullable TriState permissionDefault) { + this(name, description, permission, permissionDefault, false, false); + } + + @NonNull + @Override + public final String name() { + return name; + } + + @NonNull + @Override + public final String description() { + return description; + } + + @NonNull + @Override + public final String permission() { + return permission; + } + + @Nullable + public final TriState permissionDefault() { + return permissionDefault; + } + + @Override + public final boolean isPlayerOnly() { + return playerOnly; + } + + @Override + public final boolean isBedrockOnly() { + return bedrockOnly; + } + + @NonNull + @Override + public final List aliases() { + return Collections.unmodifiableList(aliases); } /** - * Used for permission defaults on server implementations. - * - * @return if this command is designated to be used only by server operators. + * @return the first (literal) argument of this command, which comes before {@link #name()}. */ - @Override - public boolean isSuggestedOpOnly() { - return false; + public String rootCommand() { + return DEFAULT_ROOT_COMMAND; } -} \ No newline at end of file + + /** + * Returns a {@link org.incendo.cloud.permission.Permission} that handles {@link #isBedrockOnly()}, {@link #isPlayerOnly()}, and {@link #permission()}. + * + * @param manager the manager to be used for permission node checking + * @return a permission that will properly restrict usage of this command + */ + public final GeyserPermission commandPermission(CommandManager manager) { + return new GeyserPermission(bedrockOnly, playerOnly, permission, manager); + } + + /** + * Creates a new command builder with {@link #rootCommand()}, {@link #name()}, and {@link #aliases()} built on it. + * A permission predicate that takes into account {@link #permission()}, {@link #isBedrockOnly()}, and {@link #isPlayerOnly()} + * is applied. The Applicable from {@link #meta()} is also applied to the builder. + */ + @Contract(value = "_ -> new", pure = true) + public final Command.Builder baseBuilder(CommandManager manager) { + return manager.commandBuilder(rootCommand()) + .literal(name, aliases.toArray(new String[0])) + .permission(commandPermission(manager)) + .apply(meta()); + } + + /** + * @return an Applicable that applies this command's description + */ + protected Command.Builder.Applicable meta() { + return builder -> builder + .commandDescription(CommandDescription.commandDescription(GeyserLocale.getLocaleStringLog(description))); // used in cloud-bukkit impl + } + + /** + * Registers this command to the given command manager. + * This method may be overridden to register more than one command. + *

    + * The default implementation is that {@link #baseBuilder(CommandManager)} with {@link #execute(CommandContext)} + * applied as the handler is registered to the manager. + */ + public void register(CommandManager manager) { + manager.command(baseBuilder(manager).handler(this::execute)); + } + + /** + * Executes this command + * @param context the context with which this command should be executed + */ + public abstract void execute(CommandContext context); +} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java deleted file mode 100644 index 37d2ef4fb..000000000 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.command; - -import lombok.AllArgsConstructor; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.session.GeyserSession; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * Represents helper functions for listening to {@code /geyser} or {@code /geyserext} commands. - */ -@AllArgsConstructor -public class GeyserCommandExecutor { - - protected final GeyserImpl geyser; - private final Map commands; - - public GeyserCommand getCommand(String label) { - return (GeyserCommand) commands.get(label); - } - - @Nullable - public GeyserSession getGeyserSession(GeyserCommandSource sender) { - if (sender.isConsole()) { - return null; - } - - for (GeyserSession session : geyser.getSessionManager().getSessions().values()) { - if (sender.name().equals(session.getPlayerEntity().getUsername())) { - return session; - } - } - return null; - } - - /** - * Determine which subcommands to suggest in the tab complete for the main /geyser command by a given command sender. - * - * @param sender The command sender to receive the tab complete suggestions. - * If the command sender is a bedrock player, an empty list will be returned as bedrock players do not get command argument suggestions. - * If the command sender is not a bedrock player, bedrock commands will not be shown. - * If the command sender does not have the permission for a given command, the command will not be shown. - * @return A list of command names to include in the tab complete - */ - public List tabComplete(GeyserCommandSource sender) { - if (getGeyserSession(sender) != null) { - // Bedrock doesn't get tab completions or argument suggestions - return Collections.emptyList(); - } - - List availableCommands = new ArrayList<>(); - - // Only show commands they have permission to use - for (Map.Entry entry : commands.entrySet()) { - Command geyserCommand = entry.getValue(); - if (sender.hasPermission(geyserCommand.permission())) { - if (geyserCommand.isBedrockOnly()) { - // Don't show commands the JE player can't run - continue; - } - - availableCommands.add(entry.getKey()); - } - } - - return availableCommands; - } -} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java deleted file mode 100644 index 72ed22381..000000000 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (c) 2019-2022 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.geyser.command; - -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.api.command.CommandExecutor; -import org.geysermc.geyser.api.command.CommandSource; -import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent; -import org.geysermc.geyser.api.extension.Extension; -import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand; -import org.geysermc.geyser.command.defaults.AdvancementsCommand; -import org.geysermc.geyser.command.defaults.ConnectionTestCommand; -import org.geysermc.geyser.command.defaults.DumpCommand; -import org.geysermc.geyser.command.defaults.ExtensionsCommand; -import org.geysermc.geyser.command.defaults.HelpCommand; -import org.geysermc.geyser.command.defaults.ListCommand; -import org.geysermc.geyser.command.defaults.OffhandCommand; -import org.geysermc.geyser.command.defaults.ReloadCommand; -import org.geysermc.geyser.command.defaults.SettingsCommand; -import org.geysermc.geyser.command.defaults.StatisticsCommand; -import org.geysermc.geyser.command.defaults.StopCommand; -import org.geysermc.geyser.command.defaults.VersionCommand; -import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl; -import org.geysermc.geyser.extension.command.GeyserExtensionCommand; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -@RequiredArgsConstructor -public class GeyserCommandManager { - - @Getter - private final Map commands = new Object2ObjectOpenHashMap<>(12); - private final Map> extensionCommands = new Object2ObjectOpenHashMap<>(0); - - private final GeyserImpl geyser; - - public void init() { - registerBuiltInCommand(new HelpCommand(geyser, "help", "geyser.commands.help.desc", "geyser.command.help", "geyser", this.commands)); - registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list")); - registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload")); - registerBuiltInCommand(new OffhandCommand(geyser, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand")); - registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump")); - registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version")); - registerBuiltInCommand(new SettingsCommand(geyser, "settings", "geyser.commands.settings.desc", "geyser.command.settings")); - registerBuiltInCommand(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); - registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); - registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); - registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); - if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { - registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); - } - - if (!this.geyser.extensionManager().extensions().isEmpty()) { - registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions")); - } - - GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) { - - @Override - public void register(@NonNull Command command) { - if (!(command instanceof GeyserExtensionCommand extensionCommand)) { - throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?"); - } - - registerExtensionCommand(extensionCommand.extension(), extensionCommand); - } - }; - - this.geyser.eventBus().fire(defineCommandsEvent); - - // Register help commands for all extensions with commands - for (Map.Entry> entry : this.extensionCommands.entrySet()) { - String id = entry.getKey().description().id(); - registerExtensionCommand(entry.getKey(), new HelpCommand(this.geyser, "help", "geyser.commands.exthelp.desc", "geyser.command.exthelp." + id, id, entry.getValue())); - } - } - - /** - * For internal Geyser commands - */ - public void registerBuiltInCommand(GeyserCommand command) { - register(command, this.commands); - } - - public void registerExtensionCommand(@NonNull Extension extension, @NonNull Command command) { - register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>())); - } - - private void register(Command command, Map commands) { - commands.put(command.name(), command); - geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", command.name())); - - if (command.aliases().isEmpty()) { - return; - } - - for (String alias : command.aliases()) { - commands.put(alias, command); - } - } - - @NonNull - public Map commands() { - return Collections.unmodifiableMap(this.commands); - } - - @NonNull - public Map> extensionCommands() { - return Collections.unmodifiableMap(this.extensionCommands); - } - - public boolean runCommand(GeyserCommandSource sender, String command) { - Extension extension = null; - for (Extension loopedExtension : this.extensionCommands.keySet()) { - if (command.startsWith(loopedExtension.description().id() + " ")) { - extension = loopedExtension; - break; - } - } - - if (!command.startsWith("geyser ") && extension == null) { - return false; - } - - command = command.trim().replace(extension != null ? extension.description().id() + " " : "geyser ", ""); - String label; - String[] args; - - if (!command.contains(" ")) { - label = command.toLowerCase(Locale.ROOT); - args = new String[0]; - } else { - label = command.substring(0, command.indexOf(" ")).toLowerCase(Locale.ROOT); - String argLine = command.substring(command.indexOf(" ") + 1); - args = argLine.contains(" ") ? argLine.split(" ") : new String[] { argLine }; - } - - Command cmd = (extension != null ? this.extensionCommands.getOrDefault(extension, Collections.emptyMap()) : this.commands).get(label); - if (cmd == null) { - sender.sendMessage(GeyserLocale.getLocaleStringLog("geyser.commands.invalid")); - return false; - } - - if (cmd instanceof GeyserCommand) { - if (sender instanceof GeyserSession) { - ((GeyserCommand) cmd).execute((GeyserSession) sender, sender, args); - } else { - if (!cmd.isBedrockOnly()) { - ((GeyserCommand) cmd).execute(null, sender, args); - } else { - geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.command.bedrock_only")); - } - } - } - - return true; - } - - /** - * Returns the description of the given command - * - * @param command Command to get the description for - * @return Command description - */ - public String description(String command) { - return ""; - } - - @RequiredArgsConstructor - public static class CommandBuilder implements Command.Builder { - private final Extension extension; - private Class sourceType; - private String name; - private String description = ""; - private String permission = ""; - private List aliases; - private boolean suggestedOpOnly = false; - private boolean executableOnConsole = true; - private List subCommands; - private boolean bedrockOnly; - private CommandExecutor executor; - - @Override - public Command.Builder source(@NonNull Class sourceType) { - this.sourceType = sourceType; - return this; - } - - public CommandBuilder name(@NonNull String name) { - this.name = name; - return this; - } - - public CommandBuilder description(@NonNull String description) { - this.description = description; - return this; - } - - public CommandBuilder permission(@NonNull String permission) { - this.permission = permission; - return this; - } - - public CommandBuilder aliases(@NonNull List aliases) { - this.aliases = aliases; - return this; - } - - @Override - public Command.Builder suggestedOpOnly(boolean suggestedOpOnly) { - this.suggestedOpOnly = suggestedOpOnly; - return this; - } - - public CommandBuilder executableOnConsole(boolean executableOnConsole) { - this.executableOnConsole = executableOnConsole; - return this; - } - - public CommandBuilder subCommands(@NonNull List subCommands) { - this.subCommands = subCommands; - return this; - } - - public CommandBuilder bedrockOnly(boolean bedrockOnly) { - this.bedrockOnly = bedrockOnly; - return this; - } - - public CommandBuilder executor(@NonNull CommandExecutor executor) { - this.executor = executor; - return this; - } - - @NonNull - public GeyserExtensionCommand build() { - if (this.name == null || this.name.isBlank()) { - throw new IllegalArgumentException("Command cannot be null or blank!"); - } - - if (this.sourceType == null) { - throw new IllegalArgumentException("Source type was not defined for command " + this.name + " in extension " + this.extension.name()); - } - - return new GeyserExtensionCommand(this.extension, this.name, this.description, this.permission) { - - @SuppressWarnings("unchecked") - @Override - public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) { - Class sourceType = CommandBuilder.this.sourceType; - CommandExecutor executor = CommandBuilder.this.executor; - if (sourceType.isInstance(session)) { - executor.execute((T) session, this, args); - return; - } - - if (sourceType.isInstance(sender)) { - executor.execute((T) sender, this, args); - return; - } - - GeyserImpl.getInstance().getLogger().debug("Ignoring command " + this.name + " due to no suitable sender."); - } - - @NonNull - @Override - public List aliases() { - return CommandBuilder.this.aliases == null ? Collections.emptyList() : CommandBuilder.this.aliases; - } - - @Override - public boolean isSuggestedOpOnly() { - return CommandBuilder.this.suggestedOpOnly; - } - - @NonNull - @Override - public List subCommands() { - return CommandBuilder.this.subCommands == null ? Collections.emptyList() : CommandBuilder.this.subCommands; - } - - @Override - public boolean isBedrockOnly() { - return CommandBuilder.this.bedrockOnly; - } - - @Override - public boolean isExecutableOnConsole() { - return CommandBuilder.this.executableOnConsole; - } - }; - } - } -} diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java index 88d148b11..c14767496 100644 --- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java @@ -25,11 +25,16 @@ package org.geysermc.geyser.command; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.command.CommandSource; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import java.util.UUID; + /** * Implemented on top of any class that can send a command. * For example, it wraps around Spigot's CommandSender class. @@ -46,4 +51,29 @@ public interface GeyserCommandSource extends CommandSource { default void sendMessage(Component message) { sendMessage(LegacyComponentSerializer.legacySection().serialize(message)); } + + default void sendLocaleString(String key, Object... values) { + sendMessage(GeyserLocale.getPlayerLocaleString(key, locale(), values)); + } + + default void sendLocaleString(String key) { + sendMessage(GeyserLocale.getPlayerLocaleString(key, locale())); + } + + @Override + default @Nullable GeyserSession connection() { + UUID uuid = playerUuid(); + if (uuid == null) { + return null; + } + return GeyserImpl.getInstance().connectionByUuid(uuid); + } + + /** + * @return the underlying platform handle that this source represents. + * If such handle doesn't exist, this itself is returned. + */ + default Object handle() { + return this; + } } diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java b/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java new file mode 100644 index 000000000..1ee677e97 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019-2023 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.geyser.command; + +import lombok.AllArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.key.CloudKey; +import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.permission.PermissionResult; +import org.incendo.cloud.permission.PredicatePermission; + +import static org.geysermc.geyser.command.GeyserPermission.Result.Meta; + +@AllArgsConstructor +public class GeyserPermission implements PredicatePermission { + + /** + * True if this permission requires the command source to be a bedrock player + */ + private final boolean bedrockOnly; + + /** + * True if this permission requires the command source to be any player + */ + private final boolean playerOnly; + + /** + * The permission node that the command source must have + */ + private final String permission; + + /** + * The command manager to delegate permission checks to + */ + private final CommandManager manager; + + @Override + public @NonNull Result testPermission(@NonNull GeyserCommandSource source) { + if (bedrockOnly) { + if (source.connection() == null) { + return new Result(Meta.NOT_BEDROCK); + } + // connection is present -> it is a player -> playerOnly is irrelevant + } else if (playerOnly) { + if (source.isConsole()) { + return new Result(Meta.NOT_PLAYER); // must be a player but is console + } + } + + if (permission.isBlank() || manager.hasPermission(source, permission)) { + return new Result(Meta.ALLOWED); + } + return new Result(Meta.NO_PERMISSION); + } + + @Override + public @NonNull CloudKey key() { + return CloudKey.cloudKey(permission); + } + + /** + * Basic implementation of cloud's {@link PermissionResult} that delegates to the more informative {@link Meta}. + */ + public final class Result implements PermissionResult { + + private final Meta meta; + + private Result(Meta meta) { + this.meta = meta; + } + + public Meta meta() { + return meta; + } + + @Override + public boolean allowed() { + return meta == Meta.ALLOWED; + } + + @Override + public @NonNull Permission permission() { + return GeyserPermission.this; + } + + /** + * More detailed explanation of whether the permission check passed. + */ + public enum Meta { + + /** + * The source must be a bedrock player, but is not. + */ + NOT_BEDROCK, + + /** + * The source must be a player, but is not. + */ + NOT_PLAYER, + + /** + * The source does not have a required permission node. + */ + NO_PERMISSION, + + /** + * The source meets all requirements. + */ + ALLOWED + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java index 466515b3f..75b9252da 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java @@ -25,33 +25,32 @@ package org.geysermc.geyser.command.defaults; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class AdvancedTooltipsCommand extends GeyserCommand { + public AdvancedTooltipsCommand(String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session != null) { - String onOrOff = session.isAdvancedTooltips() ? "off" : "on"; - session.setAdvancedTooltips(!session.isAdvancedTooltips()); - session.sendMessage("§l§e" + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + " §r" + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale())); - session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); - } - } + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + String onOrOff = session.isAdvancedTooltips() ? "off" : "on"; + session.setAdvancedTooltips(!session.isAdvancedTooltips()); + session.sendMessage(ChatColor.BOLD + ChatColor.YELLOW + + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + + " " + ChatColor.RESET + + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale())); + session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory()); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java index 28253433f..0cba28f33 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java @@ -25,29 +25,23 @@ package org.geysermc.geyser.command.defaults; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class AdvancementsCommand extends GeyserCommand { + public AdvancementsCommand(String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session != null) { - session.getAdvancementsCache().buildAndShowMenuForm(); - } - } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.getAdvancementsCache().buildAndShowMenuForm(); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java index 981c97595..d2066dba1 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java @@ -26,90 +26,82 @@ package org.geysermc.geyser.command.defaults; import com.fasterxml.jackson.databind.JsonNode; -import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.configuration.GeyserConfiguration; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.LoopbackUtil; import org.geysermc.geyser.util.WebUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Random; import java.util.concurrent.CompletableFuture; +import static org.incendo.cloud.parser.standard.IntegerParser.integerParser; +import static org.incendo.cloud.parser.standard.StringParser.stringParser; + public class ConnectionTestCommand extends GeyserCommand { + /* * The MOTD is temporarily changed during the connection test. * This allows us to check if we are pinging the correct Geyser instance */ public static String CONNECTION_TEST_MOTD = null; - private final GeyserImpl geyser; + private static final String ADDRESS = "address"; + private static final String PORT = "port"; + private final GeyserImpl geyser; private final Random random = new Random(); public ConnectionTestCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) { - // Only allow the console to create dumps on Geyser Standalone - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .required(ADDRESS, stringParser()) + .optional(PORT, integerParser(0, 65535)) + .handler(this::execute)); + } - if (args.length == 0) { - sender.sendMessage("Provide the server IP and port you are trying to test Bedrock connections for. Example: `test.geysermc.org:19132`"); - return; - } + @Override + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String ipArgument = context.get(ADDRESS); + Integer portArgument = context.getOrDefault(PORT, null); // null if port was not specified // Replace "<" and ">" symbols if they are present to avoid the common issue of people including them - String[] fullAddress = args[0].replace("<", "").replace(">", "").split(":", 2); - - // Still allow people to not supply a port and fallback to 19132 - int port; - if (fullAddress.length == 2) { - try { - port = Integer.parseInt(fullAddress[1]); - } catch (NumberFormatException e) { - // can occur if e.g. "/geyser connectiontest : is ran - sender.sendMessage("Not a valid port! Specify a valid numeric port."); - return; - } - } else { - port = geyser.getConfig().getBedrock().broadcastPort(); - } - String ip = fullAddress[0]; + final String ip = ipArgument.replace("<", "").replace(">", ""); + final int port = portArgument != null ? portArgument : geyser.getConfig().getBedrock().broadcastPort(); // default bedrock port // Issue: people commonly checking placeholders if (ip.equals("ip")) { - sender.sendMessage(ip + " is not a valid IP, and instead a placeholder. Please specify the IP to check."); + source.sendMessage(ip + " is not a valid IP, and instead a placeholder. Please specify the IP to check."); return; } // Issue: checking 0.0.0.0 won't work if (ip.equals("0.0.0.0")) { - sender.sendMessage("Please specify the IP that you would connect with. 0.0.0.0 in the config tells Geyser to the listen on the server's IPv4."); + source.sendMessage("Please specify the IP that you would connect with. 0.0.0.0 in the config tells Geyser to the listen on the server's IPv4."); return; } // Issue: people testing local ip if (ip.equals("localhost") || ip.startsWith("127.") || ip.startsWith("10.") || ip.startsWith("192.168.")) { - sender.sendMessage("This tool checks if connections from other networks are possible, so you cannot check a local IP."); + source.sendMessage("This tool checks if connections from other networks are possible, so you cannot check a local IP."); return; } // Issue: port out of bounds if (port <= 0 || port >= 65535) { - sender.sendMessage("The port you specified is invalid! Please specify a valid port."); + source.sendMessage("The port you specified is invalid! Please specify a valid port."); return; } @@ -118,37 +110,37 @@ public class ConnectionTestCommand extends GeyserCommand { // Issue: do the ports not line up? We only check this if players don't override the broadcast port - if they do, they (hopefully) know what they're doing if (config.getBedrock().broadcastPort() == config.getBedrock().port()) { if (port != config.getBedrock().port()) { - if (fullAddress.length == 2) { - sender.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration (" + if (portArgument != null) { + source.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration (" + config.getBedrock().port() + ")"); - sender.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config."); + source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config."); if (config.getBedrock().isCloneRemotePort()) { - sender.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead."); + source.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead."); } } else { - sender.sendMessage("You did not specify the port to check (add it with \":\"), " + + source.sendMessage("You did not specify the port to check (add it with \":\"), " + "and the default port 19132 does not match the port in your Geyser configuration (" + config.getBedrock().port() + ")!"); - sender.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`."); + source.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`."); } } } else { if (config.getBedrock().broadcastPort() != port) { - sender.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration (" + source.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration (" + config.getBedrock().broadcastPort() + "). "); - sender.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on."); - sender.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config."); + source.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on."); + source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config."); } } // Issue: is the `bedrock` `address` in the config different? if (!config.getBedrock().address().equals("0.0.0.0")) { - sender.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional."); + source.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional."); } // Issue: did someone turn on enable-proxy-protocol, and they didn't mean it? if (config.getBedrock().isEnableProxyProtocol()) { - sender.sendMessage("You have the `enable-proxy-protocol` setting enabled. " + + source.sendMessage("You have the `enable-proxy-protocol` setting enabled. " + "Unless you're deliberately using additional software that REQUIRES this setting, you may not need it enabled."); } @@ -157,14 +149,14 @@ public class ConnectionTestCommand extends GeyserCommand { // Issue: SRV record? String[] record = WebUtils.findSrvRecord(geyser, ip); if (record != null && !ip.equals(record[3]) && !record[2].equals(String.valueOf(port))) { - sender.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2] + source.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2] + ". If that fails, re-run this command with that address and port."); return; } // Issue: does Loopback need applying? if (LoopbackUtil.needsLoopback(GeyserImpl.getInstance().getLogger())) { - sender.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " + + source.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " + "See here for steps on how to resolve: " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/#using-geyser-on-the-same-computer"); } @@ -178,7 +170,7 @@ public class ConnectionTestCommand extends GeyserCommand { String connectionTestMotd = "Geyser Connection Test " + randomStr; CONNECTION_TEST_MOTD = connectionTestMotd; - sender.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait..."); + source.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait..."); JsonNode output; try { String hostname = URLEncoder.encode(ip, StandardCharsets.UTF_8); @@ -200,31 +192,31 @@ public class ConnectionTestCommand extends GeyserCommand { JsonNode pong = ping.get("pong"); String remoteMotd = pong.get("motd").asText(); if (!connectionTestMotd.equals(remoteMotd)) { - sender.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " + + source.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " + "Did you supply the correct IP and port of your server?"); - sendLinks(sender); + sendLinks(source); return; } if (ping.get("tcpFirst").asBoolean()) { - sender.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information."); - sendLinks(sender); + source.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information."); + sendLinks(source); return; } - sender.sendMessage("Your server is likely online and working as of " + when + "!"); - sendLinks(sender); + source.sendMessage("Your server is likely online and working as of " + when + "!"); + sendLinks(source); return; } - sender.sendMessage("Your server is likely unreachable from outside the network!"); + source.sendMessage("Your server is likely unreachable from outside the network!"); JsonNode message = output.get("message"); if (message != null && !message.asText().isEmpty()) { - sender.sendMessage("Got the error message: " + message.asText()); + source.sendMessage("Got the error message: " + message.asText()); } - sendLinks(sender); + sendLinks(source); } catch (Exception e) { - sender.sendMessage("An error occurred while trying to check your connection! Check the console for more information."); + source.sendMessage("An error occurred while trying to check your connection! Check the console for more information."); geyser.getLogger().error("Error while trying to check your connection!", e); } }); @@ -235,9 +227,4 @@ public class ConnectionTestCommand extends GeyserCommand { "https://wiki.geysermc.org/geyser/setup/"); sender.sendMessage("If that does not work, see " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/" + ", or contact us on Discord: " + "https://discord.gg/geysermc"); } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java index b3fee375f..45100f525 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java @@ -29,43 +29,71 @@ import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.dump.DumpInfo; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.WebUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.suggestion.SuggestionProvider; import java.io.FileOutputStream; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; +import static org.incendo.cloud.parser.standard.StringArrayParser.stringArrayParser; + public class DumpCommand extends GeyserCommand { + private static final String ARGUMENTS = "args"; + private static final Iterable SUGGESTIONS = List.of("full", "offline", "logs"); + private final GeyserImpl geyser; private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String DUMP_URL = "https://dump.geysermc.org/"; public DumpCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } - @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - // Only allow the console to create dumps on Geyser Standalone - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; + @Override + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .optional(ARGUMENTS, stringArrayParser(), SuggestionProvider.blockingStrings((ctx, input) -> { + // parse suggestions here + List inputs = new ArrayList<>(); + while (input.hasRemainingInput()) { + inputs.add(input.readStringSkipWhitespace()); + } + + if (inputs.size() <= 2) { + return SUGGESTIONS; // only `geyser dump` was typed (2 literals) + } + + // the rest of the input after `geyser dump` is for this argument + inputs = inputs.subList(2, inputs.size()); + + // don't suggest any words they have already typed + List suggestions = new ArrayList<>(); + SUGGESTIONS.forEach(suggestions::add); + suggestions.removeAll(inputs); + return suggestions; + })) + .handler(this::execute)); } + @Override + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String[] args = context.getOrDefault(ARGUMENTS, new String[0]); + boolean showSensitive = false; boolean offlineDump = false; boolean addLog = false; @@ -75,13 +103,14 @@ public class DumpCommand extends GeyserCommand { case "full" -> showSensitive = true; case "offline" -> offlineDump = true; case "logs" -> addLog = true; + default -> context.sender().sendMessage("Invalid geyser dump option " + arg + "! Fallback to no arguments."); } } } AsteriskSerializer.showSensitive = showSensitive; - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", source.locale())); String dumpData; try { if (offlineDump) { @@ -93,7 +122,7 @@ public class DumpCommand extends GeyserCommand { dumpData = MAPPER.writeValueAsString(new DumpInfo(addLog)); } } catch (IOException e) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.collect_error_short"), e); return; } @@ -101,21 +130,21 @@ public class DumpCommand extends GeyserCommand { String uploadedDumpUrl; if (offlineDump) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.writing", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.writing", source.locale())); try { FileOutputStream outputStream = new FileOutputStream(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("dump.json").toFile()); outputStream.write(dumpData.getBytes()); outputStream.close(); } catch (IOException e) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.write_error", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.write_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.write_error_short"), e); return; } uploadedDumpUrl = "dump.json"; } else { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", source.locale())); String response; JsonNode responseNode; @@ -123,33 +152,22 @@ public class DumpCommand extends GeyserCommand { response = WebUtils.post(DUMP_URL + "documents", dumpData); responseNode = MAPPER.readTree(response); } catch (IOException e) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", source.locale())); geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.upload_error_short"), e); return; } if (!responseNode.has("key")) { - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", sender.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response)); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response)); return; } uploadedDumpUrl = DUMP_URL + responseNode.get("key").asText(); } - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", sender.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl); - if (!sender.isConsole()) { - geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.commands.dump.created", sender.name(), uploadedDumpUrl)); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", source.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl); + if (!source.isConsole()) { + geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.commands.dump.created", source.name(), uploadedDumpUrl)); } } - - @NonNull - @Override - public List subCommands() { - return Arrays.asList("offline", "full", "logs"); - } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java index df33437d9..24881f2ca 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java @@ -25,14 +25,14 @@ package org.geysermc.geyser.command.defaults; -import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.Comparator; import java.util.List; @@ -41,22 +41,23 @@ public class ExtensionsCommand extends GeyserCommand { private final GeyserImpl geyser; public ExtensionsCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.TRUE); this.geyser = geyser; } @Override - public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) { + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + // TODO: Pagination int page = 1; int maxPage = 1; - String header = GeyserLocale.getPlayerLocaleString("geyser.commands.extensions.header", sender.locale(), page, maxPage); - sender.sendMessage(header); + String header = GeyserLocale.getPlayerLocaleString("geyser.commands.extensions.header", source.locale(), page, maxPage); + source.sendMessage(header); this.geyser.extensionManager().extensions().stream().sorted(Comparator.comparing(Extension::name)).forEach(extension -> { String extensionName = (extension.isEnabled() ? ChatColor.GREEN : ChatColor.RED) + extension.name(); - sender.sendMessage("- " + extensionName + ChatColor.RESET + " v" + extension.description().version() + formatAuthors(extension.description().authors())); + source.sendMessage("- " + extensionName + ChatColor.RESET + " v" + extension.description().version() + formatAuthors(extension.description().authors())); }); } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java index c9671b089..9911863ab 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java @@ -25,61 +25,59 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.api.util.PlatformType; -import org.geysermc.geyser.GeyserImpl; +import com.google.common.base.Predicates; import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Map; public class HelpCommand extends GeyserCommand { - private final GeyserImpl geyser; - private final String baseCommand; - private final Map commands; + private final String rootCommand; + private final Collection commands; - public HelpCommand(GeyserImpl geyser, String name, String description, String permission, - String baseCommand, Map commands) { - super(name, description, permission); - this.geyser = geyser; - this.baseCommand = baseCommand; - this.commands = commands; - - this.setAliases(Collections.singletonList("?")); + public HelpCommand(String rootCommand, String name, String description, String permission, Map commands) { + super(name, description, permission, TriState.TRUE); + this.rootCommand = rootCommand; + this.commands = commands.values(); + this.aliases = Collections.singletonList("?"); } - /** - * Sends the help menu to a command sender. Will not show certain commands depending on the command sender and session. - * - * @param session The Geyser session of the command sender, if it is a bedrock player. If null, bedrock-only commands will be hidden. - * @param sender The CommandSender to send the help message to. - * @param args Not used. - */ @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { + public String rootCommand() { + return rootCommand; + } + + @Override + public void execute(CommandContext context) { + execute(context.sender()); + } + + public void execute(GeyserCommandSource source) { + boolean bedrockPlayer = source.connection() != null; + + // todo: pagination int page = 1; int maxPage = 1; - String translationKey = this.baseCommand.equals("geyser") ? "geyser.commands.help.header" : "geyser.commands.extensions.header"; - String header = GeyserLocale.getPlayerLocaleString(translationKey, sender.locale(), page, maxPage); - sender.sendMessage(header); + String translationKey = this.rootCommand.equals(DEFAULT_ROOT_COMMAND) ? "geyser.commands.help.header" : "geyser.commands.extensions.header"; + String header = GeyserLocale.getPlayerLocaleString(translationKey, source.locale(), page, maxPage); + source.sendMessage(header); - this.commands.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { - Command cmd = entry.getValue(); - - // Standalone hack-in since it doesn't have a concept of permissions - if (geyser.getPlatformType() == PlatformType.STANDALONE || sender.hasPermission(cmd.permission())) { - // Only list commands the player can actually run - if (cmd.isBedrockOnly() && session == null) { - return; - } - - sender.sendMessage(ChatColor.YELLOW + "/" + baseCommand + " " + entry.getKey() + ChatColor.WHITE + ": " + - GeyserLocale.getPlayerLocaleString(cmd.description(), sender.locale())); - } - }); + this.commands.stream() + .distinct() // remove aliases + .filter(bedrockPlayer ? Predicates.alwaysTrue() : cmd -> !cmd.isBedrockOnly()) // remove bedrock only commands if not a bedrock player + .filter(cmd -> source.hasPermission(cmd.permission())) + .sorted(Comparator.comparing(Command::name)) + .forEachOrdered(cmd -> { + String description = GeyserLocale.getPlayerLocaleString(cmd.description(), source.locale()); + source.sendMessage(ChatColor.YELLOW + "/" + rootCommand + " " + cmd.name() + ChatColor.WHITE + ": " + description); + }); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java index 90446fbb6..5a76ab902 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java @@ -26,10 +26,12 @@ package org.geysermc.geyser.command.defaults; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.stream.Collectors; @@ -38,22 +40,18 @@ public class ListCommand extends GeyserCommand { private final GeyserImpl geyser; public ListCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - String message = GeyserLocale.getPlayerLocaleString("geyser.commands.list.message", sender.locale(), - geyser.getSessionManager().size(), - geyser.getSessionManager().getAllSessions().stream().map(GeyserSession::bedrockUsername).collect(Collectors.joining(" "))); + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); - sender.sendMessage(message); - } + String message = GeyserLocale.getPlayerLocaleString("geyser.commands.list.message", source.locale(), + geyser.getSessionManager().size(), + geyser.getSessionManager().getAllSessions().stream().map(GeyserSession::bedrockUsername).collect(Collectors.joining(" "))); - @Override - public boolean isSuggestedOpOnly() { - return true; + source.sendMessage(message); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java index 6188e6924..5f9061618 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java @@ -25,33 +25,23 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class OffhandCommand extends GeyserCommand { - public OffhandCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + public OffhandCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session == null) { - return; - } - + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); session.requestOffhandSwap(); } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java index 987860238..e54b83ddf 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java @@ -25,12 +25,12 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.concurrent.TimeUnit; @@ -39,27 +39,17 @@ public class ReloadCommand extends GeyserCommand { private final GeyserImpl geyser; public ReloadCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - return; - } - - String message = GeyserLocale.getPlayerLocaleString("geyser.commands.reload.message", sender.locale()); - - sender.sendMessage(message); + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.reload.message", source.locale())); geyser.getSessionManager().disconnectAll("geyser.commands.reload.kick"); //FIXME Without the tiny wait, players do not get kicked - same happens when Geyser tries to disconnect all sessions on shutdown geyser.getScheduledThread().schedule(geyser::reloadGeyser, 10, TimeUnit.MILLISECONDS); } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java index 7828cf1d2..a5734a69f 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java @@ -25,31 +25,24 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.SettingsUtils; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class SettingsCommand extends GeyserCommand { - public SettingsCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + + public SettingsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session != null) { - session.sendForm(SettingsUtils.buildForm(session)); - } - } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.sendForm(SettingsUtils.buildForm(session)); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java index 5952ea00d..eebb9170c 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java @@ -25,35 +25,28 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.ClientCommand; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; public class StatisticsCommand extends GeyserCommand { - public StatisticsCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + public StatisticsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (session == null) return; + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); session.setWaitingForStatistics(true); - ServerboundClientCommandPacket ServerboundClientCommandPacket = new ServerboundClientCommandPacket(ClientCommand.STATS); - session.sendDownstreamGamePacket(ServerboundClientCommandPacket); - } - - @Override - public boolean isExecutableOnConsole() { - return false; - } - - @Override - public boolean isBedrockOnly() { - return true; + ServerboundClientCommandPacket packet = new ServerboundClientCommandPacket(ClientCommand.STATS); + session.sendDownstreamGamePacket(packet); } } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java index 1cd3050c9..f6dc1610a 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java @@ -25,12 +25,11 @@ package org.geysermc.geyser.command.defaults; -import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; import java.util.Collections; @@ -39,24 +38,13 @@ public class StopCommand extends GeyserCommand { private final GeyserImpl geyser; public StopCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; - - this.setAliases(Collections.singletonList("shutdown")); + this.aliases = Collections.singletonList("shutdown"); } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { - if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); - return; - } - + public void execute(CommandContext context) { geyser.getBootstrap().onGeyserShutdown(); } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java index c6852d577..8d34c1bf0 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java @@ -29,13 +29,14 @@ import com.fasterxml.jackson.databind.JsonNode; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.WebUtils; +import org.incendo.cloud.context.CommandContext; import java.io.IOException; import java.util.List; @@ -45,13 +46,14 @@ public class VersionCommand extends GeyserCommand { private final GeyserImpl geyser; public VersionCommand(GeyserImpl geyser, String name, String description, String permission) { - super(name, description, permission); - + super(name, description, permission, TriState.NOT_SET); this.geyser = geyser; } @Override - public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) { + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String bedrockVersions; List supportedCodecs = GameProtocol.SUPPORTED_BEDROCK_CODECS; if (supportedCodecs.size() > 1) { @@ -67,45 +69,37 @@ public class VersionCommand extends GeyserCommand { javaVersions = supportedJavaVersions.get(0); } - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.version", sender.locale(), + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.version", source.locale(), GeyserImpl.NAME, GeyserImpl.VERSION, javaVersions, bedrockVersions)); // Disable update checking in dev mode and for players in Geyser Standalone - if (!GeyserImpl.getInstance().isProductionEnvironment() || (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { + if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) { return; } if (GeyserImpl.IS_DEV) { - // TODO cloud use language string - sender.sendMessage("You are running a development build of Geyser! Please report any bugs you find on our Discord server: %s" - .formatted("https://discord.gg/geysermc")); - //sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.core.dev_build", sender.locale(), "https://discord.gg/geysermc")); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.core.dev_build", source.locale(), "https://discord.gg/geysermc")); return; } - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", source.locale())); try { int buildNumber = this.geyser.buildNumber(); JsonNode response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest"); int latestBuildNumber = response.get("build").asInt(); if (latestBuildNumber == buildNumber) { - sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", sender.locale())); + source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", source.locale())); return; } - sender.sendMessage(GeyserLocale.getPlayerLocaleString( + source.sendMessage(GeyserLocale.getPlayerLocaleString( "geyser.commands.version.outdated", - sender.locale(), (latestBuildNumber - buildNumber), "https://geysermc.org/download" + source.locale(), (latestBuildNumber - buildNumber), "https://geysermc.org/download" )); } catch (IOException e) { GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.version.failed"), e); - sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", sender.locale())); + source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", source.locale())); } } - - @Override - public boolean isSuggestedOpOnly() { - return true; - } } diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java new file mode 100644 index 000000000..edacd49ff --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2024 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.geyser.command.standalone; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +import java.util.Collections; +import java.util.Set; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final +public class PermissionConfiguration { + + @JsonProperty("default-permissions") + private Set defaultPermissions = Collections.emptySet(); +} diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java new file mode 100644 index 000000000..99c53f319 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019-2024 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.geyser.command.standalone; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionCheckersEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; +import org.geysermc.geyser.api.permission.PermissionChecker; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.CommandRegistry; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.util.FileUtils; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.internal.CommandRegistrationHandler; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class StandaloneCloudCommandManager extends CommandManager { + + private final GeyserImpl geyser; + + /** + * The checkers we use to test if a command source has a permission + */ + private final List permissionCheckers = new ArrayList<>(); + + /** + * Any permissions that all connections have + */ + private final Set basePermissions = new ObjectOpenHashSet<>(); + + public StandaloneCloudCommandManager(GeyserImpl geyser) { + super(ExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler()); + // simpleCoordinator: execute commands immediately on the calling thread. + // nullCommandRegistrationHandler: cloud is not responsible for handling our CommandRegistry, which is fairly decoupled. + this.geyser = geyser; + + // allow any extensions to customize permissions + geyser.getEventBus().fire((GeyserRegisterPermissionCheckersEvent) permissionCheckers::add); + + // must still implement a basic permission system + try { + File permissionsFile = geyser.getBootstrap().getConfigFolder().resolve("permissions.yml").toFile(); + FileUtils.fileOrCopiedFromResource(permissionsFile, "permissions.yml", geyser.getBootstrap()); + PermissionConfiguration config = FileUtils.loadConfig(permissionsFile, PermissionConfiguration.class); + basePermissions.addAll(config.getDefaultPermissions()); + } catch (Exception e) { + geyser.getLogger().error("Failed to load permissions.yml - proceeding without it", e); + } + } + + /** + * Fire a {@link GeyserRegisterPermissionsEvent} to determine any additions or removals to the base list of + * permissions. This should be called after any event listeners have been registered, such as that of {@link CommandRegistry}. + */ + public void fireRegisterPermissionsEvent() { + geyser.getEventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> { + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(def, "permission default for " + permission); + + if (permission.isBlank()) { + return; + } + if (def == TriState.TRUE) { + basePermissions.add(permission); + } + }); + } + + @Override + public boolean hasPermission(@NonNull GeyserCommandSource sender, @NonNull String permission) { + // Note: the two GeyserCommandSources on Geyser-Standalone are GeyserLogger and GeyserSession + // GeyserLogger#hasPermission always returns true + // GeyserSession#hasPermission delegates to this method, + // which is why this method doesn't just call GeyserCommandSource#hasPermission + if (sender.isConsole()) { + return true; + } + + // An empty or blank permission is treated as a lack of permission requirement + if (permission.isBlank()) { + return true; + } + + for (PermissionChecker checker : permissionCheckers) { + Boolean result = checker.hasPermission(sender, permission).toBoolean(); + if (result != null) { + return result; + } + // undefined - try the next checker to see if it has a defined value + } + // fallback to our list of default permissions + // note that a PermissionChecker may in fact override any values set here by returning FALSE + return basePermissions.contains(permission); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java index e07a62d8a..4a6efbbd4 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java @@ -35,12 +35,12 @@ import java.util.Map; public abstract class GeyserDefineCommandsEventImpl implements GeyserDefineCommandsEvent { private final Map commands; - public GeyserDefineCommandsEventImpl(Map commands) { - this.commands = commands; + public GeyserDefineCommandsEventImpl(Map commands) { + this.commands = Collections.unmodifiableMap(commands); } @Override public @NonNull Map commands() { - return Collections.unmodifiableMap(this.commands); + return this.commands; } } diff --git a/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java b/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java index 4a7830c90..0b22a8b8e 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java +++ b/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java @@ -25,19 +25,208 @@ package org.geysermc.geyser.extension.command; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.command.CommandExecutor; +import org.geysermc.geyser.api.command.CommandSource; +import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; public abstract class GeyserExtensionCommand extends GeyserCommand { + private final Extension extension; + private final String rootCommand; - public GeyserExtensionCommand(Extension extension, String name, String description, String permission) { - super(name, description, permission); + public GeyserExtensionCommand(@NonNull Extension extension, @NonNull String name, @NonNull String description, + @NonNull String permission, @Nullable TriState permissionDefault, + boolean playerOnly, boolean bedrockOnly) { + super(name, description, permission, permissionDefault, playerOnly, bedrockOnly); this.extension = extension; + this.rootCommand = Objects.requireNonNull(extension.rootCommand()); + + if (this.rootCommand.isBlank()) { + throw new IllegalStateException("rootCommand of extension " + extension.name() + " may not be blank"); + } } - public Extension extension() { + public final Extension extension() { return this.extension; } + + @Override + public final String rootCommand() { + return this.rootCommand; + } + + public static class Builder implements Command.Builder { + @NonNull private final Extension extension; + @Nullable private Class sourceType; + @Nullable private String name; + @NonNull private String description = ""; + @NonNull private String permission = ""; + @Nullable private TriState permissionDefault; + @Nullable private List aliases; + private boolean suggestedOpOnly = false; // deprecated for removal + private boolean playerOnly = false; + private boolean bedrockOnly = false; + @Nullable private CommandExecutor executor; + + public Builder(@NonNull Extension extension) { + this.extension = Objects.requireNonNull(extension); + } + + @Override + public Command.Builder source(@NonNull Class sourceType) { + this.sourceType = Objects.requireNonNull(sourceType, "command source type"); + return this; + } + + @Override + public Builder name(@NonNull String name) { + this.name = Objects.requireNonNull(name, "command name"); + return this; + } + + @Override + public Builder description(@NonNull String description) { + this.description = Objects.requireNonNull(description, "command description"); + return this; + } + + @Override + public Builder permission(@NonNull String permission) { + this.permission = Objects.requireNonNull(permission, "command permission"); + return this; + } + + @Override + public Builder permission(@NonNull String permission, @NonNull TriState defaultValue) { + this.permission = Objects.requireNonNull(permission, "command permission"); + this.permissionDefault = Objects.requireNonNull(defaultValue, "command permission defaultValue"); + return this; + } + + @Override + public Builder aliases(@NonNull List aliases) { + this.aliases = Objects.requireNonNull(aliases, "command aliases"); + return this; + } + + @SuppressWarnings("removal") // this is our doing + @Override + public Builder suggestedOpOnly(boolean suggestedOpOnly) { + this.suggestedOpOnly = suggestedOpOnly; + if (suggestedOpOnly) { + // the most amount of legacy/deprecated behaviour I'm willing to support + this.permissionDefault = TriState.NOT_SET; + } + return this; + } + + @SuppressWarnings("removal") // this is our doing + @Override + public Builder executableOnConsole(boolean executableOnConsole) { + this.playerOnly = !executableOnConsole; + return this; + } + + @Override + public Command.Builder playerOnly(boolean playerOnly) { + this.playerOnly = playerOnly; + return this; + } + + @Override + public Builder bedrockOnly(boolean bedrockOnly) { + this.bedrockOnly = bedrockOnly; + return this; + } + + @Override + public Builder executor(@NonNull CommandExecutor executor) { + this.executor = Objects.requireNonNull(executor, "command executor"); + return this; + } + + @NonNull + @Override + public GeyserExtensionCommand build() { + // These are captured in the anonymous lambda below and shouldn't change even if the builder does + final Class sourceType = this.sourceType; + final boolean suggestedOpOnly = this.suggestedOpOnly; + final CommandExecutor executor = this.executor; + + if (name == null) { + throw new IllegalArgumentException("name was not provided for a command in extension " + extension.name()); + } + if (sourceType == null) { + throw new IllegalArgumentException("Source type was not defined for command " + name + " in extension " + extension.name()); + } + if (executor == null) { + throw new IllegalArgumentException("Command executor was not defined for command " + name + " in extension " + extension.name()); + } + + // if the source type is a GeyserConnection then it is inherently bedrockOnly + final boolean bedrockOnly = this.bedrockOnly || GeyserConnection.class.isAssignableFrom(sourceType); + // a similar check would exist for executableOnConsole, but there is not a logger type exposed in the api + + GeyserExtensionCommand command = new GeyserExtensionCommand(extension, name, description, permission, permissionDefault, playerOnly, bedrockOnly) { + + @Override + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .optional("args", greedyStringParser()) + .handler(this::execute)); + } + + @SuppressWarnings("unchecked") + @Override + public void execute(CommandContext context) { + GeyserCommandSource source = context.sender(); + String[] args = context.getOrDefault("args", "").split(" "); + + if (sourceType.isInstance(source)) { + executor.execute((T) source, this, args); + return; + } + + @Nullable GeyserSession session = source.connection(); + if (sourceType.isInstance(session)) { + executor.execute((T) session, this, args); + return; + } + + // currently, the only subclass of CommandSource exposed in the api is GeyserConnection. + // when this command was registered, we enabled bedrockOnly if the sourceType was a GeyserConnection. + // as a result, the permission checker should handle that case and this method shouldn't even be reached. + source.sendMessage("You must be a " + sourceType.getSimpleName() + " to run this command."); + } + + @SuppressWarnings("removal") // this is our doing + @Override + public boolean isSuggestedOpOnly() { + return suggestedOpOnly; + } + }; + + if (aliases != null) { + command.aliases = new ArrayList<>(aliases); + } + return command; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java index 9faa7424c..9cf2c0179 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java @@ -118,11 +118,6 @@ public class GeyserWorldManager extends WorldManager { return GameMode.SURVIVAL; } - @Override - public boolean hasPermission(GeyserSession session, String permission) { - return false; - } - @NonNull @Override public CompletableFuture<@Nullable DataComponents> getPickItemComponents(GeyserSession session, int x, int y, int z, boolean addNbtData) { diff --git a/core/src/main/java/org/geysermc/geyser/level/WorldManager.java b/core/src/main/java/org/geysermc/geyser/level/WorldManager.java index 4a20771f2..6baf9c2b4 100644 --- a/core/src/main/java/org/geysermc/geyser/level/WorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/WorldManager.java @@ -185,15 +185,6 @@ public abstract class WorldManager { session.sendCommand("difficulty " + difficulty.name().toLowerCase(Locale.ROOT)); } - /** - * Checks if the given session's player has a permission - * - * @param session The session of the player to check the permission of - * @param permission The permission node to check - * @return True if the player has the requested permission, false if not - */ - public abstract boolean hasPermission(GeyserSession session, String permission); - /** * Returns a list of biome identifiers available on the server. */ diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index 4b159438c..94de0c298 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -42,8 +42,8 @@ import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData; import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.impl.camera.GeyserCameraFade; import org.geysermc.geyser.impl.camera.GeyserCameraPosition; -import org.geysermc.geyser.command.GeyserCommandManager; import org.geysermc.geyser.event.GeyserEventRegistrar; +import org.geysermc.geyser.extension.command.GeyserExtensionCommand; import org.geysermc.geyser.item.GeyserCustomItemData; import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; @@ -67,7 +67,7 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov @Override public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { // misc - providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Extension) args[0])); + providers.put(Command.Builder.class, args -> new GeyserExtensionCommand.Builder<>((Extension) args[0])); providers.put(CustomBlockComponents.Builder.class, args -> new GeyserCustomBlockComponents.Builder()); providers.put(CustomBlockData.Builder.class, args -> new GeyserCustomBlockData.Builder()); diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 97dd75905..899b53fb3 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1454,11 +1454,28 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return false; } + @Override + public UUID playerUuid() { + return javaUuid(); // CommandSource allows nullable + } + + @Override + public GeyserSession connection() { + return this; + } + @Override public String locale() { return clientData.getLanguageCode(); } + @Override + public boolean hasPermission(String permission) { + // for Geyser-Standalone, standalone's permission system will handle it. + // for server platforms, the session will be mapped to a server command sender, and the server's api will be used. + return geyser.commandRegistry().hasPermission(this, permission); + } + /** * Sends a chat message to the Java server. */ @@ -1771,17 +1788,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { upstream.sendPacket(gameRulesChangedPacket); } - /** - * Checks if the given session's player has a permission - * - * @param permission The permission node to check - * @return true if the player has the requested permission, false if not - */ - @Override - public boolean hasPermission(String permission) { - return geyser.getWorldManager().hasPermission(this, permission); - } - private static final Ability[] USED_ABILITIES = Ability.values(); /** diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java index 8d4df6f3f..1e84f032e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.bedrock; import org.cloudburstmc.protocol.bedrock.packet.CommandRequestPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.util.PlatformType; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -43,13 +44,26 @@ public class BedrockCommandRequestTranslator extends PacketTranslator 0) { + String root = args[0]; + + CommandRegistry registry = GeyserImpl.getInstance().commandRegistry(); + if (registry.rootCommands().contains(root)) { + registry.runCommand(session, command); + return; // don't pass the command to the java server + } + } } + + if (MessageTranslator.isTooLong(command, session)) { + return; + } + + session.sendCommand(command); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java index aa815fab7..a7199be97 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player; import org.cloudburstmc.protocol.bedrock.packet.SetDefaultGameTypePacket; import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -41,7 +42,7 @@ public class BedrockSetDefaultGameTypeTranslator extends PacketTranslator= 2 && session.hasPermission("geyser.settings.server")) { + if (session.getOpPermissionLevel() >= 2 && session.hasPermission(Permissions.SERVER_SETTINGS)) { session.getGeyser().getWorldManager().setDefaultGameMode(session, GameMode.byId(packet.getGamemode())); } // Stop the client from updating their own Gamemode without telling the server diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java index 176f00b8f..c3fa2a1b3 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player; +import org.geysermc.geyser.Permissions; import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket; import org.geysermc.geyser.session.GeyserSession; @@ -39,7 +40,7 @@ public class BedrockSetDifficultyTranslator extends PacketTranslator= 2 && session.hasPermission("geyser.settings.server")) { + if (session.getOpPermissionLevel() >= 2 && session.hasPermission(Permissions.SERVER_SETTINGS)) { if (packet.getDifficulty() != session.getWorldCache().getDifficulty().ordinal()) { session.getGeyser().getWorldManager().setDifficulty(session, Difficulty.from(packet.getDifficulty())); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java index f00156268..0590ca0ad 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.translator.protocol.bedrock.entity.player; import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -45,7 +46,7 @@ public class BedrockSetPlayerGameTypeTranslator extends PacketTranslator= 2 && session.hasPermission("geyser.settings.server")) { + if (session.getOpPermissionLevel() >= 2 && session.hasPermission(Permissions.SERVER_SETTINGS)) { if (packet.getGamemode() != session.getGameMode().ordinal()) { // Bedrock has more Gamemodes than Java, leading to cases 5 (for "default") and 6 (for "spectator") being sent // https://github.com/CloudburstMC/Protocol/blob/3.0/bedrock-codec/src/main/java/org/cloudburstmc/protocol/bedrock/data/GameType.java diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java index c0e3f5716..4c817ba01 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java @@ -41,7 +41,7 @@ import org.cloudburstmc.protocol.bedrock.data.command.*; import org.cloudburstmc.protocol.bedrock.packet.AvailableCommandsPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.java.ServerDefineCommandsEvent; -import org.geysermc.geyser.command.GeyserCommandManager; +import org.geysermc.geyser.command.CommandRegistry; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -122,7 +122,7 @@ public class JavaCommandsTranslator extends PacketTranslator commandData = new ArrayList<>(); IntSet commandNodes = new IntOpenHashSet(); @@ -151,8 +151,10 @@ public class JavaCommandsTranslator extends PacketTranslator new HashSet<>()).add(node.getName().toLowerCase()); + String name = node.getName().toLowerCase(Locale.ROOT); + String description = registry.description(name, session.locale()); + BedrockCommandInfo info = new BedrockCommandInfo(name, description, params); + commands.computeIfAbsent(info, $ -> new HashSet<>()).add(name); } var eventBus = session.getGeyser().eventBus(); diff --git a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java index c8423c3be..87ed8af02 100644 --- a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java @@ -100,6 +100,18 @@ public class FileUtils { return file; } + /** + * Open the specified file or copy if from resources + * + * @param file File to open + * @param name Name of the resource get if needed + * @return File handle of the specified file + * @throws IOException if the file failed to copy from resource + */ + public static File fileOrCopiedFromResource(File file, String name, GeyserBootstrap bootstrap) throws IOException { + return fileOrCopiedFromResource(file, name, Function.identity(), bootstrap); + } + /** * Writes the given data to the specified file on disk * diff --git a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java index 6f46b191c..cb6ad6f0c 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SettingsUtils.java @@ -29,6 +29,7 @@ import org.cloudburstmc.protocol.bedrock.packet.SetDifficultyPacket; import org.geysermc.cumulus.component.DropdownComponent; import org.geysermc.cumulus.form.CustomForm; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.Permissions; import org.geysermc.geyser.level.GameRule; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.session.GeyserSession; @@ -81,7 +82,7 @@ public class SettingsUtils { } } - boolean showGamerules = session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules"); + boolean showGamerules = session.getOpPermissionLevel() >= 2 || session.hasPermission(Permissions.SETTINGS_GAMERULES); if (showGamerules) { builder.label("geyser.settings.title.game_rules") .translator(MinecraftLocale::getLocaleString); // we need translate gamerules next diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index afbf78bbe..60b20023a 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit afbf78bbe0b39d0a076a42c228828c12f7f7da90 +Subproject commit 60b20023a92f084aba895ab0336e70fa7fb311fb diff --git a/core/src/main/resources/permissions.yml b/core/src/main/resources/permissions.yml new file mode 100644 index 000000000..4da9251e8 --- /dev/null +++ b/core/src/main/resources/permissions.yml @@ -0,0 +1,9 @@ + +# Add any permissions here that all players should have. +# Permissions for builtin Geyser commands do not have to be listed here. + +# If an extension/plugin registers their permissions with default values, entries here are typically unnecessary. +# If extensions don't register their permissions, permissions that everyone should have must be added here manually. + +default-permissions: + - geyser.command.help # this is unnecessary diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e50756ef1..f4abe18a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,13 +24,15 @@ terminalconsoleappender = "1.2.0" folia = "1.19.4-R0.1-SNAPSHOT" viaversion = "4.9.2" adapters = "1.13-SNAPSHOT" +cloud = "2.0.0-rc.2" +cloud-minecraft = "2.0.0-beta.9" +cloud-minecraft-modded = "2.0.0-beta.7" commodore = "2.2" bungeecord = "a7c6ede" velocity = "3.3.0-SNAPSHOT" viaproxy = "3.2.1" fabric-loader = "0.15.11" fabric-api = "0.100.1+1.21" -fabric-permissions = "0.2-SNAPSHOT" neoforge-minecraft = "21.0.0-beta" mixin = "0.8.5" mixinextras = "0.3.5" @@ -86,8 +88,14 @@ jline-terminal = { group = "org.jline", name = "jline-terminal", version.ref = " jline-terminal-jna = { group = "org.jline", name = "jline-terminal-jna", version.ref = "jline" } jline-reader = { group = "org.jline", name = "jline-reader", version.ref = "jline" } +cloud-core = { group = "org.incendo", name = "cloud-core", version.ref = "cloud" } +cloud-paper = { group = "org.incendo", name = "cloud-paper", version.ref = "cloud-minecraft" } +cloud-velocity = { group = "org.incendo", name = "cloud-velocity", version.ref = "cloud-minecraft" } +cloud-bungee = { group = "org.incendo", name = "cloud-bungee", version.ref = "cloud-minecraft" } +cloud-fabric = { group = "org.incendo", name = "cloud-fabric", version.ref = "cloud-minecraft-modded" } +cloud-neoforge = { group = "org.incendo", name = "cloud-neoforge", version.ref = "cloud-minecraft-modded" } + folia-api = { group = "dev.folia", name = "folia-api", version.ref = "folia" } -paper-mojangapi = { group = "io.papermc.paper", name = "paper-mojangapi", version.ref = "folia" } mixin = { group = "org.spongepowered", name = "mixin", version.ref = "mixin" } mixinextras = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinextras" } @@ -97,7 +105,6 @@ minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft # Check these on https://modmuss50.me/fabric.html fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "fabric-loader" } fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api" } -fabric-permissions = { group = "me.lucko", name = "fabric-permissions-api", version.ref = "fabric-permissions" } neoforge-minecraft = { group = "net.neoforged", name = "neoforge", version.ref = "neoforge-minecraft" } From 48311f877106ec6cf61a208358eaf304f861436f Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 12 Jul 2024 20:42:31 +0200 Subject: [PATCH 55/84] Add a /geyser ping command (#4131) * Init: Add /geyser ping command * Block just console execution, not everything but console senders * Use RTT as that seems to vary less wildly compared to getPing() * Cleanup, use lang strings * Add ping() method to GeyserConnection in api * Update to cloud changes --- .../api/connection/GeyserConnection.java | 5 ++ .../geyser/command/CommandRegistry.java | 2 + .../geyser/command/defaults/PingCommand.java | 49 +++++++++++++++++++ .../geyser/session/GeyserSession.java | 8 +++ 4 files changed, 64 insertions(+) create mode 100644 core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index 9bda4f903..ba559a462 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -132,4 +132,9 @@ public interface GeyserConnection extends Connection, CommandSource { @Deprecated @NonNull Set fogEffects(); + + /** + * Returns the current ping of the connection. + */ + int ping(); } diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java index f07092afd..54681abea 100644 --- a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java @@ -43,6 +43,7 @@ import org.geysermc.geyser.command.defaults.ExtensionsCommand; import org.geysermc.geyser.command.defaults.HelpCommand; import org.geysermc.geyser.command.defaults.ListCommand; import org.geysermc.geyser.command.defaults.OffhandCommand; +import org.geysermc.geyser.command.defaults.PingCommand; import org.geysermc.geyser.command.defaults.ReloadCommand; import org.geysermc.geyser.command.defaults.SettingsCommand; import org.geysermc.geyser.command.defaults.StatisticsCommand; @@ -139,6 +140,7 @@ public class CommandRegistry implements EventRegistrar { registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); + registerBuiltInCommand(new PingCommand("ping", "geyser.commands.ping.desc", "geyser.command.ping")); if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java new file mode 100644 index 000000000..f39be0528 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2023 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.geyser.command.defaults; + +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; + +public class PingCommand extends GeyserCommand { + + public PingCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.ping.message", session.locale(), session.ping())); + } +} + diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 899b53fb3..60321ea75 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -54,6 +54,8 @@ import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.netty.channel.raknet.RakChildChannel; +import org.cloudburstmc.netty.handler.codec.raknet.common.RakSessionCodec; import org.cloudburstmc.protocol.bedrock.BedrockDisconnectReasons; import org.cloudburstmc.protocol.bedrock.BedrockServerSession; import org.cloudburstmc.protocol.bedrock.data.Ability; @@ -2098,6 +2100,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return this.cameraData.fogEffects(); } + @Override + public int ping() { + RakSessionCodec rakSessionCodec = ((RakChildChannel) getUpstream().getSession().getPeer().getChannel()).rakPipeline().get(RakSessionCodec.class); + return (int) Math.floor(rakSessionCodec.getPing()); + } + public void addCommandEnum(String name, String enums) { softEnumPacket(name, SoftEnumUpdateType.ADD, enums); } From 813d1978875a6ef3538eb07e9767de9819959068 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 14 Jul 2024 22:09:55 +0200 Subject: [PATCH 56/84] Feature: API to switch items in the offhand/mainhand (#4819) --- .../java/org/geysermc/geyser/api/entity/EntityData.java | 6 ++++++ .../java/org/geysermc/geyser/entity/GeyserEntityData.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java index 90b3fc821..48c717089 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java @@ -81,4 +81,10 @@ public interface EntityData { * @return whether the movement is locked */ boolean isMovementLocked(); + + /** + * Sends a request to the Java server to switch the items in the main and offhand. + * There is no guarantee of the server accepting the request. + */ + void switchHands(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java index c9ef7a2dd..6f8f2525f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java @@ -96,4 +96,9 @@ public class GeyserEntityData implements EntityData { public boolean isMovementLocked() { return !movementLockOwners.isEmpty(); } + + @Override + public void switchHands() { + session.requestOffhandSwap(); + } } From 98c412c9edb4ab0e88ccb39a60272fbac7df05ae Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 23 Jul 2024 20:28:01 +0200 Subject: [PATCH 57/84] fix missing import --- core/src/main/java/org/geysermc/geyser/GeyserImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 464ebda96..01f1a118e 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -62,6 +62,7 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserPostReloadEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserPreReloadEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent; import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.BedrockListener; From 03187b6139214ed3a5d3e2697409d3ba9904127b Mon Sep 17 00:00:00 2001 From: rtm516 Date: Tue, 23 Jul 2024 19:43:19 +0100 Subject: [PATCH 58/84] Update DeviceOs to latest protocol (#4553) * Update DeviceOs to latest protocol * Revert enum name change and add deprecation annotations --- .../org/geysermc/floodgate/util/DeviceOs.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java index 406204759..1a92f9c40 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java +++ b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2024 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 @@ -39,15 +39,19 @@ public enum DeviceOs { OSX("macOS"), AMAZON("Amazon"), GEARVR("Gear VR"), - HOLOLENS("Hololens"), + @Deprecated HOLOLENS("Hololens"), UWP("Windows"), WIN32("Windows x86"), DEDICATED("Dedicated"), - TVOS("Apple TV"), - PS4("PS4"), + @Deprecated TVOS("Apple TV"), + /** + * This is for all PlayStation platforms not just PS4 + */ + PS4("PlayStation"), NX("Switch"), - XBOX("Xbox One"), - WINDOWS_PHONE("Windows Phone"); + XBOX("Xbox"), + @Deprecated WINDOWS_PHONE("Windows Phone"), + LINUX("Linux"); private static final DeviceOs[] VALUES = values(); From f3ba5848c2b9fd187c4982bd449d894a837d469e Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 1 Aug 2024 00:11:13 +0200 Subject: [PATCH 59/84] Extensions should specify geyser api version in the extension.yml (#3880) * let extensions specify geyser api version instead of base api version * fix spacing, @link formatting, properly check for compat * Proper warning, update to API changes to also check patch version * Bump base-api version * adapt to new base api changes * Actually bump to 2.4.1 * Update api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java * Address reviews * Address reviews * Update to latest base api changes; proper extension *human* version checking * no need to apply a plugin, that's the default --------- Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> --- api/build.gradle.kts | 18 ++++++- .../org.geysermc.geyser.api/BuildData.java | 53 +++++++++++++++++++ .../org/geysermc/geyser/api/GeyserApi.java | 11 ++++ .../api/extension/ExtensionDescription.java | 37 ++++++++----- .../extension/GeyserExtensionDescription.java | 10 ++-- .../extension/GeyserExtensionLoader.java | 40 +++++++++----- gradle.properties | 2 +- gradle/libs.versions.toml | 2 +- 8 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 api/src/main/java-templates/org.geysermc.geyser.api/BuildData.java diff --git a/api/build.gradle.kts b/api/build.gradle.kts index bd54a9ce4..eac02ebeb 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,8 +1,24 @@ plugins { + // Allow blossom to mark sources root of templates + idea id("geyser.publish-conventions") + alias(libs.plugins.blossom) } dependencies { api(libs.base.api) api(libs.math) -} \ No newline at end of file +} + +version = property("version")!! +val apiVersion = (version as String).removeSuffix("-SNAPSHOT") + +sourceSets { + main { + blossom { + javaSources { + property("version", apiVersion) + } + } + } +} diff --git a/api/src/main/java-templates/org.geysermc.geyser.api/BuildData.java b/api/src/main/java-templates/org.geysermc.geyser.api/BuildData.java new file mode 100644 index 000000000..f9a580e7b --- /dev/null +++ b/api/src/main/java-templates/org.geysermc.geyser.api/BuildData.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 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.geyser.api; + +import org.geysermc.api.util.ApiVersion; + +/** + * Not a public API. For internal use only. May change without notice. + * This class is processed before compilation to insert build properties. + */ +class BuildData { + static final String VERSION = "{{ version }}"; + static final ApiVersion API_VERSION; + + static { + String[] parts = VERSION.split("\\."); + if (parts.length != 3) { + throw new RuntimeException("Invalid api version: " + VERSION); + } + + try { + int human = Integer.parseInt(parts[0]); + int major = Integer.parseInt(parts[1]); + int minor = Integer.parseInt(parts[2]); + API_VERSION = new ApiVersion(human, major, minor); + } catch (Exception e) { + throw new RuntimeException("Invalid api version: " + VERSION, e); + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java index a9327d0db..5c20d06e1 100644 --- a/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java +++ b/api/src/main/java/org/geysermc/geyser/api/GeyserApi.java @@ -29,6 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.api.Geyser; import org.geysermc.api.GeyserApiBase; +import org.geysermc.api.util.ApiVersion; import org.geysermc.geyser.api.command.CommandSource; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.EventBus; @@ -169,4 +170,14 @@ public interface GeyserApi extends GeyserApiBase { static GeyserApi api() { return Geyser.api(GeyserApi.class); } + + /** + * Returns the {@link ApiVersion} representing the current Geyser api version. + * See the Geyser version outline) + * + * @return the current geyser api version + */ + default ApiVersion geyserApiVersion() { + return BuildData.API_VERSION; + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java index 2df3ee815..25daf450f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java +++ b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java @@ -59,33 +59,46 @@ public interface ExtensionDescription { String main(); /** - * Gets the extension's major api version + * Represents the human api version that the extension requires. + * See the Geyser version outline) + * for more details on the Geyser API version. * - * @return the extension's major api version + * @return the extension's requested human api version + */ + int humanApiVersion(); + + /** + * Represents the major api version that the extension requires. + * See the Geyser version outline) + * for more details on the Geyser API version. + * + * @return the extension's requested major api version */ int majorApiVersion(); /** - * Gets the extension's minor api version + * Represents the minor api version that the extension requires. + * See the Geyser version outline) + * for more details on the Geyser API version. * - * @return the extension's minor api version + * @return the extension's requested minor api version */ int minorApiVersion(); /** - * Gets the extension's patch api version - * - * @return the extension's patch api version + * No longer in use. Geyser is now using an adaption of the romantic versioning scheme. + * See here for details. */ - int patchApiVersion(); + @Deprecated(forRemoval = true) + default int patchApiVersion() { + return minorApiVersion(); + } /** - * Gets the extension's api version. - * - * @return the extension's api version + * Returns the extension's requested Geyser Api version. */ default String apiVersion() { - return majorApiVersion() + "." + minorApiVersion() + "." + patchApiVersion(); + return humanApiVersion() + "." + majorApiVersion() + "." + minorApiVersion(); } /** diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java index 239ffc450..a84f12813 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java @@ -43,9 +43,9 @@ import java.util.regex.Pattern; public record GeyserExtensionDescription(@NonNull String id, @NonNull String name, @NonNull String main, + int humanApiVersion, int majorApiVersion, int minorApiVersion, - int patchApiVersion, @NonNull String version, @NonNull List authors) implements ExtensionDescription { @@ -82,9 +82,9 @@ public record GeyserExtensionDescription(@NonNull String id, throw new InvalidDescriptionException(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion)); } String[] api = apiVersion.split("\\."); - int majorApi = Integer.parseUnsignedInt(api[0]); - int minorApi = Integer.parseUnsignedInt(api[1]); - int patchApi = Integer.parseUnsignedInt(api[2]); + int humanApi = Integer.parseUnsignedInt(api[0]); + int majorApi = Integer.parseUnsignedInt(api[1]); + int minorApi = Integer.parseUnsignedInt(api[2]); List authors = new ArrayList<>(); if (source.author != null) { @@ -94,7 +94,7 @@ public record GeyserExtensionDescription(@NonNull String id, authors.addAll(source.authors); } - return new GeyserExtensionDescription(id, name, main, majorApi, minorApi, patchApi, version, authors); + return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors); } @NonNull diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java index 2f0ff1580..a56e00671 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java @@ -29,10 +29,15 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.RequiredArgsConstructor; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.api.Geyser; +import org.geysermc.api.util.ApiVersion; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.event.ExtensionEventBus; -import org.geysermc.geyser.api.extension.*; +import org.geysermc.geyser.api.extension.Extension; +import org.geysermc.geyser.api.extension.ExtensionDescription; +import org.geysermc.geyser.api.extension.ExtensionLoader; +import org.geysermc.geyser.api.extension.ExtensionLogger; +import org.geysermc.geyser.api.extension.ExtensionManager; import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; import org.geysermc.geyser.extension.event.GeyserExtensionEventBus; @@ -40,7 +45,12 @@ import org.geysermc.geyser.text.GeyserLocale; import java.io.IOException; import java.io.Reader; -import java.nio.file.*; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -176,16 +186,22 @@ public class GeyserExtensionLoader extends ExtensionLoader { return; } - // Completely different API version - if (description.majorApiVersion() != Geyser.api().majorApiVersion()) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); - return; - } + // Check whether an extensions' requested api version is compatible + ApiVersion.Compatibility compatibility = GeyserApi.api().geyserApiVersion().supportsRequestedVersion( + description.humanApiVersion(), + description.majorApiVersion(), + description.minorApiVersion() + ); - // If the extension requires new API features, being backwards compatible - if (description.minorApiVersion() > Geyser.api().minorApiVersion()) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); - return; + if (compatibility != ApiVersion.Compatibility.COMPATIBLE) { + // Workaround for the switch to the Geyser API version instead of the Base API version in extensions + if (compatibility == ApiVersion.Compatibility.HUMAN_DIFFER && description.humanApiVersion() == 1) { + GeyserImpl.getInstance().getLogger().warning("The extension %s requested the Base API version %s, which is deprecated in favor of specifying the Geyser API version. Please update the extension, or contact its developer." + .formatted(name, description.apiVersion())); + } else { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); + return; + } } GeyserExtensionContainer container = this.loadExtension(path, description); diff --git a/gradle.properties b/gradle.properties index a222b1d99..10d236a1b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,5 +7,5 @@ org.gradle.vfs.watch=false group=org.geysermc id=geyser -version=2.4.0-SNAPSHOT +version=2.4.1-SNAPSHOT description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f4abe18a9..b002c448c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -base-api = "1.0.0-SNAPSHOT" +base-api = "1.0.1-SNAPSHOT" cumulus = "1.1.2" erosion = "1.1-20240515.191456-1" events = "1.1-SNAPSHOT" From 8e3977810690e301772b6ac5083868cccf584483 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 1 Aug 2024 00:59:28 +0200 Subject: [PATCH 60/84] Target 1.0.1 release of the base api --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b002c448c..7a81ed923 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -base-api = "1.0.1-SNAPSHOT" +base-api = "1.0.1" cumulus = "1.1.2" erosion = "1.1-20240515.191456-1" events = "1.1-SNAPSHOT" From 5019b5aded85e9a938b27b3431fe551d9cbc8851 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:22:56 -0500 Subject: [PATCH 61/84] Fix Geyser Api BuildData directory --- .../geysermc/geyser/api}/BuildData.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/src/main/java-templates/{org.geysermc.geyser.api => org/geysermc/geyser/api}/BuildData.java (100%) diff --git a/api/src/main/java-templates/org.geysermc.geyser.api/BuildData.java b/api/src/main/java-templates/org/geysermc/geyser/api/BuildData.java similarity index 100% rename from api/src/main/java-templates/org.geysermc.geyser.api/BuildData.java rename to api/src/main/java-templates/org/geysermc/geyser/api/BuildData.java From 95c6f7c9cf9779205588fee5f0f1f42080a83e41 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Thu, 1 Aug 2024 01:18:49 +0000 Subject: [PATCH 62/84] Add advancement progress tracker (#4568) * Fix fetching advancements with invalid parents * Add progress tracker to advancements * Use Java language key for progress counter --- .../geyser/level/GeyserAdvancement.java | 12 ++-- .../session/cache/AdvancementsCache.java | 56 ++++++++++++++----- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java b/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java index 7d48b90af..7dad1639b 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java +++ b/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java @@ -82,11 +82,15 @@ public class GeyserAdvancement { this.rootId = this.advancement.getId(); } else { // Go through our cache, and descend until we find the root ID - GeyserAdvancement advancement = advancementsCache.getStoredAdvancements().get(this.advancement.getParentId()); - if (advancement.getParentId() == null) { - this.rootId = advancement.getId(); + GeyserAdvancement parent = advancementsCache.getStoredAdvancements().get(this.advancement.getParentId()); + if (parent == null) { + // Parent doesn't exist, is invalid, or couldn't be found for another reason + // So assuming there is no parent and this is the root + this.rootId = this.advancement.getId(); + } else if (parent.getParentId() == null) { + this.rootId = parent.getId(); } else { - this.rootId = advancement.getRootId(advancementsCache); + this.rootId = parent.getRootId(advancementsCache); } } } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java index be1eb3a5b..ac04bdf04 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java @@ -158,7 +158,15 @@ public class AdvancementsCache { // Cache language for easier access String language = session.locale(); - String earned = isEarned(advancement) ? "yes" : "no"; + boolean advancementHasProgress = advancement.getRequirements().size() > 1; + + int advancementProgress = getProgress(advancement); + int advancementRequirements = advancement.getRequirements().size(); + + boolean advancementEarned = advancementRequirements > 0 + && advancementProgress >= advancementRequirements; + + String earned = advancementEarned ? "yes" : "no"; String description = getColorFromAdvancementFrameType(advancement) + MessageTranslator.convertMessage(advancement.getDisplayData().getDescription(), language); String earnedString = GeyserLocale.getPlayerLocaleString("geyser.advancements.earned", language, MinecraftLocale.getLocaleString("gui." + earned, language)); @@ -171,10 +179,20 @@ public class AdvancementsCache { (Description) Mine stone with your new pickaxe Earned: Yes + Progress: 1/4 // When advancement has multiple requirements Parent Advancement: Minecraft // If relevant */ String content = description + "\n\n§f" + earnedString + "\n"; + + if (advancementHasProgress) { + // Only display progress with multiple requirements + String progress = MinecraftLocale.getLocaleString("advancements.progress", language) + .replaceFirst("%s", String.valueOf(advancementProgress)) + .replaceFirst("%s", String.valueOf(advancementRequirements)); + content += GeyserLocale.getPlayerLocaleString("geyser.advancements.progress", language, progress) + "\n"; + } + if (!currentAdvancementCategoryId.equals(advancement.getParentId())) { // Only display the parent if it is not the category content += GeyserLocale.getPlayerLocaleString("geyser.advancements.parentid", language, MessageTranslator.convertMessage(storedAdvancements.get(advancement.getParentId()).getDisplayData().getTitle(), language)); @@ -200,34 +218,44 @@ public class AdvancementsCache { * @return true if the advancement has been earned. */ public boolean isEarned(GeyserAdvancement advancement) { - boolean earned = false; - if (advancement.getRequirements().size() == 0) { + if (advancement.getRequirements().isEmpty()) { // Minecraft handles this case, so we better as well return false; } - Map progress = storedAdvancementProgress.get(advancement.getId()); - if (progress != null) { + // Progress should never be above requirements count, but you never know + return getProgress(advancement) >= advancement.getRequirements().size(); + } + + /** + * Determine the progress on an advancement. + * + * @param advancement the advancement to determine + * @return the progress on the advancement. + */ + public int getProgress(GeyserAdvancement advancement) { + if (advancement.getRequirements().isEmpty()) { + // Minecraft handles this case + return 0; + } + int progress = 0; + Map progressMap = storedAdvancementProgress.get(advancement.getId()); + if (progressMap != null) { // Each advancement's requirement must be fulfilled // For example, [[zombie, blaze, skeleton]] means that one of those three categories must be achieved // But [[zombie], [blaze], [skeleton]] means that all three requirements must be completed for (List requirements : advancement.getRequirements()) { - boolean requirementsDone = false; for (String requirement : requirements) { - Long obtained = progress.get(requirement); + Long obtained = progressMap.get(requirement); // -1 means that this particular component required for completing the advancement // has yet to be fulfilled if (obtained != null && !obtained.equals(-1L)) { - requirementsDone = true; - break; + progress++; } } - if (!requirementsDone) { - return false; - } } - earned = true; } - return earned; + + return progress; } public String getColorFromAdvancementFrameType(GeyserAdvancement advancement) { From 3d7e62a408b2b4a6f86430e940a0219c5b595fa0 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:35:03 -0400 Subject: [PATCH 63/84] Fix some server switching issues and GeyserConnect --- .../type/player/SessionPlayerEntity.java | 2 +- .../geysermc/geyser/level/JavaDimension.java | 5 ++++- .../geyser/session/GeyserSession.java | 10 ++++++++- .../geyser/session/cache/ChunkCache.java | 14 ++---------- .../protocol/java/JavaLoginTranslator.java | 22 ++++++++----------- .../JavaHorseScreenOpenTranslator.java | 6 ++++- .../JavaLevelChunkWithLightTranslator.java | 4 ++-- .../org/geysermc/geyser/util/ChunkUtils.java | 6 ++--- .../geysermc/geyser/util/DimensionUtils.java | 2 +- .../geysermc/geyser/util/InventoryUtils.java | 2 +- 10 files changed, 37 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index dc0545cee..b924461af 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -321,7 +321,7 @@ public class SessionPlayerEntity extends PlayerEntity { public int voidFloorPosition() { // The void floor is offset about 40 blocks below the bottom of the world - BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); + BedrockDimension bedrockDimension = session.getBedrockDimension(); return bedrockDimension.minY() - 40; } diff --git a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java index 7462844fc..0ca428830 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java +++ b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java @@ -34,6 +34,9 @@ import org.geysermc.geyser.util.DimensionUtils; * Represents the information we store from the current Java dimension * @param piglinSafe Whether piglins and hoglins are safe from conversion in this dimension. * This controls if they have the shaking effect applied in the dimension. + * @param bedrockId the Bedrock dimension ID of this dimension. + * As a Java dimension can be null in some login cases (e.g. GeyserConnect), make sure the player + * is logged in before utilizing this field. */ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale, int bedrockId, boolean isNetherLike) { @@ -46,7 +49,7 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double world // Set if piglins/hoglins should shake boolean piglinSafe = dimension.getBoolean("piglin_safe"); // Load world coordinate scale for the world border - double coordinateScale = dimension.getDouble("coordinate_scale"); + double coordinateScale = dimension.getNumber("coordinate_scale").doubleValue(); // FIXME see if we can change this in the NBT library itself. boolean isNetherLike; // Cache the Bedrock version of this dimension, and base it off the ID - THE ID CAN CHANGE!!! diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 60321ea75..9a990865e 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -137,6 +137,7 @@ import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.BlockItem; +import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.physics.CollisionManager; import org.geysermc.geyser.network.netty.LocalSession; @@ -386,6 +387,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @MonotonicNonNull @Setter private JavaDimension dimensionType = null; + /** + * Which dimension Bedrock understands themselves to be in. + * This should only be set after the ChangeDimensionPacket is sent, or + * right before the StartGamePacket is sent. + */ + @Setter + private BedrockDimension bedrockDimension = BedrockDimension.OVERWORLD; @Setter private int breakingBlock; @@ -1547,7 +1555,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.setRotation(Vector2f.from(1, 1)); startGamePacket.setSeed(-1L); - startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(chunkCache.getBedrockDimension())); + startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(bedrockDimension)); startGamePacket.setGeneratorId(1); startGamePacket.setLevelGameType(GameType.SURVIVAL); startGamePacket.setDifficulty(1); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java index 7b279857a..ad5237c23 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java @@ -25,17 +25,14 @@ package org.geysermc.geyser.session.cache; -import org.geysermc.geyser.level.block.type.Block; -import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import lombok.Getter; import lombok.Setter; -import org.geysermc.geyser.level.BedrockDimension; -import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.chunk.GeyserChunk; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette; public class ChunkCache { private final boolean cache; @@ -46,13 +43,6 @@ public class ChunkCache { @Setter private int heightY; - /** - * Which dimension Bedrock understands themselves to be in. - */ - @Getter - @Setter - private BedrockDimension bedrockDimension = BedrockDimension.OVERWORLD; - public ChunkCache(GeyserSession session) { this.cache = !session.getGeyser().getWorldManager().hasOwnChunkCache(); // To prevent Spigot from initializing chunks = cache ? new Long2ObjectOpenHashMap<>() : null; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java index cf4b7058b..a6d6e6c70 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java @@ -64,14 +64,17 @@ public class JavaLoginTranslator extends PacketTranslator> 4) - 1; int sectionCount; @@ -509,7 +509,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator entry : session.getItemFrameCache().entrySet()) { diff --git a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java index 2e7df51bd..288b425ba 100644 --- a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java @@ -149,7 +149,7 @@ public class ChunkUtils { } public static void sendEmptyChunk(GeyserSession session, int chunkX, int chunkZ, boolean forceUpdate) { - BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); + BedrockDimension bedrockDimension = session.getBedrockDimension(); int bedrockSubChunkCount = bedrockDimension.height() >> 4; byte[] payload; @@ -167,7 +167,7 @@ public class ChunkUtils { byteBuf.readBytes(payload); LevelChunkPacket data = new LevelChunkPacket(); - data.setDimension(DimensionUtils.javaToBedrock(session.getChunkCache().getBedrockDimension())); + data.setDimension(DimensionUtils.javaToBedrock(session.getBedrockDimension())); data.setChunkX(chunkX); data.setChunkZ(chunkZ); data.setSubChunksLength(0); @@ -214,7 +214,7 @@ public class ChunkUtils { throw new RuntimeException("Maximum Y must be a multiple of 16!"); } - BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); + BedrockDimension bedrockDimension = session.getBedrockDimension(); // Yell in the console if the world height is too height in the current scenario // The constraints change depending on if the player is in the overworld or not, and if experimental height is enabled // (Ignore this for the Nether. We can't change that at the moment without the workaround. :/ ) diff --git a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java index 821358bd8..f043631b6 100644 --- a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java @@ -179,7 +179,7 @@ public class DimensionUtils { } public static void setBedrockDimension(GeyserSession session, int bedrockDimension) { - session.getChunkCache().setBedrockDimension(switch (bedrockDimension) { + session.setBedrockDimension(switch (bedrockDimension) { case BEDROCK_END_ID -> BedrockDimension.THE_END; case BEDROCK_DEFAULT_NETHER_ID -> BedrockDimension.THE_NETHER; // JavaDimension *should* be set to BEDROCK_END_ID if the Nether workaround is enabled. default -> BedrockDimension.OVERWORLD; diff --git a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java index b0bfffc19..d8c41d626 100644 --- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java @@ -159,7 +159,7 @@ public class InventoryUtils { @Nullable public static Vector3i findAvailableWorldSpace(GeyserSession session) { // Check if a fake block can be placed, either above the player or beneath. - BedrockDimension dimension = session.getChunkCache().getBedrockDimension(); + BedrockDimension dimension = session.getBedrockDimension(); int minY = dimension.minY(), maxY = minY + dimension.height(); Vector3i flatPlayerPosition = session.getPlayerEntity().getPosition().toInt(); Vector3i position = flatPlayerPosition.add(Vector3i.UP); From 61ae5debd4527875a5dc0bff912c029f2501a1b1 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Sat, 3 Aug 2024 10:23:06 -0500 Subject: [PATCH 64/84] Allow dumps to be created even if GeyserServer failed to start (#4930) --- .../geyser/command/defaults/DumpCommand.java | 50 ++++++++++--------- .../org/geysermc/geyser/dump/DumpInfo.java | 22 ++++---- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java index 45100f525..fc46f0108 100644 --- a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java @@ -63,31 +63,31 @@ public class DumpCommand extends GeyserCommand { this.geyser = geyser; } - @Override - public void register(CommandManager manager) { - manager.command(baseBuilder(manager) - .optional(ARGUMENTS, stringArrayParser(), SuggestionProvider.blockingStrings((ctx, input) -> { - // parse suggestions here - List inputs = new ArrayList<>(); - while (input.hasRemainingInput()) { - inputs.add(input.readStringSkipWhitespace()); - } + @Override + public void register(CommandManager manager) { + manager.command(baseBuilder(manager) + .optional(ARGUMENTS, stringArrayParser(), SuggestionProvider.blockingStrings((ctx, input) -> { + // parse suggestions here + List inputs = new ArrayList<>(); + while (input.hasRemainingInput()) { + inputs.add(input.readStringSkipWhitespace()); + } - if (inputs.size() <= 2) { - return SUGGESTIONS; // only `geyser dump` was typed (2 literals) - } + if (inputs.size() <= 2) { + return SUGGESTIONS; // only `geyser dump` was typed (2 literals) + } - // the rest of the input after `geyser dump` is for this argument - inputs = inputs.subList(2, inputs.size()); + // the rest of the input after `geyser dump` is for this argument + inputs = inputs.subList(2, inputs.size()); - // don't suggest any words they have already typed - List suggestions = new ArrayList<>(); - SUGGESTIONS.forEach(suggestions::add); - suggestions.removeAll(inputs); - return suggestions; - })) - .handler(this::execute)); - } + // don't suggest any words they have already typed + List suggestions = new ArrayList<>(); + SUGGESTIONS.forEach(suggestions::add); + suggestions.removeAll(inputs); + return suggestions; + })) + .handler(this::execute)); + } @Override public void execute(CommandContext context) { @@ -113,13 +113,15 @@ public class DumpCommand extends GeyserCommand { source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", source.locale())); String dumpData; try { + DumpInfo dump = new DumpInfo(geyser, addLog); + if (offlineDump) { DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); // Make arrays easier to read prettyPrinter.indentArraysWith(new DefaultIndenter(" ", "\n")); - dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(new DumpInfo(addLog)); + dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(dump); } else { - dumpData = MAPPER.writeValueAsString(new DumpInfo(addLog)); + dumpData = MAPPER.writeValueAsString(dump); } } catch (IOException e) { source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", source.locale())); diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java index 6989dc10a..515e1a629 100644 --- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java +++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java @@ -81,7 +81,7 @@ public class DumpInfo { private final FlagsInfo flagsInfo; private final List extensionInfo; - public DumpInfo(boolean addLog) { + public DumpInfo(GeyserImpl geyser, boolean addLog) { this.versionInfo = new VersionInfo(); this.cpuCount = Runtime.getRuntime().availableProcessors(); @@ -91,7 +91,7 @@ public class DumpInfo { this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY); - this.config = GeyserImpl.getInstance().getConfig(); + this.config = geyser.getConfig(); this.floodgate = new Floodgate(); String md5Hash = "unknown"; @@ -107,7 +107,7 @@ public class DumpInfo { //noinspection UnstableApiUsage sha256Hash = byteSource.hash(Hashing.sha256()).toString(); } catch (Exception e) { - if (GeyserImpl.getInstance().getConfig().isDebugMode()) { + if (this.config.isDebugMode()) { e.printStackTrace(); } } @@ -116,18 +116,22 @@ public class DumpInfo { this.ramInfo = new RamInfo(); if (addLog) { - this.logsInfo = new LogsInfo(); + this.logsInfo = new LogsInfo(geyser); } this.userPlatforms = new Object2IntOpenHashMap<>(); - for (GeyserSession session : GeyserImpl.getInstance().getSessionManager().getAllSessions()) { + for (GeyserSession session : geyser.getSessionManager().getAllSessions()) { DeviceOs device = session.getClientData().getDeviceOs(); userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1); } - this.connectionAttempts = GeyserImpl.getInstance().getGeyserServer().getConnectionAttempts(); + if (geyser.getGeyserServer() != null) { + this.connectionAttempts = geyser.getGeyserServer().getConnectionAttempts(); + } else { + this.connectionAttempts = 0; // Fallback if Geyser failed to fully startup + } - this.bootstrapInfo = GeyserImpl.getInstance().getBootstrap().getDumpInfo(); + this.bootstrapInfo = geyser.getBootstrap().getDumpInfo(); this.flagsInfo = new FlagsInfo(); @@ -244,10 +248,10 @@ public class DumpInfo { public static class LogsInfo { private String link; - public LogsInfo() { + public LogsInfo(GeyserImpl geyser) { try { Map fields = new HashMap<>(); - fields.put("content", FileUtils.readAllLines(GeyserImpl.getInstance().getBootstrap().getLogsPath()).collect(Collectors.joining("\n"))); + fields.put("content", FileUtils.readAllLines(geyser.getBootstrap().getLogsPath()).collect(Collectors.joining("\n"))); JsonNode logData = GeyserImpl.JSON_MAPPER.readTree(WebUtils.postForm("https://api.mclo.gs/1/log", fields)); From 523bcdc095a1fb6bf6f6bccca033418a5ad7d92a Mon Sep 17 00:00:00 2001 From: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> Date: Sun, 4 Aug 2024 22:00:15 -0700 Subject: [PATCH 65/84] Specify 1.21.2/1.21.3 support Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> --- .../src/main/java/org/geysermc/geyser/network/GameProtocol.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 18dee94e6..087ecf5cc 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -72,7 +72,7 @@ public final class GameProtocol { .minecraftVersion("1.21.0/1.21.1") .build())); SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v686.CODEC.toBuilder() - .minecraftVersion("1.21.2") + .minecraftVersion("1.21.2/1.21.3") .build())); SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } From ea6b0df9b57b209077198342ace7ddacf2b805bc Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:54:17 -0500 Subject: [PATCH 66/84] Remove GeyserImpl#shouldStartListener (#4935) --- .../java/org/geysermc/geyser/GeyserImpl.java | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 01f1a118e..5c08e34d7 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -156,12 +156,6 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { private final SessionManager sessionManager = new SessionManager(); - /** - * This is used in GeyserConnect to stop the bedrock server binding to a port - */ - @Setter - private static boolean shouldStartListener = true; - private FloodgateCipher cipher; private FloodgateSkinUploader skinUploader; private NewsHandler newsHandler; @@ -435,24 +429,22 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { bedrockThreadCount = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); } - if (shouldStartListener) { - this.geyserServer = new GeyserServer(this, bedrockThreadCount); - this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port())) - .whenComplete((avoid, throwable) -> { - if (throwable == null) { - logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().address(), - String.valueOf(config.getBedrock().port()))); - } else { - String address = config.getBedrock().address(); - int port = config.getBedrock().port(); - logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port))); - if (!"0.0.0.0".equals(address)) { - logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN)); - logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN)); - } + this.geyserServer = new GeyserServer(this, bedrockThreadCount); + this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port())) + .whenComplete((avoid, throwable) -> { + if (throwable == null) { + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().address(), + String.valueOf(config.getBedrock().port()))); + } else { + String address = config.getBedrock().address(); + int port = config.getBedrock().port(); + logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port))); + if (!"0.0.0.0".equals(address)) { + logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN)); + logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN)); } - }).join(); - } + } + }).join(); if (config.getRemote().authType() == AuthType.FLOODGATE) { try { From 83d8c19824c9fec4218a028d0d0e833f7abe13c4 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Tue, 6 Aug 2024 12:56:10 +0100 Subject: [PATCH 67/84] Make missing locale log as debug (#4940) --- core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java b/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java index 28fd6f9a4..b8867c356 100644 --- a/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java @@ -150,7 +150,7 @@ public class GeyserLocale { } else { if (!validLocalLanguage) { // Don't warn on missing locales if a local file has been found - bootstrap.getGeyserLogger().warning("Missing locale: " + locale); + bootstrap.getGeyserLogger().debug("Missing locale: " + locale); } } From 54c43f2b022f1be1fdd6bda2c3603372369c8c3c Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:36:34 -0500 Subject: [PATCH 68/84] Suppress address in bind log if it is 0.0.0.0 (#4160) Co-authored-by: onebeastchris --- .../main/java/org/geysermc/geyser/GeyserImpl.java | 15 ++++++++++----- core/src/main/resources/languages | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 5c08e34d7..8febf4d21 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -432,13 +432,18 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { this.geyserServer = new GeyserServer(this, bedrockThreadCount); this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port())) .whenComplete((avoid, throwable) -> { + String address = config.getBedrock().address(); + String port = String.valueOf(config.getBedrock().port()); // otherwise we get commas + if (throwable == null) { - logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().address(), - String.valueOf(config.getBedrock().port()))); + if ("0.0.0.0".equals(address)) { + // basically just hide it in the log because some people get confused and try to change it + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start.ip_suppressed", port)); + } else { + logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", address, port)); + } } else { - String address = config.getBedrock().address(); - int port = config.getBedrock().port(); - logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port))); + logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, port)); if (!"0.0.0.0".equals(address)) { logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN)); logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN)); diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index 60b20023a..a943a1bb9 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 60b20023a92f084aba895ab0336e70fa7fb311fb +Subproject commit a943a1bb910f58caa61f14bafacbc622bd48a694 From 069d35c6422a05a74f960d2fdb5d2788823ff722 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:08:27 -0400 Subject: [PATCH 69/84] Likely fix for #2573 Tested working on Paper 1.21 --- .../translator/protocol/java/JavaCommandsTranslator.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java index 4c817ba01..01da23809 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCommandsTranslator.java @@ -76,6 +76,9 @@ public class JavaCommandsTranslator extends PacketTranslator Date: Tue, 6 Aug 2024 22:09:01 -0400 Subject: [PATCH 70/84] New files for .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a44afd242..aff61aa60 100644 --- a/.gitignore +++ b/.gitignore @@ -249,6 +249,8 @@ locales/ /packs/ /dump.json /saved-refresh-tokens.json +/saved-auth-chains.json /custom_mappings/ /languages/ -/custom-skulls.yml \ No newline at end of file +/custom-skulls.yml +/permissions.yml From 86d0a4720631513c0446558bb3bd53a121050eb8 Mon Sep 17 00:00:00 2001 From: RK_01 <50594595+RaphiMC@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:25:06 +0200 Subject: [PATCH 71/84] Fix floodgate not working with the default config (#4951) --- .../geyser/platform/viaproxy/GeyserViaProxyPlugin.java | 3 +++ bootstrap/viaproxy/src/main/resources/viaproxy.yml | 2 +- gradle/libs.versions.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java index 1eed778f2..5551b9755 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java @@ -155,6 +155,9 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst // Only initialize the ping passthrough if the protocol version is above beta 1.7.3, as that's when the status protocol was added this.pingPassthrough = GeyserLegacyPingPassthrough.init(this.geyser); } + if (this.config.getRemote().authType() == AuthType.FLOODGATE) { + ViaProxy.getConfig().setPassthroughBungeecordPlayerInfo(true); + } } @Override diff --git a/bootstrap/viaproxy/src/main/resources/viaproxy.yml b/bootstrap/viaproxy/src/main/resources/viaproxy.yml index 66fbdb932..89fc612cd 100644 --- a/bootstrap/viaproxy/src/main/resources/viaproxy.yml +++ b/bootstrap/viaproxy/src/main/resources/viaproxy.yml @@ -2,4 +2,4 @@ name: "${name}-ViaProxy" version: "${version}" author: "${author}" main: "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyPlugin" -min-version: "3.2.1" +min-version: "3.3.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a81ed923..2ed67e96c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ cloud-minecraft-modded = "2.0.0-beta.7" commodore = "2.2" bungeecord = "a7c6ede" velocity = "3.3.0-SNAPSHOT" -viaproxy = "3.2.1" +viaproxy = "3.3.2-SNAPSHOT" fabric-loader = "0.15.11" fabric-api = "0.100.1+1.21" neoforge-minecraft = "21.0.0-beta" From f5b7cc725b9bdb8ecb2e554947fed10e0cc360a1 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:55:14 -0400 Subject: [PATCH 72/84] Fix mangrove propagule age (#4949) --- core/src/main/resources/mappings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 597dcd3a7..698fd2b10 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 597dcd3a78d0896638788f4b966eaa8554cf0b43 +Subproject commit 698fd2b108a9e53f1e47b8cfdc122651b70d6059 From ee0b34e49033feda757f5e1a72e6a87211514476 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 9 Aug 2024 02:15:08 +0200 Subject: [PATCH 73/84] Indicate 1.21.1 Java support - Indicate 1.21.1 support on modrinth/in the README.md - Add all supported versions of Geyser-Spigot to modrinth (#4952) --- README.md | 2 +- bootstrap/spigot/build.gradle.kts | 2 ++ .../kotlin/geyser.modrinth-uploading-conventions.gradle.kts | 4 ++-- gradle/libs.versions.toml | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8eac49a24..bc60a1847 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,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 joined us here! ## Supported Versions -Geyser is currently supporting Minecraft Bedrock 1.20.80 - 1.21.3 and Minecraft Java Server 1.21. For more info please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.20.80 - 1.21.3 and Minecraft Java Server 1.21/1.21.1. For more info please see [here](https://geysermc.org/wiki/geyser/supported-versions/). ## Setting Up Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser. diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index 0a1271145..f680b1949 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -81,5 +81,7 @@ tasks.withType { modrinth { uploadFile.set(tasks.getByPath("shadowJar")) + gameVersions.addAll("1.16.5", "1.17", "1.17.1", "1.18", "1.18.1", "1.18.2", "1.19", + "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6") loaders.addAll("spigot", "paper") } diff --git a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts index d710ae1a2..fe2284137 100644 --- a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts @@ -11,8 +11,8 @@ modrinth { versionNumber.set(project.version as String + "-" + System.getenv("BUILD_NUMBER")) versionType.set("beta") changelog.set(System.getenv("CHANGELOG") ?: "") - gameVersions.add(libs.minecraft.get().version as String) + gameVersions.addAll("1.21", libs.minecraft.get().version as String) failSilently.set(true) syncBodyFrom.set(rootProject.file("README.md").readText()) -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ed67e96c..b141d9989 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,10 +33,10 @@ velocity = "3.3.0-SNAPSHOT" viaproxy = "3.3.2-SNAPSHOT" fabric-loader = "0.15.11" fabric-api = "0.100.1+1.21" -neoforge-minecraft = "21.0.0-beta" +neoforge-minecraft = "21.1.1" mixin = "0.8.5" mixinextras = "0.3.5" -minecraft = "1.21" +minecraft = "1.21.1" # plugin versions indra = "3.1.3" From cd897feb1b60bcad6362a3027c95cad84b179441 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 9 Aug 2024 11:35:25 +0200 Subject: [PATCH 74/84] Unify repository definition (#4953) * Unify repository definition * Remove duplicate repo * Update build-logic/src/main/kotlin/geyser.build-logic.gradle.kts Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> --------- Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> --- .../main/kotlin/geyser.build-logic.gradle.kts | 45 ++++++++++++++++++ .../geyser.modded-conventions.gradle.kts | 10 +--- settings.gradle.kts | 46 ------------------- 3 files changed, 46 insertions(+), 55 deletions(-) diff --git a/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts b/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts index e69de29bb..b6168507e 100644 --- a/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts @@ -0,0 +1,45 @@ +repositories { + // mavenLocal() + + mavenCentral() + + // Floodgate, Cumulus etc. + maven("https://repo.opencollab.dev/main") + + // Paper, Velocity + maven("https://repo.papermc.io/repository/maven-public") + + // Spigot + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") { + mavenContent { snapshotsOnly() } + } + + // BungeeCord + maven("https://oss.sonatype.org/content/repositories/snapshots") { + mavenContent { snapshotsOnly() } + } + + // NeoForge + maven("https://maven.neoforged.net/releases") { + mavenContent { releasesOnly() } + } + + // Minecraft + maven("https://libraries.minecraft.net") { + name = "minecraft" + mavenContent { releasesOnly() } + } + + // ViaVersion + maven("https://repo.viaversion.com") { + name = "viaversion" + } + + // Jitpack for e.g. MCPL + maven("https://jitpack.io") { + content { includeGroupByRegex("com\\.github\\..*") } + } + + // For Adventure snapshots + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") +} diff --git a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts index 20d14c443..8a6602778 100644 --- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts @@ -5,6 +5,7 @@ import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.maven plugins { + id("geyser.build-logic") id("geyser.publish-conventions") id("architectury-plugin") id("dev.architectury.loom") @@ -116,12 +117,3 @@ dependencies { minecraft(libs.minecraft) mappings(loom.officialMojangMappings()) } - -repositories { - // mavenLocal() - maven("https://repo.opencollab.dev/main") - maven("https://jitpack.io") - maven("https://oss.sonatype.org/content/repositories/snapshots/") - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") - maven("https://maven.neoforged.net/releases") -} diff --git a/settings.gradle.kts b/settings.gradle.kts index a39bfa3d2..9aaf6ba59 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,52 +2,6 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -dependencyResolutionManagement { - repositories { - // mavenLocal() - - // Floodgate, Cumulus etc. - maven("https://repo.opencollab.dev/main") - - // Paper, Velocity - maven("https://repo.papermc.io/repository/maven-public") - // Spigot - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") { - mavenContent { snapshotsOnly() } - } - - // BungeeCord - maven("https://oss.sonatype.org/content/repositories/snapshots") { - mavenContent { snapshotsOnly() } - } - - // NeoForge - maven("https://maven.neoforged.net/releases") { - mavenContent { releasesOnly() } - } - - // Minecraft - maven("https://libraries.minecraft.net") { - name = "minecraft" - mavenContent { releasesOnly() } - } - - mavenCentral() - - // ViaVersion - maven("https://repo.viaversion.com") { - name = "viaversion" - } - - maven("https://jitpack.io") { - content { includeGroupByRegex("com\\.github\\..*") } - } - - // For Adventure snapshots - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") - } -} - pluginManagement { repositories { gradlePluginPortal() From 41e65b0fcc5d4c905b4c6bc21a25d3c7b464ba81 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 9 Aug 2024 12:53:32 +0200 Subject: [PATCH 75/84] Bump minecraftauth dependency (#4943) * Bump minecraftauth to snapshot build fixing rare issues with Geyser-Spigot --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b141d9989..b8c80d0bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ gson = "2.3.1" # Provided by Spigot 1.8.8 websocket = "1.5.1" protocol = "3.0.0.Beta2-20240704.153116-14" raknet = "1.0.0.CR3-20240416.144209-1" -minecraftauth = "4.1.0" +minecraftauth = "4.1.1-20240806.235051-7" mcprotocollib = "1.21-20240725.013034-16" adventure = "4.14.0" adventure-platform = "4.3.0" From d3ea65196bf4f75c4500830059d6a0612eba8599 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 11 Aug 2024 00:50:27 +0200 Subject: [PATCH 76/84] Feature: Detect incorrect proxy setups (#4941) * Feature: Detect & warn about incorrect proxy setups on Spigot platforms * Properly disable Geyser if we failed to load --- .../bungeecord/GeyserBungeePlugin.java | 6 +--- .../platform/mod/GeyserModBootstrap.java | 5 +++ .../platform/spigot/GeyserSpigotPlugin.java | 33 +++++++++++++++++-- .../velocity/GeyserVelocityPlugin.java | 4 +++ .../viaproxy/GeyserViaProxyPlugin.java | 4 +++ core/src/main/resources/languages | 2 +- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java index 1c0049231..e2735c80e 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java @@ -71,9 +71,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { private IGeyserPingPassthrough geyserBungeePingPassthrough; private GeyserImpl geyser; - // We can't disable the plugin; hence we need to keep track of it manually - private boolean disabled; - @Override public void onLoad() { onGeyserInitialize(); @@ -98,7 +95,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } if (!this.loadConfig()) { - disabled = true; return; } this.geyserLogger.setDebug(geyserConfig.isDebugMode()); @@ -112,7 +108,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { @Override public void onEnable() { - if (disabled) { + if (geyser == null) { return; // Config did not load properly! } // Big hack - Bungee does not provide us an event to listen to, so schedule a repeating diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java index f11b5fbd6..69d6dc9a4 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java @@ -89,6 +89,11 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap { } public void onGeyserEnable() { + // "Disabling" a mod isn't possible; so if we fail to initialize we need to manually stop here + if (geyser == null) { + return; + } + if (GeyserImpl.getInstance().isReloading()) { if (!loadConfig()) { return; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index 3bb44a4bc..a2d52ce5a 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -117,7 +117,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2")); geyserLogger.error(""); geyserLogger.error("*********************************************"); - Bukkit.getPluginManager().disablePlugin(this); return; } @@ -131,7 +130,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper")); geyserLogger.error(""); geyserLogger.error("*********************************************"); - Bukkit.getPluginManager().disablePlugin(this); return; } } @@ -144,10 +142,25 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { geyserLogger.error("This version of Spigot is using an outdated version of netty. Please use Paper instead!"); geyserLogger.error(""); geyserLogger.error("*********************************************"); - Bukkit.getPluginManager().disablePlugin(this); return; } + try { + // Check spigot config for BungeeCord mode + if (Bukkit.getServer().spigot().getConfig().getBoolean("settings.bungeecord")) { + warnInvalidProxySetups("BungeeCord"); + return; + } + + // Now: Check for velocity mode - deliberately after checking bungeecord because this is a paper only option + if (Bukkit.getServer().spigot().getPaperConfig().getBoolean("proxies.velocity.enabled")) { + warnInvalidProxySetups("Velocity"); + return; + } + } catch (NoSuchMethodError e) { + // no-op + } + if (!loadConfig()) { return; } @@ -162,6 +175,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { @Override public void onEnable() { + // Disabling the plugin in onLoad() is not supported; we need to manually stop here + if (geyser == null) { + return; + } + // Create command manager early so we can add Geyser extension commands var sourceConverter = new CommandSourceConverter<>( CommandSender.class, @@ -458,4 +476,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return true; } + + private void warnInvalidProxySetups(String platform) { + geyserLogger.error("*********************************************"); + geyserLogger.error(""); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy_backend", platform)); + geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.setup_guide", "https://geysermc.org/wiki/geyser/setup/")); + geyserLogger.error(""); + geyserLogger.error("*********************************************"); + } } diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java index 868cdbf8e..413355813 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java @@ -113,6 +113,10 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { @Override public void onGeyserEnable() { + // If e.g. the config failed to load, GeyserImpl was not loaded and we cannot start + if (geyser == null) { + return; + } if (GeyserImpl.getInstance().isReloading()) { if (!loadConfig()) { return; diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java index 5551b9755..b5e614468 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java @@ -132,6 +132,10 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst @Override public void onGeyserEnable() { + // If e.g. the config failed to load, GeyserImpl was not loaded and we cannot start + if (geyser == null) { + return; + } boolean reloading = geyser.isReloading(); if (reloading) { if (!this.loadConfig()) { diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index a943a1bb9..7499daf71 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit a943a1bb910f58caa61f14bafacbc622bd48a694 +Subproject commit 7499daf712ad6de70a07fba471b51b4ad92315c5 From 10281a839f13547f511005ef5304c07459a60be8 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Sun, 11 Aug 2024 01:58:31 +0200 Subject: [PATCH 77/84] Bump version to 2.4.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 10d236a1b..814529d6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,5 +7,5 @@ org.gradle.vfs.watch=false group=org.geysermc id=geyser -version=2.4.1-SNAPSHOT +version=2.4.2-SNAPSHOT description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers. From ce62824899e59990e7720fb4a557d172b6f075e6 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 12 Aug 2024 23:29:00 +0200 Subject: [PATCH 78/84] Feature: Add method to close forms in the API (#4957) * Add closeForm api method * Move version check to GameProtocol --- .../geyser/api/connection/GeyserConnection.java | 15 ++++++++++----- .../org/geysermc/geyser/network/GameProtocol.java | 4 ++++ .../geysermc/geyser/session/GeyserSession.java | 9 +++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index ba559a462..0a580f975 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -60,6 +60,16 @@ public interface GeyserConnection extends Connection, CommandSource { */ @NonNull EntityData entities(); + /** + * Returns the current ping of the connection. + */ + int ping(); + + /** + * Closes the currently open form on the client. + */ + void closeForm(); + /** * @param javaId the Java entity ID to look up. * @return a {@link GeyserEntity} if present in this connection's entity tracker. @@ -132,9 +142,4 @@ public interface GeyserConnection extends Connection, CommandSource { @Deprecated @NonNull Set fogEffects(); - - /** - * Returns the current ping of the connection. - */ - int ping(); } diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 087ecf5cc..422fa3d5a 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -97,6 +97,10 @@ public final class GameProtocol { return session.getUpstream().getProtocolVersion() < Bedrock_v685.CODEC.getProtocolVersion(); } + public static boolean isPre1_21_2(GeyserSession session) { + return session.getUpstream().getProtocolVersion() < Bedrock_v686.CODEC.getProtocolVersion(); + } + /** * Gets the {@link PacketCodec} for Minecraft: Java Edition. * diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 9a990865e..9137c4756 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -79,6 +79,7 @@ import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.cloudburstmc.protocol.bedrock.packet.BiomeDefinitionListPacket; import org.cloudburstmc.protocol.bedrock.packet.CameraPresetsPacket; import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket; +import org.cloudburstmc.protocol.bedrock.packet.ClientboundCloseFormPacket; import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; import org.cloudburstmc.protocol.bedrock.packet.CreativeContentPacket; import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket; @@ -140,6 +141,7 @@ import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.netty.LocalSession; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.BlockMappings; @@ -2114,6 +2116,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return (int) Math.floor(rakSessionCodec.getPing()); } + @Override + public void closeForm() { + if (!GameProtocol.isPre1_21_2(this)) { + sendUpstreamPacket(new ClientboundCloseFormPacket()); + } + } + public void addCommandEnum(String name, String enums) { softEnumPacket(name, SoftEnumUpdateType.ADD, enums); } From ee43ef836925716fdf8eab26befd405836c56259 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 13 Aug 2024 01:45:25 +0200 Subject: [PATCH 79/84] Disable the plugin if we failed to load on Spigot (#4960) --- .../geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java index a2d52ce5a..c52927a83 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java @@ -175,8 +175,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { @Override public void onEnable() { - // Disabling the plugin in onLoad() is not supported; we need to manually stop here + // Disabling the plugin in onLoad() is not supported; we need to manually stop here and disable ourselves if (geyser == null) { + Bukkit.getPluginManager().disablePlugin(this); return; } From 8f7d512073532cba3b761b99830ccbcf7a28cddc Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:42:20 -0400 Subject: [PATCH 80/84] Fix armor not being visible on 1.21.20 --- .../geysermc/geyser/entity/type/LivingEntity.java | 6 ++++++ .../java/entity/JavaSetEquipmentTranslator.java | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 2a1bc1188..1dfe02b09 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -74,6 +74,7 @@ public class LivingEntity extends Entity { protected ItemData chestplate = ItemData.AIR; protected ItemData leggings = ItemData.AIR; protected ItemData boots = ItemData.AIR; + protected ItemData body = ItemData.AIR; protected ItemData hand = ItemData.AIR; protected ItemData offhand = ItemData.AIR; @@ -112,6 +113,10 @@ public class LivingEntity extends Entity { this.chestplate = ItemTranslator.translateToBedrock(session, stack); } + public void setBody(ItemStack stack) { + this.body = ItemTranslator.translateToBedrock(session, stack); + } + public void setLeggings(ItemStack stack) { this.leggings = ItemTranslator.translateToBedrock(session, stack); } @@ -323,6 +328,7 @@ public class LivingEntity extends Entity { armorEquipmentPacket.setChestplate(chestplate); armorEquipmentPacket.setLeggings(leggings); armorEquipmentPacket.setBoots(boots); + armorEquipmentPacket.setBody(body); session.sendUpstreamPacket(armorEquipmentPacket); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEquipmentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEquipmentTranslator.java index 07dcced47..11178115a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEquipmentTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaSetEquipmentTranslator.java @@ -29,6 +29,7 @@ import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.skin.FakeHeadProvider; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -72,11 +73,19 @@ public class JavaSetEquipmentTranslator extends PacketTranslator { - // BODY is sent for llamas with a carpet equipped, as of 1.20.5 + case CHESTPLATE -> { livingEntity.setChestplate(stack); armorUpdated = true; } + case BODY -> { + // BODY is sent for llamas with a carpet equipped, as of 1.20.5 + if (GameProtocol.isPre1_21_2(session)) { + livingEntity.setChestplate(stack); + } else { + livingEntity.setBody(stack); + } + armorUpdated = true; + } case LEGGINGS -> { livingEntity.setLeggings(stack); armorUpdated = true; From 0bc39d5a191777fcded4d9435393c511a3f37f43 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 13 Aug 2024 22:05:40 +0200 Subject: [PATCH 81/84] Remove old config option (#4962) --- core/src/main/resources/config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index a5fe2072b..15d3a20a6 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -54,9 +54,6 @@ remote: # For plugin versions, it's recommended to keep the `address` field to "auto" so Floodgate support is automatically configured. # If Floodgate is installed and `address:` is set to "auto", then "auth-type: floodgate" will automatically be used. 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) From 4f7e9fca9cea213d5968401fdfc60a2495d6bec9 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:07:15 -0400 Subject: [PATCH 82/84] Update Protocol and fix item stack encoding --- .../geyser/translator/inventory/InventoryTranslator.java | 7 ++++--- gradle/libs.versions.toml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index ce1022936..546ebda19 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -29,6 +29,7 @@ import it.unimi.dsi.fastutil.ints.*; import lombok.AllArgsConstructor; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.*; @@ -894,11 +895,11 @@ public abstract class InventoryTranslator { List containerEntries = new ArrayList<>(); for (Map.Entry> entry : containerMap.entrySet()) { - containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), null)); + containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), new FullContainerName(entry.getKey(), 0))); } ItemStackResponseSlot cursorEntry = makeItemEntry(0, session.getPlayerInventory().getCursor()); - containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), null)); + containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), new FullContainerName(ContainerSlotType.CURSOR, 0))); return containerEntries; } @@ -952,4 +953,4 @@ public abstract class InventoryTranslator { TRANSFER, DONE } -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f46dfdaed..a4b274c80 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ netty-io-uring = "0.0.25.Final-SNAPSHOT" guava = "29.0-jre" gson = "2.3.1" # Provided by Spigot 1.8.8 websocket = "1.5.1" -protocol = "3.0.0.Beta3-20240726.112706-2" +protocol = "3.0.0.Beta3-20240814.133201-7" raknet = "1.0.0.CR3-20240416.144209-1" minecraftauth = "4.1.1-20240806.235051-7" mcprotocollib = "1.21-20240725.013034-16" From 34bab14860db0476129fb86280b3c9b69293e5c8 Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Thu, 15 Aug 2024 03:03:34 -0400 Subject: [PATCH 83/84] Emulate client side vehicle movement (#4648) * WIP client side vehicles * Address reviews and remove use of Optional * Only tick active vehicle * Track world ticks * Fixes for Camel dash and pose transition * Remove vehicle parameter * Start using blocks refactor * Update BlockRegistryPopulator * Update blocks * Support step height attribute * Use climbable block tag and TrapDoorBlock * Lock camel rotation if stationary * Fix boost ticking * Keep cache of surrounding blocks * Fix bug causing BoundingBox position to change in CollisionManager * Clamp user input * Support weaving status effect * Support gravity attribute * Piston support * Tick boost for Pig and Strider if any player is controlling * Submodule * Address some reviews * Support world border * Optimize world border check * Small optimizations * Add comments --- .../geyser/entity/EntityDefinitions.java | 6 +- .../geyser/entity/type/LivingEntity.java | 49 +- .../entity/type/living/animal/PigEntity.java | 63 +- .../type/living/animal/StriderEntity.java | 68 +- .../type/living/animal/horse/CamelEntity.java | 67 +- .../type/player/SessionPlayerEntity.java | 23 + .../vehicle/BoostableVehicleComponent.java | 60 ++ .../entity/vehicle/CamelVehicleComponent.java | 153 +++ .../geyser/entity/vehicle/ClientVehicle.java | 46 + .../entity/vehicle/VehicleComponent.java | 964 ++++++++++++++++++ .../geyser/inventory/PlayerInventory.java | 10 + .../inventory/item/StoredItemMappings.java | 4 + .../geysermc/geyser/level/JavaDimension.java | 8 +- .../geyser/level/block/BlockStateValues.java | 56 +- .../geysermc/geyser/level/block/Fluid.java | 32 + .../geyser/level/physics/BoundingBox.java | 38 +- .../level/physics/CollisionManager.java | 93 +- .../geyser/level/physics/Direction.java | 1 + .../geyser/session/GeyserSession.java | 21 + .../geyser/session/cache/PistonCache.java | 26 +- .../geyser/session/cache/WorldBorder.java | 52 + .../translator/collision/BlockCollision.java | 18 + .../level/block/entity/PistonBlockEntity.java | 60 +- .../bedrock/BedrockPlayerInputTranslator.java | 2 + .../player/BedrockMovePlayerTranslator.java | 5 +- .../player/BedrockRiderJumpTranslator.java | 2 + .../protocol/java/JavaRespawnTranslator.java | 1 + .../entity/JavaMoveVehicleTranslator.java | 5 + .../entity/JavaRemoveMobEffectTranslator.java | 9 +- .../entity/JavaSetPassengersTranslator.java | 9 + .../entity/JavaTeleportEntityTranslator.java | 5 + .../entity/JavaUpdateMobEffectTranslator.java | 10 +- .../java/level/JavaSetTimeTranslator.java | 2 + gradle/libs.versions.toml | 2 +- 34 files changed, 1903 insertions(+), 67 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/Fluid.java diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 9063c7421..5932ecf41 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -888,7 +888,7 @@ public final class EntityDefinitions { .type(EntityType.PIG) .heightAndWidth(0.9f) .addTranslator(MetadataType.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) - .addTranslator(null) // Boost time + .addTranslator(MetadataType.INT, PigEntity::setBoost) .build(); POLAR_BEAR = EntityDefinition.inherited(PolarBearEntity::new, ageableEntityBase) .type(EntityType.POLAR_BEAR) @@ -914,7 +914,7 @@ public final class EntityDefinitions { STRIDER = EntityDefinition.inherited(StriderEntity::new, ageableEntityBase) .type(EntityType.STRIDER) .height(1.7f).width(0.9f) - .addTranslator(null) // Boost time + .addTranslator(MetadataType.INT, StriderEntity::setBoost) .addTranslator(MetadataType.BOOLEAN, StriderEntity::setCold) .addTranslator(MetadataType.BOOLEAN, StriderEntity::setSaddled) .build(); @@ -955,7 +955,7 @@ public final class EntityDefinitions { .type(EntityType.CAMEL) .height(2.375f).width(1.7f) .addTranslator(MetadataType.BOOLEAN, CamelEntity::setDashing) - .addTranslator(null) // Last pose change tick + .addTranslator(MetadataType.LONG, CamelEntity::setLastPoseTick) .build(); HORSE = EntityDefinition.inherited(HorseEntity::new, abstractHorseEntityBase) .type(EntityType.HORSE) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java index 1dfe02b09..266189e63 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java @@ -41,6 +41,7 @@ import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.registry.type.ItemMapping; @@ -294,6 +295,36 @@ public class LivingEntity extends Entity { return super.interact(hand); } + @Override + public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) { + if (this instanceof ClientVehicle clientVehicle) { + if (clientVehicle.isClientControlled()) { + return; + } + clientVehicle.getVehicleComponent().moveRelative(relX, relY, relZ); + } + + super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround); + } + + @Override + public boolean setBoundingBoxHeight(float height) { + if (valid && this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setHeight(height); + } + + return super.setBoundingBoxHeight(height); + } + + @Override + public void setBoundingBoxWidth(float width) { + if (valid && this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setWidth(width); + } + + super.setBoundingBoxWidth(width); + } + /** * Checks to see if a nametag interaction would go through. */ @@ -407,9 +438,25 @@ public class LivingEntity extends Entity { this.maxHealth = Math.max((float) AttributeUtils.calculateValue(javaAttribute), 1f); newAttributes.add(createHealthAttribute()); } + case GENERIC_MOVEMENT_SPEED -> { + AttributeData attributeData = calculateAttribute(javaAttribute, GeyserAttributeType.MOVEMENT_SPEED); + newAttributes.add(attributeData); + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setMoveSpeed(attributeData.getValue()); + } + } + case GENERIC_STEP_HEIGHT -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setStepHeight((float) AttributeUtils.calculateValue(javaAttribute)); + } + } + case GENERIC_GRAVITY -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setGravity(AttributeUtils.calculateValue(javaAttribute)); + } + } case GENERIC_ATTACK_DAMAGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.ATTACK_DAMAGE)); case GENERIC_FLYING_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED)); - case GENERIC_MOVEMENT_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.MOVEMENT_SPEED)); case GENERIC_FOLLOW_RANGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FOLLOW_RANGE)); case GENERIC_KNOCKBACK_RESISTANCE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.KNOCKBACK_RESISTANCE)); case GENERIC_JUMP_STRENGTH -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.HORSE_JUMP_STRENGTH)); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java index 446e3e109..2ec23d673 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java @@ -27,20 +27,30 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.Tickable; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class PigEntity extends AnimalEntity { +public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { + private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f); public PigEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -84,4 +94,55 @@ public class PigEntity extends AnimalEntity { } } } + + public void setBoost(IntEntityMetadata entityMetadata) { + vehicleComponent.startBoost(entityMetadata.getPrimitiveValue()); + } + + @Override + public void tick() { + PlayerEntity player = getPlayerPassenger(); + if (player == null) { + return; + } + + if (player == session.getPlayerEntity()) { + if (session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK)) { + vehicleComponent.tickBoost(); + } + } else { // getHand() for session player seems to always return air + ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().carrotOnAStick().getBedrockDefinition(); + if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) { + vehicleComponent.tickBoost(); + } + } + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector2f getAdjustedInput(Vector2f input) { + return Vector2f.UNIT_Y; + } + + @Override + public float getVehicleSpeed() { + return vehicleComponent.getMoveSpeed() * 0.225f * vehicleComponent.getBoostMultiplier(); + } + + private @Nullable PlayerEntity getPlayerPassenger() { + if (getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) instanceof PlayerEntity playerEntity) { + return playerEntity; + } + + return null; + } + + @Override + public boolean isClientControlled() { + return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK); + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java index 0291f75d9..e06af2786 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java @@ -27,23 +27,33 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.Tickable; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class StriderEntity extends AnimalEntity { +public class StriderEntity extends AnimalEntity implements Tickable, ClientVehicle { + private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f); private boolean isCold = false; public StriderEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -131,4 +141,60 @@ public class StriderEntity extends AnimalEntity { } } } + + public void setBoost(IntEntityMetadata entityMetadata) { + vehicleComponent.startBoost(entityMetadata.getPrimitiveValue()); + } + + @Override + public void tick() { + PlayerEntity player = getPlayerPassenger(); + if (player == null) { + return; + } + + if (player == session.getPlayerEntity()) { + if (session.getPlayerInventory().isHolding(Items.WARPED_FUNGUS_ON_A_STICK)) { + vehicleComponent.tickBoost(); + } + } else { // getHand() for session player seems to always return air + ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().warpedFungusOnAStick().getBedrockDefinition(); + if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) { + vehicleComponent.tickBoost(); + } + } + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector2f getAdjustedInput(Vector2f input) { + return Vector2f.UNIT_Y; + } + + @Override + public float getVehicleSpeed() { + return vehicleComponent.getMoveSpeed() * (isCold ? 0.35f : 0.55f) * vehicleComponent.getBoostMultiplier(); + } + + private @Nullable PlayerEntity getPlayerPassenger() { + if (getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) instanceof PlayerEntity playerEntity) { + return playerEntity; + } + + return null; + } + + @Override + public boolean isClientControlled() { + return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.WARPED_FUNGUS_ON_A_STICK); + } + + @Override + public boolean canWalkOnLava() { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java index ee3b2be70..3c0bf1a70 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java @@ -25,26 +25,36 @@ package org.geysermc.geyser.entity.type.living.animal.horse; +import org.cloudburstmc.math.vector.Vector2f; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.vehicle.CamelVehicleComponent; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.LongEntityMetadata; import java.util.UUID; -public class CamelEntity extends AbstractHorseEntity { - +public class CamelEntity extends AbstractHorseEntity implements ClientVehicle { public static final float SITTING_HEIGHT_DIFFERENCE = 1.43F; + private final CamelVehicleComponent vehicleComponent = new CamelVehicleComponent(this); + public CamelEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); @@ -111,5 +121,58 @@ public class CamelEntity extends AbstractHorseEntity { } public void setDashing(BooleanEntityMetadata entityMetadata) { + // Java sends true to show dash animation and start the dash cooldown, + // false ends the dash animation, not the cooldown. + // Bedrock shows dash animation if HAS_DASH_COOLDOWN is set and the camel is above ground + if (entityMetadata.getPrimitiveValue()) { + setFlag(EntityFlag.HAS_DASH_COOLDOWN, true); + vehicleComponent.startDashCooldown(); + } else if (!isClientControlled()) { // Don't remove dash cooldown prematurely if client is controlling + setFlag(EntityFlag.HAS_DASH_COOLDOWN, false); + } + } + + public void setLastPoseTick(LongEntityMetadata entityMetadata) { + // Tick is based on world time. If negative, the camel is sitting. + // Must be compared to world time to know if the camel is fully standing/sitting or transitioning. + vehicleComponent.setLastPoseTick(entityMetadata.getPrimitiveValue()); + } + + @Override + protected AttributeData calculateAttribute(Attribute javaAttribute, GeyserAttributeType type) { + AttributeData attributeData = super.calculateAttribute(javaAttribute, type); + if (javaAttribute.getType() == AttributeType.Builtin.GENERIC_JUMP_STRENGTH) { + vehicleComponent.setHorseJumpStrength(attributeData.getValue()); + } + return attributeData; + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector2f getAdjustedInput(Vector2f input) { + return input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f); + } + + @Override + public boolean isClientControlled() { + return getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) == session.getPlayerEntity(); + } + + @Override + public float getVehicleSpeed() { + float moveSpeed = vehicleComponent.getMoveSpeed(); + if (!getFlag(EntityFlag.HAS_DASH_COOLDOWN) && session.getPlayerEntity().getFlag(EntityFlag.SPRINTING)) { + return moveSpeed + 0.1f; + } + return moveSpeed; + } + + @Override + public boolean canClimb() { + return false; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java index b924461af..ccf2d25e6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java @@ -29,6 +29,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; import lombok.Setter; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -42,6 +43,7 @@ import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.DimensionUtils; +import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.GlobalPos; @@ -74,6 +76,16 @@ public class SessionPlayerEntity extends PlayerEntity { */ @Getter private boolean isRidingInFront; + /** + * Used when emulating client-side vehicles + */ + @Getter + private Vector2f vehicleInput = Vector2f.ZERO; + /** + * Used when emulating client-side vehicles + */ + @Getter + private int vehicleJumpStrength; private int lastAirSupply = getMaxAir(); @@ -315,6 +327,17 @@ public class SessionPlayerEntity extends PlayerEntity { this.setAirSupply(getMaxAir()); } + public void setVehicleInput(Vector2f vehicleInput) { + this.vehicleInput = Vector2f.from( + MathUtils.clamp(vehicleInput.getX(), -1.0f, 1.0f), + MathUtils.clamp(vehicleInput.getY(), -1.0f, 1.0f) + ); + } + + public void setVehicleJumpStrength(int vehicleJumpStrength) { + this.vehicleJumpStrength = MathUtils.constrain(vehicleJumpStrength, 0, 100); + } + private boolean isBelowVoidFloor() { return position.getY() < voidFloorPosition(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java new file mode 100644 index 000000000..41224012d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019-2023 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.geyser.entity.vehicle; + +import org.cloudburstmc.math.TrigMath; +import org.geysermc.geyser.entity.type.LivingEntity; + +public class BoostableVehicleComponent extends VehicleComponent { + private int boostLength; + private int boostTicks = 1; + + public BoostableVehicleComponent(T vehicle, float stepHeight) { + super(vehicle, stepHeight); + } + + public void startBoost(int boostLength) { + this.boostLength = boostLength; + this.boostTicks = 1; + } + + public float getBoostMultiplier() { + if (isBoosting()) { + return 1.0f + 1.15f * TrigMath.sin((float) boostTicks / (float) boostLength * TrigMath.PI); + } + return 1.0f; + } + + public boolean isBoosting() { + return boostTicks <= boostLength; + } + + public void tickBoost() { + if (isBoosting()) { + boostTicks++; + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java new file mode 100644 index 000000000..7d022ed7c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2023 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.geyser.entity.vehicle; + +import lombok.Setter; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; + +public class CamelVehicleComponent extends VehicleComponent { + private static final int STANDING_TICKS = 52; + private static final int DASH_TICKS = 55; + + @Setter + private float horseJumpStrength = 0.42f; // Not sent by vanilla Java server when spawned + + @Setter + private long lastPoseTick; + + private int dashTick; + private int effectJumpBoost; + + public CamelVehicleComponent(CamelEntity vehicle) { + super(vehicle, 1.5f); + } + + public void startDashCooldown() { + // tickVehicle is only called while the vehicle is mounted. Use session ticks to keep + // track of time instead of counting down + this.dashTick = vehicle.getSession().getTicks() + DASH_TICKS; + } + + @Override + public void tickVehicle() { + if (this.dashTick != 0) { + if (vehicle.getSession().getTicks() > this.dashTick) { + vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, false); + this.dashTick = 0; + } else { + vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, true); + } + } + + vehicle.setFlag(EntityFlag.CAN_DASH, vehicle.getFlag(EntityFlag.SADDLED) && !isStationary()); + vehicle.updateBedrockMetadata(); + super.tickVehicle(); + } + + @Override + public void onDismount() { + // Prevent camel from getting stuck in dash animation + vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, false); + vehicle.updateBedrockMetadata(); + super.onDismount(); + } + + @Override + protected boolean travel(VehicleContext ctx, float speed) { + if (vehicle.isOnGround() && isStationary()) { + vehicle.setMotion(vehicle.getMotion().mul(0, 1, 0)); + } + + return super.travel(ctx, speed); + } + + @Override + protected Vector3f getInputVelocity(VehicleContext ctx, float speed) { + if (isStationary()) { + return Vector3f.ZERO; + } + + SessionPlayerEntity player = vehicle.getSession().getPlayerEntity(); + Vector3f inputVelocity = super.getInputVelocity(ctx, speed); + float jumpStrength = player.getVehicleJumpStrength(); + + if (jumpStrength > 0) { + player.setVehicleJumpStrength(0); + + if (jumpStrength >= 90) { + jumpStrength = 1.0f; + } else { + jumpStrength = 0.4f + 0.4f * jumpStrength / 90.0f; + } + + return inputVelocity.add(Vector3f.createDirectionDeg(0, -player.getYaw()) + .mul(22.2222f * jumpStrength * this.moveSpeed * getVelocityMultiplier(ctx)) + .up(1.4285f * jumpStrength * (this.horseJumpStrength * getJumpVelocityMultiplier(ctx) + (this.effectJumpBoost * 0.1f)))); + } + + return inputVelocity; + } + + @Override + protected Vector2f getVehicleRotation() { + if (isStationary()) { + return Vector2f.from(vehicle.getYaw(), vehicle.getPitch()); + } + return super.getVehicleRotation(); + } + + /** + * Checks if the camel is sitting + * or transitioning to standing pose. + */ + private boolean isStationary() { + // Java checks if sitting using lastPoseTick + return this.lastPoseTick < 0 || vehicle.getSession().getWorldTicks() < this.lastPoseTick + STANDING_TICKS; + } + + @Override + public void setEffect(Effect effect, int effectAmplifier) { + if (effect == Effect.JUMP_BOOST) { + effectJumpBoost = effectAmplifier + 1; + } else { + super.setEffect(effect, effectAmplifier); + } + } + + @Override + public void removeEffect(Effect effect) { + if (effect == Effect.JUMP_BOOST) { + effectJumpBoost = 0; + } else { + super.removeEffect(effect); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java new file mode 100644 index 000000000..e6aaf1daa --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2023 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.geyser.entity.vehicle; + +import org.cloudburstmc.math.vector.Vector2f; + +public interface ClientVehicle { + VehicleComponent getVehicleComponent(); + + Vector2f getAdjustedInput(Vector2f input); + + float getVehicleSpeed(); + + boolean isClientControlled(); + + default boolean canWalkOnLava() { + return false; + } + + default boolean canClimb() { + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java new file mode 100644 index 000000000..db703a3cb --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java @@ -0,0 +1,964 @@ +/* + * Copyright (c) 2019-2023 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.geyser.entity.vehicle; + +import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.TrigMath; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; +import org.geysermc.erosion.util.BlockPositionIterator; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.Fluid; +import org.geysermc.geyser.level.block.property.Properties; +import org.geysermc.geyser.level.block.type.BedBlock; +import org.geysermc.geyser.level.block.type.Block; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.block.type.TrapDoorBlock; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.level.physics.CollisionManager; +import org.geysermc.geyser.level.physics.Direction; +import org.geysermc.geyser.session.cache.tags.BlockTag; +import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.SolidCollision; +import org.geysermc.geyser.util.BlockUtils; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket; + +public class VehicleComponent { + private static final ObjectDoublePair EMPTY_FLUID_PAIR = ObjectDoublePair.of(Fluid.EMPTY, 0.0); + private static final float MAX_LOGICAL_FLUID_HEIGHT = 8.0f / BlockStateValues.NUM_FLUID_LEVELS; + private static final float BASE_SLIPPERINESS_CUBED = 0.6f * 0.6f * 0.6f; + private static final float MIN_VELOCITY = 0.003f; + + protected final T vehicle; + protected final BoundingBox boundingBox; + + protected float stepHeight; + protected float moveSpeed; + protected double gravity; + protected int effectLevitation; + protected boolean effectSlowFalling; + protected boolean effectWeaving; + + public VehicleComponent(T vehicle, float stepHeight) { + this.vehicle = vehicle; + this.stepHeight = stepHeight; + this.moveSpeed = (float) AttributeType.Builtin.GENERIC_MOVEMENT_SPEED.getDef(); + this.gravity = AttributeType.Builtin.GENERIC_GRAVITY.getDef(); + + double width = vehicle.getBoundingBoxWidth(); + double height = vehicle.getBoundingBoxHeight(); + this.boundingBox = new BoundingBox( + vehicle.getPosition().getX(), + vehicle.getPosition().getY() + height / 2, + vehicle.getPosition().getZ(), + width, height, width + ); + } + + public void setWidth(float width) { + boundingBox.setSizeX(width); + boundingBox.setSizeZ(width); + } + + public void setHeight(float height) { + boundingBox.translate(0, (height - boundingBox.getSizeY()) / 2, 0); + boundingBox.setSizeY(height); + } + + public void moveAbsolute(double x, double y, double z) { + boundingBox.setMiddleX(x); + boundingBox.setMiddleY(y + boundingBox.getSizeY() / 2); + boundingBox.setMiddleZ(z); + } + + public void moveRelative(double x, double y, double z) { + boundingBox.translate(x, y, z); + } + + public void moveRelative(Vector3d vec) { + boundingBox.translate(vec); + } + + public BoundingBox getBoundingBox() { + return this.boundingBox; + } + + public void setEffect(Effect effect, int effectAmplifier) { + switch (effect) { + case LEVITATION -> effectLevitation = effectAmplifier + 1; + case SLOW_FALLING -> effectSlowFalling = true; + case WEAVING -> effectWeaving = true; + } + } + + public void removeEffect(Effect effect) { + switch (effect) { + case LEVITATION -> effectLevitation = 0; + case SLOW_FALLING -> effectSlowFalling = false; + case WEAVING -> effectWeaving = false; + } + } + + public void setMoveSpeed(float moveSpeed) { + this.moveSpeed = moveSpeed; + } + + public float getMoveSpeed() { + return moveSpeed; + } + + public void setStepHeight(float stepHeight) { + this.stepHeight = MathUtils.clamp(stepHeight, 1.0f, 10.0f); + } + + public void setGravity(double gravity) { + this.gravity = MathUtils.constrain(gravity, -1.0, 1.0); + } + + public Vector3d correctMovement(Vector3d movement) { + return vehicle.getSession().getCollisionManager().correctMovement( + movement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava() + ); + } + + public void onMount() { + vehicle.getSession().getPlayerEntity().setVehicleInput(Vector2f.ZERO); + vehicle.getSession().getPlayerEntity().setVehicleJumpStrength(0); + } + + public void onDismount() { + // + } + + /** + * Called every session tick while the player is mounted on the vehicle. + */ + public void tickVehicle() { + if (!vehicle.isClientControlled()) { + return; + } + + VehicleContext ctx = new VehicleContext(); + ctx.loadSurroundingBlocks(); + + ObjectDoublePair fluidHeight = updateFluidMovement(ctx); + switch (fluidHeight.left()) { + case WATER -> waterMovement(ctx); + case LAVA -> { + if (vehicle.canWalkOnLava() && ctx.centerBlock().is(Blocks.LAVA)) { + landMovement(ctx); + } else { + lavaMovement(ctx, fluidHeight.rightDouble()); + } + } + case EMPTY -> landMovement(ctx); + } + } + + /** + * Adds velocity of all colliding fluids to the vehicle, and returns the height of the fluid to use for movement. + * + * @param ctx context + * @return type and height of fluid to use for movement + */ + protected ObjectDoublePair updateFluidMovement(VehicleContext ctx) { + BoundingBox box = boundingBox.clone(); + box.expand(-0.001); + + Vector3d min = box.getMin(); + Vector3d max = box.getMax(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + + double waterHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.WATER, 0.014, min.getY()); + double lavaHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.LAVA, vehicle.getSession().getDimensionType().ultrawarm() ? 0.007 : 0.007 / 3, min.getY()); + + // Apply upward motion if the vehicle is a Strider, and it is submerged in lava + if (lavaHeight > 0 && vehicle.getDefinition().entityType() == EntityType.STRIDER) { + Vector3i blockPos = ctx.centerPos().toInt(); + if (!CollisionManager.FLUID_COLLISION.isBelow(blockPos.getY(), boundingBox) || ctx.getBlock(blockPos.up()).is(Blocks.LAVA)) { + vehicle.setMotion(vehicle.getMotion().mul(0.5f).add(0, 0.05f, 0)); + } else { + vehicle.setOnGround(true); + } + } + + // Water movement has priority over lava movement + if (waterHeight > 0) { + return ObjectDoublePair.of(Fluid.WATER, waterHeight); + } + + if (lavaHeight > 0) { + return ObjectDoublePair.of(Fluid.LAVA, lavaHeight); + } + + return EMPTY_FLUID_PAIR; + } + + /** + * Calculates how deep the vehicle is in a fluid, and applies its velocity. + * + * @param ctx context + * @param iter iterator of colliding blocks + * @param fluid type of fluid + * @param speed multiplier for fluid motion + * @param minY minY of the bounding box used to check for fluid collision; not exactly the same as the vehicle's bounding box + * @return height of fluid compared to minY + */ + protected double getFluidHeightAndApplyMovement(VehicleContext ctx, BlockPositionIterator iter, Fluid fluid, double speed, double minY) { + Vector3d totalVelocity = Vector3d.ZERO; + double maxFluidHeight = 0; + int fluidBlocks = 0; + + for (iter.reset(); iter.hasNext(); iter.next()) { + int blockId = ctx.getBlockId(iter); + if (BlockStateValues.getFluid(blockId) != fluid) { + continue; + } + + Vector3i blockPos = Vector3i.from(iter.getX(), iter.getY(), iter.getZ()); + float worldFluidHeight = getWorldFluidHeight(fluid, blockId); + + double vehicleFluidHeight = blockPos.getY() + worldFluidHeight - minY; + if (vehicleFluidHeight < 0) { + // Vehicle is not submerged in this fluid block + continue; + } + + // flowBlocked is only used when determining if a falling fluid should drag the vehicle downwards. + // If this block is not a falling fluid, set to true to avoid unnecessary checks. + boolean flowBlocked = worldFluidHeight != 1; + + Vector3d velocity = Vector3d.ZERO; + for (Direction direction : Direction.HORIZONTAL) { + Vector3i adjacentBlockPos = blockPos.add(direction.getUnitVector()); + int adjacentBlockId = ctx.getBlockId(adjacentBlockPos); + Fluid adjacentFluid = BlockStateValues.getFluid(adjacentBlockId); + + float fluidHeightDiff = 0; + if (adjacentFluid == fluid) { + fluidHeightDiff = getLogicalFluidHeight(fluid, blockId) - getLogicalFluidHeight(fluid, adjacentBlockId); + } else if (adjacentFluid == Fluid.EMPTY) { + // If the adjacent block is not a fluid and does not have collision, + // check if there is a fluid under it + BlockCollision adjacentBlockCollision = BlockUtils.getCollision(adjacentBlockId); + if (adjacentBlockCollision == null) { + float adjacentFluidHeight = getLogicalFluidHeight(fluid, ctx.getBlockId(adjacentBlockPos.add(Direction.DOWN.getUnitVector()))); + if (adjacentFluidHeight != -1) { // Only care about same type of fluid + fluidHeightDiff = getLogicalFluidHeight(fluid, blockId) - (adjacentFluidHeight - MAX_LOGICAL_FLUID_HEIGHT); + } + } else if (!flowBlocked) { + // No need to check if flow is already blocked from another direction, or if this isn't a falling fluid. + flowBlocked = isFlowBlocked(fluid, adjacentBlockId); + } + } + + if (fluidHeightDiff != 0) { + velocity = velocity.add(direction.getUnitVector().toDouble().mul(fluidHeightDiff)); + } + } + + if (worldFluidHeight == 1) { // If falling fluid + // If flow is not blocked, check if it is blocked for the fluid above + if (!flowBlocked) { + Vector3i blockPosUp = blockPos.up(); + for (Direction direction : Direction.HORIZONTAL) { + flowBlocked = isFlowBlocked(fluid, ctx.getBlockId(blockPosUp.add(direction.getUnitVector()))); + if (flowBlocked) { + break; + } + } + } + + if (flowBlocked) { + velocity = javaNormalize(velocity).add(0.0, -6.0, 0.0); + } + } + + velocity = javaNormalize(velocity); + + maxFluidHeight = Math.max(vehicleFluidHeight, maxFluidHeight); + if (maxFluidHeight < 0.4) { + velocity = velocity.mul(maxFluidHeight); + } + + totalVelocity = totalVelocity.add(velocity); + fluidBlocks++; + } + + if (!totalVelocity.equals(Vector3d.ZERO)) { + Vector3f motion = vehicle.getMotion(); + + totalVelocity = javaNormalize(totalVelocity.mul(1.0 / fluidBlocks)); + totalVelocity = totalVelocity.mul(speed); + + if (totalVelocity.length() < 0.0045 && Math.abs(motion.getX()) < MIN_VELOCITY && Math.abs(motion.getZ()) < MIN_VELOCITY) { + totalVelocity = javaNormalize(totalVelocity).mul(0.0045); + } + + vehicle.setMotion(motion.add(totalVelocity.toFloat())); + } + + return maxFluidHeight; + } + + /** + * Java edition returns the zero vector if the length of the input vector is less than 0.0001 + */ + protected Vector3d javaNormalize(Vector3d vec) { + double len = vec.length(); + return len < 1.0E-4 ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len); + } + + protected float getWorldFluidHeight(Fluid fluidType, int blockId) { + return (float) switch (fluidType) { + case WATER -> BlockStateValues.getWaterHeight(blockId); + case LAVA -> BlockStateValues.getLavaHeight(blockId); + case EMPTY -> -1; + }; + } + + protected float getLogicalFluidHeight(Fluid fluidType, int blockId) { + return Math.min(getWorldFluidHeight(fluidType, blockId), MAX_LOGICAL_FLUID_HEIGHT); + } + + protected boolean isFlowBlocked(Fluid fluid, int adjacentBlockId) { + if (BlockState.of(adjacentBlockId).is(Blocks.ICE)) { + return false; + } + + if (BlockStateValues.getFluid(adjacentBlockId) == fluid) { + return false; + } + + // TODO: supposed to check if the opposite face of the block touching the fluid is solid, instead of SolidCollision + return BlockUtils.getCollision(adjacentBlockId) instanceof SolidCollision; + } + + protected void waterMovement(VehicleContext ctx) { + double gravity = getGravity(); + float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : 0.8f; // 0.8f: getBaseMovementSpeedMultiplier + double originalY = ctx.centerPos().getY(); + boolean falling = vehicle.getMotion().getY() <= 0; + + // NOT IMPLEMENTED: depth strider and dolphins grace + + boolean horizontalCollision = travel(ctx, 0.02f); + + if (horizontalCollision && isClimbing(ctx)) { + vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.2f, vehicle.getMotion().getZ())); + } + + vehicle.setMotion(vehicle.getMotion().mul(drag, 0.8f, drag)); + vehicle.setMotion(getFluidGravity(gravity, falling)); + + if (horizontalCollision && shouldApplyFluidJumpBoost(ctx, originalY)) { + vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.3f, vehicle.getMotion().getZ())); + } + } + + protected void lavaMovement(VehicleContext ctx, double lavaHeight) { + double gravity = getGravity(); + double originalY = ctx.centerPos().getY(); + boolean falling = vehicle.getMotion().getY() <= 0; + + boolean horizontalCollision = travel(ctx, 0.02f); + + if (lavaHeight <= (boundingBox.getSizeY() * 0.85 < 0.4 ? 0.0 : 0.4)) { // Swim height + vehicle.setMotion(vehicle.getMotion().mul(0.5f, 0.8f, 0.5f)); + vehicle.setMotion(getFluidGravity(gravity, falling)); + } else { + vehicle.setMotion(vehicle.getMotion().mul(0.5f)); + } + + vehicle.setMotion(vehicle.getMotion().down((float) (gravity / 4.0))); + + if (horizontalCollision && shouldApplyFluidJumpBoost(ctx, originalY)) { + vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.3f, vehicle.getMotion().getZ())); + } + } + + protected void landMovement(VehicleContext ctx) { + double gravity = getGravity(); + float slipperiness = BlockStateValues.getSlipperiness(getVelocityBlock(ctx)); + float drag = vehicle.isOnGround() ? 0.91f * slipperiness : 0.91f; + float speed = vehicle.getVehicleSpeed() * (vehicle.isOnGround() ? BASE_SLIPPERINESS_CUBED / (slipperiness * slipperiness * slipperiness) : 0.1f); + + boolean horizontalCollision = travel(ctx, speed); + + if (isClimbing(ctx)) { + Vector3f motion = vehicle.getMotion(); + vehicle.setMotion( + Vector3f.from( + MathUtils.clamp(motion.getX(), -0.15f, 0.15f), + horizontalCollision ? 0.2f : Math.max(motion.getY(), -0.15f), + MathUtils.clamp(motion.getZ(), -0.15f, 0.15f) + ) + ); + // NOT IMPLEMENTED: climbing in powdered snow + } + + if (effectLevitation > 0) { + vehicle.setMotion(vehicle.getMotion().up((0.05f * effectLevitation - vehicle.getMotion().getY()) * 0.2f)); + } else { + vehicle.setMotion(vehicle.getMotion().down((float) gravity)); + // NOT IMPLEMENTED: slow fall when in unloaded chunk + } + + vehicle.setMotion(vehicle.getMotion().mul(drag, 0.98f, drag)); + } + + protected boolean shouldApplyFluidJumpBoost(VehicleContext ctx, double originalY) { + BoundingBox box = boundingBox.clone(); + box.translate(vehicle.getMotion().toDouble().up(0.6f - ctx.centerPos().getY() + originalY)); + box.expand(-1.0E-7); + + BlockPositionIterator iter = vehicle.getSession().getCollisionManager().collidableBlocksIterator(box); + for (iter.reset(); iter.hasNext(); iter.next()) { + int blockId = ctx.getBlockId(iter); + + // Also check for fluids + BlockCollision blockCollision = BlockUtils.getCollision(blockId); + if (blockCollision == null && BlockStateValues.getFluid(blockId) != Fluid.EMPTY) { + blockCollision = CollisionManager.SOLID_COLLISION; + } + + if (blockCollision != null && blockCollision.checkIntersection(iter.getX(), iter.getY(), iter.getZ(), box)) { + return false; + } + } + + return true; + } + + protected Vector3f getFluidGravity(double gravity, boolean falling) { + Vector3f motion = vehicle.getMotion(); + if (gravity != 0 && !vehicle.getFlag(EntityFlag.SPRINTING)) { + float newY = (float) (motion.getY() - gravity / 16); + if (falling && Math.abs(motion.getY() - 0.005f) >= MIN_VELOCITY && Math.abs(newY) < MIN_VELOCITY) { + newY = -MIN_VELOCITY; + } + return Vector3f.from(motion.getX(), newY, motion.getZ()); + } + return motion; + } + + /** + * Check if any blocks the vehicle is colliding with should multiply movement. (Cobweb, powder snow, berry bush) + *

    + * This is different from the speed factor of a block the vehicle is standing on, such as soul sand. + * + * @param ctx context + * @return the multiplier + */ + protected @Nullable Vector3f getBlockMovementMultiplier(VehicleContext ctx) { + BoundingBox box = boundingBox.clone(); + box.expand(-1.0E-7); + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + + // Iterate xyz backwards + // Minecraft iterates forwards but only the last multiplier affects movement + for (int x = max.getX(); x >= min.getX(); x--) { + for (int y = max.getY(); y >= min.getY(); y--) { + for (int z = max.getZ(); z >= min.getZ(); z--) { + Block block = ctx.getBlock(x, y, z).block(); + Vector3f multiplier = null; + + if (block == Blocks.COBWEB) { + if (effectWeaving) { + multiplier = Vector3f.from(0.5, 0.25, 0.5); + } else { + multiplier = Vector3f.from(0.25, 0.05f, 0.25); + } + } else if (block == Blocks.POWDER_SNOW) { + multiplier = Vector3f.from(0.9f, 1.5, 0.9f); + } else if (block == Blocks.SWEET_BERRY_BUSH) { + multiplier = Vector3f.from(0.8f, 0.75, 0.8f); + } + + if (multiplier != null) { + return multiplier; + } + } + } + } + + return null; + } + + protected void applyBlockCollisionEffects(VehicleContext ctx) { + BoundingBox box = boundingBox.clone(); + box.expand(-1.0E-7); + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); + for (iter.reset(); iter.hasNext(); iter.next()) { + BlockState blockState = ctx.getBlock(iter); + + if (blockState.is(Blocks.HONEY_BLOCK)) { + onHoneyBlockCollision(); + } else if (blockState.is(Blocks.BUBBLE_COLUMN)) { + onBubbleColumnCollision(blockState.getValue(Properties.DRAG)); + } + } + } + + protected void onHoneyBlockCollision() { + if (vehicle.isOnGround() || vehicle.getMotion().getY() >= -0.08f) { + return; + } + + // NOT IMPLEMENTED: don't slide if inside the honey block + Vector3f motion = vehicle.getMotion(); + float mul = motion.getY() < -0.13f ? -0.05f / motion.getY() : 1; + vehicle.setMotion(Vector3f.from(motion.getX() * mul, -0.05f, motion.getZ() * mul)); + } + + protected void onBubbleColumnCollision(boolean drag) { + Vector3f motion = vehicle.getMotion(); + vehicle.setMotion(Vector3f.from( + motion.getX(), + drag ? Math.max(-0.3f, motion.getY() - 0.03f) : Math.min(0.7f, motion.getY() + 0.06f), + motion.getZ() + )); + } + + /** + * Calculates the next position of the vehicle while checking for collision and adjusting velocity. + * + * @return true if there was a horizontal collision + */ + protected boolean travel(VehicleContext ctx, float speed) { + Vector3f motion = vehicle.getMotion(); + + // Java only does this client side + motion = motion.mul(0.98f); + + motion = Vector3f.from( + Math.abs(motion.getX()) < MIN_VELOCITY ? 0 : motion.getX(), + Math.abs(motion.getY()) < MIN_VELOCITY ? 0 : motion.getY(), + Math.abs(motion.getZ()) < MIN_VELOCITY ? 0 : motion.getZ() + ); + + // !isImmobile + if (vehicle.isAlive()) { + motion = motion.add(getInputVelocity(ctx, speed)); + } + + Vector3f movementMultiplier = getBlockMovementMultiplier(ctx); + if (movementMultiplier != null) { + motion = motion.mul(movementMultiplier); + } + + // Check world border before blocks + Vector3d correctedMovement = vehicle.getSession().getWorldBorder().correctMovement(boundingBox, motion.toDouble()); + correctedMovement = vehicle.getSession().getCollisionManager().correctMovement( + correctedMovement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava() + ); + + boundingBox.translate(correctedMovement); + ctx.loadSurroundingBlocks(); // Context must be reloaded after vehicle is moved + + // Non-zero values indicate a collision on that axis + Vector3d moveDiff = motion.toDouble().sub(correctedMovement); + + vehicle.setOnGround(moveDiff.getY() != 0 && motion.getY() < 0); + boolean horizontalCollision = moveDiff.getX() != 0 || moveDiff.getZ() != 0; + + boolean bounced = false; + if (vehicle.isOnGround()) { + Block landingBlock = getLandingBlock(ctx).block(); + + if (landingBlock == Blocks.SLIME_BLOCK) { + motion = Vector3f.from(motion.getX(), -motion.getY(), motion.getZ()); + bounced = true; + + // Slow horizontal movement + float absY = Math.abs(motion.getY()); + if (absY < 0.1f) { + float mul = 0.4f + absY * 0.2f; + motion = motion.mul(mul, 1.0f, mul); + } + } else if (landingBlock instanceof BedBlock) { + motion = Vector3f.from(motion.getX(), -motion.getY() * 0.66f, motion.getZ()); + bounced = true; + } + } + + // Set motion to 0 if a movement multiplier was used, else set to 0 on each axis with a collision + if (movementMultiplier != null) { + motion = Vector3f.ZERO; + } else { + motion = motion.mul( + moveDiff.getX() == 0 ? 1 : 0, + moveDiff.getY() == 0 || bounced ? 1 : 0, + moveDiff.getZ() == 0 ? 1 : 0 + ); + } + + // Send the new position to the bedrock client and java server + moveVehicle(ctx.centerPos()); + vehicle.setMotion(motion); + + applyBlockCollisionEffects(ctx); + + float velocityMultiplier = getVelocityMultiplier(ctx); + vehicle.setMotion(vehicle.getMotion().mul(velocityMultiplier, 1.0f, velocityMultiplier)); + + return horizontalCollision; + } + + protected boolean isClimbing(VehicleContext ctx) { + if (!vehicle.canClimb()) { + return false; + } + + BlockState blockState = ctx.centerBlock(); + if (vehicle.getSession().getTagCache().is(BlockTag.CLIMBABLE, blockState.block())) { + return true; + } + + // Check if the vehicle is in an open trapdoor with a ladder of the same direction under it + if (blockState.block() instanceof TrapDoorBlock && blockState.getValue(Properties.OPEN)) { + BlockState ladderState = ctx.getBlock(ctx.centerPos().toInt().down()); + return ladderState.is(Blocks.LADDER) && + ladderState.getValue(Properties.HORIZONTAL_FACING) == blockState.getValue(Properties.HORIZONTAL_FACING); + } + + return false; + } + + /** + * Translates the player's input into velocity. + * + * @param ctx context + * @param speed multiplier for input + * @return velocity + */ + protected Vector3f getInputVelocity(VehicleContext ctx, float speed) { + Vector2f input = vehicle.getSession().getPlayerEntity().getVehicleInput(); + input = input.mul(0.98f); + input = vehicle.getAdjustedInput(input); + input = normalizeInput(input); + input = input.mul(speed); + + // Match player rotation + float yaw = vehicle.getSession().getPlayerEntity().getYaw(); + float sin = TrigMath.sin(yaw * TrigMath.DEG_TO_RAD); + float cos = TrigMath.cos(yaw * TrigMath.DEG_TO_RAD); + return Vector3f.from(input.getX() * cos - input.getY() * sin, 0, input.getY() * cos + input.getX() * sin); + } + + protected Vector2f normalizeInput(Vector2f input) { + float lenSquared = input.lengthSquared(); + if (lenSquared < 1.0E-7) { + return Vector2f.ZERO; + } else if (lenSquared > 1.0) { + return input.normalize(); + } + return input; + } + + /** + * Gets the rotation to use for the vehicle. This is based on the player's head rotation. + */ + protected Vector2f getVehicleRotation() { + LivingEntity player = vehicle.getSession().getPlayerEntity(); + return Vector2f.from(player.getYaw(), player.getPitch() * 0.5f); + } + + /** + * Sets the new position for the vehicle and sends packets to both the java server and bedrock client. + *

    + * This also updates the session's last vehicle move timestamp. + * @param javaPos the new java position of the vehicle + */ + protected void moveVehicle(Vector3d javaPos) { + Vector3f bedrockPos = javaPos.toFloat(); + Vector2f rotation = getVehicleRotation(); + + MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); + moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId()); + + if (vehicle.isOnGround()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND); + } + + if (vehicle.getPosition().getX() != bedrockPos.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X); + moveEntityDeltaPacket.setX(bedrockPos.getX()); + } + if (vehicle.getPosition().getY() != bedrockPos.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y); + moveEntityDeltaPacket.setY(bedrockPos.getY()); + } + if (vehicle.getPosition().getZ() != bedrockPos.getZ()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z); + moveEntityDeltaPacket.setZ(bedrockPos.getZ()); + } + vehicle.setPosition(bedrockPos); + + if (vehicle.getYaw() != rotation.getX()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + moveEntityDeltaPacket.setYaw(rotation.getX()); + vehicle.setYaw(rotation.getX()); + } + if (vehicle.getPitch() != rotation.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); + moveEntityDeltaPacket.setPitch(rotation.getY()); + vehicle.setPitch(rotation.getY()); + } + if (vehicle.getHeadYaw() != rotation.getX()) { // Same as yaw + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); + moveEntityDeltaPacket.setHeadYaw(rotation.getX()); + vehicle.setHeadYaw(rotation.getX()); + } + + if (!moveEntityDeltaPacket.getFlags().isEmpty()) { + vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket); + } + + ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos.getX(), javaPos.getY(), javaPos.getZ(), rotation.getX(), rotation.getY()); + vehicle.getSession().sendDownstreamPacket(moveVehiclePacket); + vehicle.getSession().setLastVehicleMoveTimestamp(System.currentTimeMillis()); + } + + protected double getGravity() { + if (!vehicle.getFlag(EntityFlag.HAS_GRAVITY)) { + return 0; + } + + if (vehicle.getMotion().getY() <= 0 && effectSlowFalling) { + return Math.min(0.01, this.gravity); + } + + return this.gravity; + } + + /** + * Finds the position of the main block supporting the vehicle. + * Used when determining slipperiness, speed, etc. + *

    + * Should use {@link VehicleContext#supportingBlockPos()}, instead of calling this directly. + * + * @param ctx context + * @return position of the main block supporting this entity + */ + private @Nullable Vector3i getSupportingBlockPos(VehicleContext ctx) { + Vector3i result = null; + + if (vehicle.isOnGround()) { + BoundingBox box = boundingBox.clone(); + box.extend(0, -1.0E-6, 0); // Extend slightly down + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + + // Use minY as maxY + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), min.getY(), max.getZ()); + + double minDistance = Double.MAX_VALUE; + for (iter.reset(); iter.hasNext(); iter.next()) { + Vector3i blockPos = Vector3i.from(iter.getX(), iter.getY(), iter.getZ()); + int blockId = ctx.getBlockId(iter); + + BlockCollision blockCollision; + if (vehicle.canWalkOnLava()) { + blockCollision = vehicle.getSession().getCollisionManager().getCollisionLavaWalking(blockId, blockPos.getY(), boundingBox); + } else { + blockCollision = BlockUtils.getCollision(blockId); + } + + if (blockCollision != null && blockCollision.checkIntersection(blockPos, box)) { + double distance = ctx.centerPos().distanceSquared(blockPos.toDouble().add(0.5f, 0.5f, 0.5f)); + if (distance <= minDistance) { + minDistance = distance; + result = blockPos; + } + } + } + } + + return result; + } + + /** + * Returns the block that is x amount of blocks under the main supporting block. + */ + protected BlockState getBlockUnderSupport(VehicleContext ctx, float dist) { + Vector3i supportingBlockPos = ctx.supportingBlockPos(); + + Vector3i blockPos; + if (supportingBlockPos != null) { + blockPos = Vector3i.from(supportingBlockPos.getX(), Math.floor(ctx.centerPos().getY() - dist), supportingBlockPos.getZ()); + } else { + blockPos = ctx.centerPos().sub(0, dist, 0).toInt(); + } + + return ctx.getBlock(blockPos); + } + + /** + * The block to use when determining if the vehicle should bounce after landing. Currently just slime and bed blocks. + */ + protected BlockState getLandingBlock(VehicleContext ctx) { + return getBlockUnderSupport(ctx, 0.2f); + } + + /** + * The block to use when calculating slipperiness and speed. If on a slab, this will be the block under the slab. + */ + protected BlockState getVelocityBlock(VehicleContext ctx) { + return getBlockUnderSupport(ctx, 0.500001f); + } + + protected float getVelocityMultiplier(VehicleContext ctx) { + Block block = ctx.centerBlock().block(); + if (block == Blocks.WATER || block == Blocks.BUBBLE_COLUMN) { + return 1.0f; + } + + if (block == Blocks.SOUL_SAND || block == Blocks.HONEY_BLOCK) { + return 0.4f; + } + + block = getVelocityBlock(ctx).block(); + if (block == Blocks.SOUL_SAND || block == Blocks.HONEY_BLOCK) { + return 0.4f; + } + + return 1.0f; + } + + protected float getJumpVelocityMultiplier(VehicleContext ctx) { + Block block = ctx.centerBlock().block(); + if (block == Blocks.HONEY_BLOCK) { + return 0.5f; + } + + block = getVelocityBlock(ctx).block(); + if (block == Blocks.HONEY_BLOCK) { + return 0.5f; + } + + return 1.0f; + } + + protected class VehicleContext { + private Vector3d centerPos; + private Vector3d cachePos; + private BlockState centerBlock; + private Vector3i supportingBlockPos; + private BlockPositionIterator blockIter; + private int[] blocks; + + /** + * Cache frequently used data and blocks used in movement calculations. + *

    + * Can be called multiple times, and must be called at least once before using the VehicleContext. + */ + protected void loadSurroundingBlocks() { + this.centerPos = boundingBox.getBottomCenter(); + + // Reuse block cache if vehicle moved less than 1 block + if (this.cachePos == null || this.cachePos.distanceSquared(this.centerPos) > 1) { + BoundingBox box = boundingBox.clone(); + box.expand(2); + + Vector3i min = box.getMin().toInt(); + Vector3i max = box.getMax().toInt(); + this.blockIter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); + this.blocks = vehicle.getSession().getGeyser().getWorldManager().getBlocksAt(vehicle.getSession(), this.blockIter); + + this.cachePos = this.centerPos; + } + + this.centerBlock = getBlock(this.centerPos.toInt()); + this.supportingBlockPos = null; + } + + protected Vector3d centerPos() { + return this.centerPos; + } + + protected BlockState centerBlock() { + return this.centerBlock; + } + + protected Vector3i supportingBlockPos() { + if (this.supportingBlockPos == null) { + this.supportingBlockPos = getSupportingBlockPos(this); + } + + return this.supportingBlockPos; + } + + protected int getBlockId(int x, int y, int z) { + int index = this.blockIter.getIndex(x, y, z); + if (index == -1) { + vehicle.getSession().getGeyser().getLogger().debug("[client-vehicle] Block cache miss"); + return vehicle.getSession().getGeyser().getWorldManager().getBlockAt(vehicle.getSession(), x, y, z); + } + + return blocks[index]; + } + + protected int getBlockId(Vector3i pos) { + return getBlockId(pos.getX(), pos.getY(), pos.getZ()); + } + + protected int getBlockId(BlockPositionIterator iter) { + return getBlockId(iter.getX(), iter.getY(), iter.getZ()); + } + + protected BlockState getBlock(int x, int y, int z) { + return BlockState.of(getBlockId(x, y, z)); + } + + protected BlockState getBlock(Vector3i pos) { + return BlockState.of(getBlockId(pos.getX(), pos.getY(), pos.getZ())); + } + + protected BlockState getBlock(BlockPositionIterator iter) { + return BlockState.of(getBlockId(iter.getX(), iter.getY(), iter.getZ())); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java index c3756d663..3ea9cd112 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java @@ -62,6 +62,16 @@ public class PlayerInventory extends Inventory { cursor = newCursor; } + /** + * Checks if the player is holding the specified item in either hand + * + * @param item The item to look for + * @return If the player is holding the item in either hand + */ + public boolean isHolding(@NonNull Item item) { + return getItemInHand().asItem() == item || getOffhand().asItem() == item; + } + public GeyserItemStack getItemInHand(@NonNull Hand hand) { return hand == Hand.OFF_HAND ? getOffhand() : getItemInHand(); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index 475a3e588..a8a711cc2 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -43,6 +43,7 @@ public class StoredItemMappings { private final ItemMapping banner; private final ItemMapping barrier; private final ItemMapping bow; + private final ItemMapping carrotOnAStick; private final ItemMapping compass; private final ItemMapping crossbow; private final ItemMapping egg; @@ -52,6 +53,7 @@ public class StoredItemMappings { private final ItemMapping shield; private final ItemMapping totem; private final ItemMapping upgradeTemplate; + private final ItemMapping warpedFungusOnAStick; private final ItemMapping wheat; private final ItemMapping writableBook; private final ItemMapping writtenBook; @@ -60,6 +62,7 @@ public class StoredItemMappings { this.banner = load(itemMappings, Items.WHITE_BANNER); // As of 1.17.10, all banners have the same Bedrock ID this.barrier = load(itemMappings, Items.BARRIER); this.bow = load(itemMappings, Items.BOW); + this.carrotOnAStick = load(itemMappings, Items.CARROT_ON_A_STICK); this.compass = load(itemMappings, Items.COMPASS); this.crossbow = load(itemMappings, Items.CROSSBOW); this.egg = load(itemMappings, Items.EGG); @@ -69,6 +72,7 @@ public class StoredItemMappings { this.shield = load(itemMappings, Items.SHIELD); this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING); this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); + this.warpedFungusOnAStick = load(itemMappings, Items.WARPED_FUNGUS_ON_A_STICK); this.wheat = load(itemMappings, Items.WHEAT); this.writableBook = load(itemMappings, Items.WRITABLE_BOOK); this.writtenBook = load(itemMappings, Items.WRITTEN_BOOK); diff --git a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java index 0ca428830..50589851b 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java +++ b/core/src/main/java/org/geysermc/geyser/level/JavaDimension.java @@ -34,11 +34,13 @@ import org.geysermc.geyser.util.DimensionUtils; * Represents the information we store from the current Java dimension * @param piglinSafe Whether piglins and hoglins are safe from conversion in this dimension. * This controls if they have the shaking effect applied in the dimension. + * @param ultrawarm If this dimension is ultrawarm. + * Used when calculating movement in lava for client-side vehicles. * @param bedrockId the Bedrock dimension ID of this dimension. * As a Java dimension can be null in some login cases (e.g. GeyserConnect), make sure the player * is logged in before utilizing this field. */ -public record JavaDimension(int minY, int maxY, boolean piglinSafe, double worldCoordinateScale, int bedrockId, boolean isNetherLike) { +public record JavaDimension(int minY, int maxY, boolean piglinSafe, boolean ultrawarm, double worldCoordinateScale, int bedrockId, boolean isNetherLike) { public static JavaDimension read(RegistryEntryContext entry) { NbtMap dimension = entry.data(); @@ -48,6 +50,8 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double world // Set if piglins/hoglins should shake boolean piglinSafe = dimension.getBoolean("piglin_safe"); + // Entities in lava move faster in ultrawarm dimensions + boolean ultrawarm = dimension.getBoolean("ultrawarm"); // Load world coordinate scale for the world border double coordinateScale = dimension.getNumber("coordinate_scale").doubleValue(); // FIXME see if we can change this in the NBT library itself. @@ -67,6 +71,6 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, double world isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(effects); } - return new JavaDimension(minY, maxY, piglinSafe, coordinateScale, bedrockId, isNetherLike); + return new JavaDimension(minY, maxY, piglinSafe, ultrawarm, coordinateScale, bedrockId, isNetherLike); } } diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java index 01e95fc7a..36e437026 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java @@ -36,7 +36,7 @@ import org.geysermc.geyser.registry.BlockRegistries; * Used for block entities if the Java block state contains Bedrock block information. */ public final class BlockStateValues { - public static final int NUM_WATER_LEVELS = 9; + public static final int NUM_FLUID_LEVELS = 9; /** * Checks if a block sticks to other blocks @@ -99,6 +99,25 @@ public final class BlockStateValues { }; } + /** + * Get the type of fluid from the block state, including waterlogged blocks. + * + * @param state BlockState of the block + * @return The type of fluid + */ + public static Fluid getFluid(int state) { + BlockState blockState = BlockState.of(state); + if (blockState.is(Blocks.WATER) || BlockRegistries.WATERLOGGED.get().get(state)) { + return Fluid.WATER; + } + + if (blockState.is(Blocks.LAVA)) { + return Fluid.LAVA; + } + + return Fluid.EMPTY; + } + /** * Get the level of water from the block state. * @@ -127,7 +146,7 @@ public final class BlockStateValues { waterLevel = 0; } if (waterLevel >= 0) { - double waterHeight = 1 - (waterLevel + 1) / ((double) NUM_WATER_LEVELS); + double waterHeight = 1 - (waterLevel + 1) / ((double) NUM_FLUID_LEVELS); // Falling water is a full block if (waterLevel >= 8) { waterHeight = 1; @@ -137,6 +156,39 @@ public final class BlockStateValues { return -1; } + /** + * Get the level of lava from the block state. + * + * @param state BlockState of the block + * @return The lava level or -1 if the block isn't lava + */ + public static int getLavaLevel(int state) { + BlockState blockState = BlockState.of(state); + if (!blockState.is(Blocks.LAVA)) { + return -1; + } + return blockState.getValue(Properties.LEVEL); + } + + /** + * Get the height of lava from the block state + * + * @param state BlockState of the block + * @return The lava height or -1 if the block does not contain lava + */ + public static double getLavaHeight(int state) { + int lavaLevel = BlockStateValues.getLavaLevel(state); + if (lavaLevel >= 0) { + double lavaHeight = 1 - (lavaLevel + 1) / ((double) NUM_FLUID_LEVELS); + // Falling lava is a full block + if (lavaLevel >= 8) { + lavaHeight = 1; + } + return lavaHeight; + } + return -1; + } + /** * Get the slipperiness of a block. * This is used in ItemEntity to calculate the friction on an item as it slides across the ground diff --git a/core/src/main/java/org/geysermc/geyser/level/block/Fluid.java b/core/src/main/java/org/geysermc/geyser/level/block/Fluid.java new file mode 100644 index 000000000..a9693bbf4 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/Fluid.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2023 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.geyser.level.block; + +public enum Fluid { + WATER, + LAVA, + EMPTY +} diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java b/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java index b1a93d8ee..395467c02 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/BoundingBox.java @@ -33,6 +33,8 @@ import org.cloudburstmc.math.vector.Vector3d; @Data @AllArgsConstructor public class BoundingBox implements Cloneable { + private static final double EPSILON = 1.0E-7; + private double middleX; private double middleY; private double middleZ; @@ -57,10 +59,24 @@ public class BoundingBox implements Cloneable { sizeZ += Math.abs(z); } + public void expand(double x, double y, double z) { + sizeX += x; + sizeY += y; + sizeZ += z; + } + + public void translate(Vector3d translate) { + translate(translate.getX(), translate.getY(), translate.getZ()); + } + public void extend(Vector3d extend) { extend(extend.getX(), extend.getY(), extend.getZ()); } + public void expand(double expand) { + expand(expand, expand, expand); + } + public boolean checkIntersection(double offsetX, double offsetY, double offsetZ, BoundingBox otherBox) { return (Math.abs((middleX + offsetX) - otherBox.getMiddleX()) * 2 < (sizeX + otherBox.getSizeX())) && (Math.abs((middleY + offsetY) - otherBox.getMiddleY()) * 2 < (sizeY + otherBox.getSizeY())) && @@ -78,6 +94,14 @@ public class BoundingBox implements Cloneable { return Vector3d.from(x, y, z); } + public double getMin(Axis axis) { + return switch (axis) { + case X -> middleX - sizeX / 2; + case Y -> middleY - sizeY / 2; + case Z -> middleZ - sizeZ / 2; + }; + } + public Vector3d getMax() { double x = middleX + sizeX / 2; double y = middleY + sizeY / 2; @@ -85,15 +109,23 @@ public class BoundingBox implements Cloneable { return Vector3d.from(x, y, z); } + public double getMax(Axis axis) { + return switch (axis) { + case X -> middleX + sizeX / 2; + case Y -> middleY + sizeY / 2; + case Z -> middleZ + sizeZ / 2; + }; + } + public Vector3d getBottomCenter() { return Vector3d.from(middleX, middleY - sizeY / 2, middleZ); } private boolean checkOverlapInAxis(double xOffset, double yOffset, double zOffset, BoundingBox otherBox, Axis axis) { return switch (axis) { - case X -> Math.abs((middleX + xOffset) - otherBox.getMiddleX()) * 2 < (sizeX + otherBox.getSizeX()); - case Y -> Math.abs((middleY + yOffset) - otherBox.getMiddleY()) * 2 < (sizeY + otherBox.getSizeY()); - case Z -> Math.abs((middleZ + zOffset) - otherBox.getMiddleZ()) * 2 < (sizeZ + otherBox.getSizeZ()); + case X -> (sizeX + otherBox.getSizeX()) - Math.abs((middleX + xOffset) - otherBox.getMiddleX()) * 2 > EPSILON; + case Y -> (sizeY + otherBox.getSizeY()) - Math.abs((middleY + yOffset) - otherBox.getMiddleY()) * 2 > EPSILON; + case Z -> (sizeZ + otherBox.getSizeZ()) - Math.abs((middleZ + zOffset) - otherBox.getMiddleZ()) * 2 > EPSILON; }; } diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java index 2be4e7a38..a0fb312b4 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java @@ -38,6 +38,7 @@ import org.geysermc.erosion.util.BlockPositionIterator; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.property.Properties; @@ -45,7 +46,9 @@ import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.PistonCache; import org.geysermc.geyser.translator.collision.BlockCollision; +import org.geysermc.geyser.translator.collision.OtherCollision; import org.geysermc.geyser.translator.collision.ScaffoldingCollision; +import org.geysermc.geyser.translator.collision.SolidCollision; import org.geysermc.geyser.util.BlockUtils; import java.text.DecimalFormat; @@ -53,6 +56,8 @@ import java.text.DecimalFormatSymbols; import java.util.Locale; public class CollisionManager { + public static final BlockCollision SOLID_COLLISION = new SolidCollision(null); + public static final BlockCollision FLUID_COLLISION = new OtherCollision(new BoundingBox[]{new BoundingBox(0.5, 0.25, 0.5, 1, 0.5, 1)}); private final GeyserSession session; @@ -128,6 +133,21 @@ public class CollisionManager { playerBoundingBox.setSizeY(playerHeight); } + /** + * Gets the bounding box to use for player movement. + *

    + * This will return either the bounding box of a {@link ClientVehicle}, or the player's own bounding box. + * + * @return the bounding box to use for movement calculations + */ + public BoundingBox getActiveBoundingBox() { + if (session.getPlayerEntity().getVehicle() instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled()) { + return clientVehicle.getVehicleComponent().getBoundingBox(); + } + + return playerBoundingBox; + } + /** * Adjust the Bedrock position before sending to the Java server to account for inaccuracies in movement between * the two versions. Will also send corrected movement packets back to Bedrock if they collide with pistons. @@ -150,6 +170,15 @@ public class CollisionManager { Vector3d position = Vector3d.from(Double.parseDouble(Float.toString(bedrockPosition.getX())), javaY, Double.parseDouble(Float.toString(bedrockPosition.getZ()))); + // Don't correct position if controlling a vehicle + if (session.getPlayerEntity().getVehicle() instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled()) { + playerBoundingBox.setMiddleX(position.getX()); + playerBoundingBox.setMiddleY(position.getY() + playerBoundingBox.getSizeY() / 2); + playerBoundingBox.setMiddleZ(position.getZ()); + + return playerBoundingBox.getBottomCenter(); + } + Vector3d startingPos = playerBoundingBox.getBottomCenter(); Vector3d movement = position.sub(startingPos); Vector3d adjustedMovement = correctPlayerMovement(movement, false, teleported); @@ -173,7 +202,8 @@ public class CollisionManager { // Send corrected position to Bedrock if they differ by too much to prevent de-syncs if (onGround != newOnGround || movement.distanceSquared(adjustedMovement) > INCORRECT_MOVEMENT_THRESHOLD) { PlayerEntity playerEntity = session.getPlayerEntity(); - if (pistonCache.getPlayerMotion().equals(Vector3f.ZERO) && !pistonCache.isPlayerSlimeCollision()) { + // Client will dismount if on a vehicle + if (playerEntity.getVehicle() == null && pistonCache.getPlayerMotion().equals(Vector3f.ZERO) && !pistonCache.isPlayerSlimeCollision()) { playerEntity.moveAbsolute(position.toFloat(), playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), newOnGround, true); } } @@ -268,13 +298,13 @@ public class CollisionManager { if (teleported || (!checkWorld && session.getPistonCache().getPistons().isEmpty())) { // There is nothing to check return movement; } - return correctMovement(movement, playerBoundingBox, session.getPlayerEntity().isOnGround(), PLAYER_STEP_UP, checkWorld); + return correctMovement(movement, playerBoundingBox, session.getPlayerEntity().isOnGround(), PLAYER_STEP_UP, checkWorld, false); } - public Vector3d correctMovement(Vector3d movement, BoundingBox boundingBox, boolean onGround, double stepUp, boolean checkWorld) { + public Vector3d correctMovement(Vector3d movement, BoundingBox boundingBox, boolean onGround, double stepUp, boolean checkWorld, boolean walkOnLava) { Vector3d adjustedMovement = movement; if (!movement.equals(Vector3d.ZERO)) { - adjustedMovement = correctMovementForCollisions(movement, boundingBox, checkWorld); + adjustedMovement = correctMovementForCollisions(movement, boundingBox, checkWorld, walkOnLava); } boolean verticalCollision = adjustedMovement.getY() != movement.getY(); @@ -283,26 +313,27 @@ public class CollisionManager { onGround = onGround || (verticalCollision && falling); if (onGround && horizontalCollision) { Vector3d horizontalMovement = Vector3d.from(movement.getX(), 0, movement.getZ()); - Vector3d stepUpMovement = correctMovementForCollisions(horizontalMovement.up(stepUp), boundingBox, checkWorld); + Vector3d stepUpMovement = correctMovementForCollisions(horizontalMovement.up(stepUp), boundingBox, checkWorld, walkOnLava); BoundingBox stretchedBoundingBox = boundingBox.clone(); stretchedBoundingBox.extend(horizontalMovement); - double maxStepUp = correctMovementForCollisions(Vector3d.from(0, stepUp, 0), stretchedBoundingBox, checkWorld).getY(); + double maxStepUp = correctMovementForCollisions(Vector3d.from(0, stepUp, 0), stretchedBoundingBox, checkWorld, walkOnLava).getY(); if (maxStepUp < stepUp) { // The player collided with a block above them - boundingBox.translate(0, maxStepUp, 0); - Vector3d adjustedStepUpMovement = correctMovementForCollisions(horizontalMovement, boundingBox, checkWorld); - boundingBox.translate(0, -maxStepUp, 0); + BoundingBox stepUpBoundingBox = boundingBox.clone(); + stepUpBoundingBox.translate(0, maxStepUp, 0); + Vector3d adjustedStepUpMovement = correctMovementForCollisions(horizontalMovement, stepUpBoundingBox, checkWorld, walkOnLava); if (squaredHorizontalLength(adjustedStepUpMovement) > squaredHorizontalLength(stepUpMovement)) { stepUpMovement = adjustedStepUpMovement.up(maxStepUp); } } if (squaredHorizontalLength(stepUpMovement) > squaredHorizontalLength(adjustedMovement)) { - boundingBox.translate(stepUpMovement.getX(), stepUpMovement.getY(), stepUpMovement.getZ()); + BoundingBox stepUpBoundingBox = boundingBox.clone(); + stepUpBoundingBox.translate(stepUpMovement.getX(), stepUpMovement.getY(), stepUpMovement.getZ()); + // Apply the player's remaining vertical movement - double verticalMovement = correctMovementForCollisions(Vector3d.from(0, movement.getY() - stepUpMovement.getY(), 0), boundingBox, checkWorld).getY(); - boundingBox.translate(-stepUpMovement.getX(), -stepUpMovement.getY(), -stepUpMovement.getZ()); + double verticalMovement = correctMovementForCollisions(Vector3d.from(0, movement.getY() - stepUpMovement.getY(), 0), stepUpBoundingBox, checkWorld, walkOnLava).getY(); stepUpMovement = stepUpMovement.up(verticalMovement); adjustedMovement = stepUpMovement; @@ -315,43 +346,53 @@ public class CollisionManager { return vector.getX() * vector.getX() + vector.getZ() * vector.getZ(); } - private Vector3d correctMovementForCollisions(Vector3d movement, BoundingBox boundingBox, boolean checkWorld) { + private Vector3d correctMovementForCollisions(Vector3d movement, BoundingBox boundingBox, boolean checkWorld, boolean walkOnLava) { double movementX = movement.getX(); double movementY = movement.getY(); double movementZ = movement.getZ(); + // Position might change slightly due to floating point error + double originalX = boundingBox.getMiddleX(); + double originalY = boundingBox.getMiddleY(); + double originalZ = boundingBox.getMiddleZ(); + BoundingBox movementBoundingBox = boundingBox.clone(); movementBoundingBox.extend(movement); BlockPositionIterator iter = collidableBlocksIterator(movementBoundingBox); if (Math.abs(movementY) > CollisionManager.COLLISION_TOLERANCE) { - movementY = computeCollisionOffset(boundingBox, Axis.Y, movementY, iter, checkWorld); + movementY = computeCollisionOffset(boundingBox, Axis.Y, movementY, iter, checkWorld, walkOnLava); boundingBox.translate(0, movementY, 0); } boolean checkZFirst = Math.abs(movementZ) > Math.abs(movementX); if (checkZFirst && Math.abs(movementZ) > CollisionManager.COLLISION_TOLERANCE) { - movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, iter, checkWorld); + movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, iter, checkWorld, walkOnLava); boundingBox.translate(0, 0, movementZ); } if (Math.abs(movementX) > CollisionManager.COLLISION_TOLERANCE) { - movementX = computeCollisionOffset(boundingBox, Axis.X, movementX, iter, checkWorld); + movementX = computeCollisionOffset(boundingBox, Axis.X, movementX, iter, checkWorld, walkOnLava); boundingBox.translate(movementX, 0, 0); } if (!checkZFirst && Math.abs(movementZ) > CollisionManager.COLLISION_TOLERANCE) { - movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, iter, checkWorld); + movementZ = computeCollisionOffset(boundingBox, Axis.Z, movementZ, iter, checkWorld, walkOnLava); boundingBox.translate(0, 0, movementZ); } - boundingBox.translate(-movementX, -movementY, -movementZ); + boundingBox.setMiddleX(originalX); + boundingBox.setMiddleY(originalY); + boundingBox.setMiddleZ(originalZ); + return Vector3d.from(movementX, movementY, movementZ); } - private double computeCollisionOffset(BoundingBox boundingBox, Axis axis, double offset, BlockPositionIterator iter, boolean checkWorld) { + private double computeCollisionOffset(BoundingBox boundingBox, Axis axis, double offset, BlockPositionIterator iter, boolean checkWorld, boolean walkOnLava) { for (iter.reset(); iter.hasNext(); iter.next()) { int x = iter.getX(); int y = iter.getY(); int z = iter.getZ(); if (checkWorld) { - BlockCollision blockCollision = BlockUtils.getCollisionAt(session, x, y, z); + int blockId = session.getGeyser().getWorldManager().getBlockAt(session, x, y, z); + + BlockCollision blockCollision = walkOnLava ? getCollisionLavaWalking(blockId, y, boundingBox) : BlockUtils.getCollision(blockId); if (blockCollision != null && !(blockCollision instanceof ScaffoldingCollision)) { offset = blockCollision.computeCollisionOffset(x, y, z, boundingBox, axis, offset); } @@ -364,6 +405,16 @@ public class CollisionManager { return offset; } + /** + * @return the block collision appropriate for entities that can walk on lava (Strider) + */ + public BlockCollision getCollisionLavaWalking(int blockId, int blockY, BoundingBox boundingBox) { + if (BlockStateValues.getLavaLevel(blockId) == 0 && FLUID_COLLISION.isBelow(blockY, boundingBox)) { + return FLUID_COLLISION; + } + return BlockUtils.getCollision(blockId); + } + /** * @return true if the block located at the player's floor position plus 1 would intersect with the player, * were they not sneaking @@ -417,7 +468,7 @@ public class CollisionManager { double eyeY = playerBoundingBox.getMiddleY() - playerBoundingBox.getSizeY() / 2d + session.getEyeHeight(); double eyeZ = playerBoundingBox.getMiddleZ(); - eyeY -= 1 / ((double) BlockStateValues.NUM_WATER_LEVELS); // Subtract the height of one water layer + eyeY -= 1 / ((double) BlockStateValues.NUM_FLUID_LEVELS); // Subtract the height of one water layer int blockID = session.getGeyser().getWorldManager().getBlockAt(session, GenericMath.floor(eyeX), GenericMath.floor(eyeY), GenericMath.floor(eyeZ)); double waterHeight = BlockStateValues.getWaterHeight(blockID); diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/Direction.java b/core/src/main/java/org/geysermc/geyser/level/physics/Direction.java index f14a46999..4821734f3 100644 --- a/core/src/main/java/org/geysermc/geyser/level/physics/Direction.java +++ b/core/src/main/java/org/geysermc/geyser/level/physics/Direction.java @@ -38,6 +38,7 @@ public enum Direction { EAST(4, Vector3i.UNIT_X, Axis.X, org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction.EAST); public static final Direction[] VALUES = values(); + public static final Direction[] HORIZONTAL = new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; private final int reversedId; @Getter diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 9137c4756..607a58e0b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -128,6 +128,7 @@ import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.erosion.AbstractGeyserboundPacketHandler; import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler; import org.geysermc.geyser.impl.camera.CameraDefinitions; @@ -602,6 +603,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { */ private ScheduledFuture tickThread = null; + /** + * The number of ticks that have elapsed since the start of this session + */ + private int ticks; + + /** + * The world time in ticks according to the server + *

    + * Note: The TickingStatePacket is currently ignored. + */ + @Setter + private long worldTicks; + /** * Used to return the player to their original rotation after using an item in BedrockInventoryTransactionTranslator */ @@ -1261,6 +1275,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { isInWorldBorderWarningArea = false; } + Entity vehicle = playerEntity.getVehicle(); + if (vehicle instanceof ClientVehicle clientVehicle && vehicle.isValid()) { + clientVehicle.getVehicleComponent().tickVehicle(); + } for (Tickable entity : entityCache.getTickableEntities()) { entity.tick(); @@ -1296,6 +1314,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } catch (Throwable throwable) { throwable.printStackTrace(); } + + ticks++; + worldTicks++; } public void setAuthenticationData(AuthData authData) { diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/PistonCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/PistonCache.java index d0a5bc094..dee4aa7cf 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/PistonCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/PistonCache.java @@ -33,7 +33,9 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; +import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.level.physics.Axis; import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.session.GeyserSession; @@ -119,6 +121,12 @@ public class PistonCache { private void sendPlayerMovement() { if (!playerDisplacement.equals(Vector3d.ZERO) && playerMotion.equals(Vector3f.ZERO)) { SessionPlayerEntity playerEntity = session.getPlayerEntity(); + + Entity vehicle = playerEntity.getVehicle(); + if (vehicle instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled()) { + return; + } + boolean isOnGround = playerDisplacement.getY() > 0 || playerEntity.isOnGround(); Vector3d position = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter(); playerEntity.moveAbsolute(position.toFloat(), playerEntity.getYaw(), playerEntity.getPitch(), playerEntity.getHeadYaw(), isOnGround, true); @@ -128,6 +136,13 @@ public class PistonCache { private void sendPlayerMotion() { if (!playerMotion.equals(Vector3f.ZERO)) { SessionPlayerEntity playerEntity = session.getPlayerEntity(); + + Entity vehicle = playerEntity.getVehicle(); + if (vehicle instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled()) { + vehicle.setMotion(playerMotion); + return; + } + playerEntity.setMotion(playerMotion); SetEntityMotionPacket setEntityMotionPacket = new SetEntityMotionPacket(); @@ -149,10 +164,15 @@ public class PistonCache { totalDisplacement = totalDisplacement.max(-0.51d, -0.51d, -0.51d).min(0.51d, 0.51d, 0.51d); Vector3d delta = totalDisplacement.sub(playerDisplacement); - // Check if the piston is pushing a player into collision - delta = session.getCollisionManager().correctPlayerMovement(delta, true, false); - session.getCollisionManager().getPlayerBoundingBox().translate(delta.getX(), delta.getY(), delta.getZ()); + // Check if the piston is pushing a player into collision + if (session.getPlayerEntity().getVehicle() instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled()) { + delta = clientVehicle.getVehicleComponent().correctMovement(delta); + clientVehicle.getVehicleComponent().moveRelative(delta); + } else { + delta = session.getCollisionManager().correctPlayerMovement(delta, true, false); + session.getCollisionManager().getPlayerBoundingBox().translate(delta.getX(), delta.getY(), delta.getZ()); + } playerDisplacement = totalDisplacement; } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java index 8cb590f57..35579db13 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.GenericMath; import org.cloudburstmc.math.vector.Vector2d; +import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.LevelEventType; @@ -36,8 +37,12 @@ import lombok.Getter; import lombok.Setter; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.level.physics.Axis; +import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.session.GeyserSession; +import static org.geysermc.geyser.level.physics.CollisionManager.COLLISION_TOLERANCE; + public class WorldBorder { private static final double DEFAULT_WORLD_BORDER_SIZE = 5.9999968E7D; @@ -190,6 +195,53 @@ public class WorldBorder { return entityPosition.getX() > warningMinX && entityPosition.getX() < warningMaxX && entityPosition.getZ() > warningMinZ && entityPosition.getZ() < warningMaxZ; } + /** + * Adjusts the movement of an entity so that it does not cross the world border. + * + * @param boundingBox bounding box of the entity + * @param movement movement of the entity + * @return the corrected movement + */ + public Vector3d correctMovement(BoundingBox boundingBox, Vector3d movement) { + double correctedX; + if (movement.getX() < 0) { + correctedX = -limitMovement(-movement.getX(), boundingBox.getMin(Axis.X) - GenericMath.floor(minX)); + } else { + correctedX = limitMovement(movement.getX(), GenericMath.ceil(maxX) - boundingBox.getMax(Axis.X)); + } + + // Outside of border, don't adjust movement + if (Double.isNaN(correctedX)) { + return movement; + } + + double correctedZ; + if (movement.getZ() < 0) { + correctedZ = -limitMovement(-movement.getZ(), boundingBox.getMin(Axis.Z) - GenericMath.floor(minZ)); + } else { + correctedZ = limitMovement(movement.getZ(), GenericMath.ceil(maxZ) - boundingBox.getMax(Axis.Z)); + } + + if (Double.isNaN(correctedZ)) { + return movement; + } + + return Vector3d.from(correctedX, movement.getY(), correctedZ); + } + + private double limitMovement(double movement, double limit) { + if (limit < 0) { + // Return NaN to indicate outside of border + return Double.NaN; + } + + if (limit < COLLISION_TOLERANCE) { + return 0; + } + + return Math.min(movement, limit); + } + /** * Updates the world border's minimum and maximum properties */ diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/BlockCollision.java b/core/src/main/java/org/geysermc/geyser/translator/collision/BlockCollision.java index 2481028a4..bfe3f4417 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/collision/BlockCollision.java +++ b/core/src/main/java/org/geysermc/geyser/translator/collision/BlockCollision.java @@ -166,4 +166,22 @@ public class BlockCollision { } return offset; } + + /** + * Checks if this block collision is below the given bounding box. + * + * @param blockY the y position of the block in the world + * @param boundingBox the bounding box to compare + * @return true if this block collision is below the bounding box + */ + public boolean isBelow(int blockY, BoundingBox boundingBox) { + double minY = boundingBox.getMiddleY() - boundingBox.getSizeY() / 2; + for (BoundingBox b : boundingBoxes) { + double offset = blockY + b.getMiddleY() + b.getSizeY() / 2 - minY; + if (offset > CollisionManager.COLLISION_TOLERANCE) { + return false; + } + } + return true; + } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java index d1dd24855..b668a88cf 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java @@ -37,6 +37,7 @@ import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.level.block.property.Properties; @@ -347,18 +348,31 @@ public class PistonBlockEntity { blockMovement = 1f - lastProgress; } - BoundingBox playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox(); + boolean onGround; + BoundingBox playerBoundingBox; + if (session.getPlayerEntity().getVehicle() instanceof ClientVehicle clientVehicle && clientVehicle.isClientControlled()) { + onGround = session.getPlayerEntity().getVehicle().isOnGround(); + playerBoundingBox = clientVehicle.getVehicleComponent().getBoundingBox(); + } else { + onGround = session.getPlayerEntity().isOnGround(); + playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox(); + } + // Shrink the collision in the other axes slightly, to avoid false positives when pressed up against the side of blocks Vector3d shrink = Vector3i.ONE.sub(direction.abs()).toDouble().mul(CollisionManager.COLLISION_TOLERANCE * 2); - playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() - shrink.getX()); - playerBoundingBox.setSizeY(playerBoundingBox.getSizeY() - shrink.getY()); - playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() - shrink.getZ()); + double sizeX = playerBoundingBox.getSizeX(); + double sizeY = playerBoundingBox.getSizeY(); + double sizeZ = playerBoundingBox.getSizeZ(); + + playerBoundingBox.setSizeX(sizeX - shrink.getX()); + playerBoundingBox.setSizeY(sizeY - shrink.getY()); + playerBoundingBox.setSizeZ(sizeZ - shrink.getZ()); // Resolve collision with the piston head BlockState pistonHeadId = Blocks.PISTON_HEAD.defaultBlockState() .withValue(Properties.SHORT, false) .withValue(Properties.FACING, orientation); - pushPlayerBlock(pistonHeadId, getPistonHeadPos().toDouble(), blockMovement, playerBoundingBox); + pushPlayerBlock(pistonHeadId, getPistonHeadPos().toDouble(), blockMovement, playerBoundingBox, onGround); // Resolve collision with any attached moving blocks, but skip slime blocks // This prevents players from being launched by slime blocks covered by other blocks @@ -366,7 +380,7 @@ public class PistonBlockEntity { BlockState state = entry.getValue(); if (!state.is(Blocks.SLIME_BLOCK)) { Vector3d blockPos = entry.getKey().toDouble(); - pushPlayerBlock(state, blockPos, blockMovement, playerBoundingBox); + pushPlayerBlock(state, blockPos, blockMovement, playerBoundingBox, onGround); } } // Resolve collision with slime blocks @@ -374,14 +388,14 @@ public class PistonBlockEntity { BlockState state = entry.getValue(); if (state.is(Blocks.SLIME_BLOCK)) { Vector3d blockPos = entry.getKey().toDouble(); - pushPlayerBlock(state, blockPos, blockMovement, playerBoundingBox); + pushPlayerBlock(state, blockPos, blockMovement, playerBoundingBox, onGround); } } // Undo shrink - playerBoundingBox.setSizeX(playerBoundingBox.getSizeX() + shrink.getX()); - playerBoundingBox.setSizeY(playerBoundingBox.getSizeY() + shrink.getY()); - playerBoundingBox.setSizeZ(playerBoundingBox.getSizeZ() + shrink.getZ()); + playerBoundingBox.setSizeX(sizeX); + playerBoundingBox.setSizeY(sizeY); + playerBoundingBox.setSizeZ(sizeZ); } /** @@ -391,20 +405,22 @@ public class PistonBlockEntity { * @param playerBoundingBox The player's bounding box * @return True if the player attached, otherwise false */ - private boolean isPlayerAttached(Vector3d blockPos, BoundingBox playerBoundingBox) { + private boolean isPlayerAttached(Vector3d blockPos, BoundingBox playerBoundingBox, boolean onGround) { if (orientation.isVertical()) { return false; } - return session.getPlayerEntity().isOnGround() && HONEY_BOUNDING_BOX.checkIntersection(blockPos, playerBoundingBox); + return onGround && HONEY_BOUNDING_BOX.checkIntersection(blockPos, playerBoundingBox); } /** * Launches a player if the player is on the pushing side of the slime block * * @param blockPos The position of the slime block - * @param playerPos The player's position + * @param playerBoundingBox The player's bounding box */ - private void applySlimeBlockMotion(Vector3d blockPos, Vector3d playerPos) { + private void applySlimeBlockMotion(Vector3d blockPos, BoundingBox playerBoundingBox) { + Vector3d playerPos = Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ()); + Direction movementDirection = orientation; // Invert direction when pulling if (action == PistonValueType.PULLING) { @@ -470,7 +486,7 @@ public class PistonBlockEntity { return maxIntersection; } - private void pushPlayerBlock(BlockState state, Vector3d startingPos, double blockMovement, BoundingBox playerBoundingBox) { + private void pushPlayerBlock(BlockState state, Vector3d startingPos, double blockMovement, BoundingBox playerBoundingBox, boolean onGround) { PistonCache pistonCache = session.getPistonCache(); Vector3d movement = getMovement().toDouble(); // Check if the player collides with the movingBlock block entity @@ -480,12 +496,12 @@ public class PistonBlockEntity { if (state.is(Blocks.SLIME_BLOCK)) { pistonCache.setPlayerSlimeCollision(true); - applySlimeBlockMotion(finalBlockPos, Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ())); + applySlimeBlockMotion(finalBlockPos, playerBoundingBox); } } Vector3d blockPos = startingPos.add(movement.mul(blockMovement)); - if (state.is(Blocks.HONEY_BLOCK) && isPlayerAttached(blockPos, playerBoundingBox)) { + if (state.is(Blocks.HONEY_BLOCK) && isPlayerAttached(blockPos, playerBoundingBox, onGround)) { pistonCache.setPlayerCollided(true); pistonCache.setPlayerAttachedToHoney(true); @@ -508,7 +524,7 @@ public class PistonBlockEntity { if (state.is(Blocks.SLIME_BLOCK)) { pistonCache.setPlayerSlimeCollision(true); - applySlimeBlockMotion(blockPos, Vector3d.from(playerBoundingBox.getMiddleX(), playerBoundingBox.getMiddleY(), playerBoundingBox.getMiddleZ())); + applySlimeBlockMotion(blockPos, playerBoundingBox); } } } @@ -584,7 +600,7 @@ public class PistonBlockEntity { movingBlockMap.put(getPistonHeadPos(), this); Vector3i movement = getMovement(); - BoundingBox playerBoundingBox = session.getCollisionManager().getPlayerBoundingBox().clone(); + BoundingBox playerBoundingBox = session.getCollisionManager().getActiveBoundingBox().clone(); if (orientation == Direction.UP) { // Extend the bounding box down, to catch collisions when the player is falling down playerBoundingBox.extend(0, -256, 0); @@ -628,17 +644,19 @@ public class PistonBlockEntity { return; } placedFinalBlocks = true; + Vector3i movement = getMovement(); + BoundingBox playerBoundingBox = session.getCollisionManager().getActiveBoundingBox().clone(); attachedBlocks.forEach((blockPos, state) -> { blockPos = blockPos.add(movement); // Don't place blocks that collide with the player - if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) { + if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), playerBoundingBox)) { ChunkUtils.updateBlock(session, state, blockPos); } }); if (action == PistonValueType.PUSHING) { Vector3i pistonHeadPos = getPistonHeadPos().add(movement); - if (!SOLID_BOUNDING_BOX.checkIntersection(pistonHeadPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) { + if (!SOLID_BOUNDING_BOX.checkIntersection(pistonHeadPos.toDouble(), playerBoundingBox)) { ChunkUtils.updateBlock(session, Blocks.PISTON_HEAD.defaultBlockState() .withValue(Properties.SHORT, false) .withValue(Properties.FACING, orientation), pistonHeadPos); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java index beb724ffb..1498c2184 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java @@ -52,6 +52,8 @@ public class BedrockPlayerInputTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, RiderJumpPacket packet) { + session.getPlayerEntity().setVehicleJumpStrength(packet.getJumpStrength()); + Entity vehicle = session.getPlayerEntity().getVehicle(); if (vehicle instanceof AbstractHorseEntity) { ServerboundPlayerCommandPacket playerCommandPacket = new ServerboundPlayerCommandPacket(vehicle.getEntityId(), PlayerState.START_HORSE_JUMP, packet.getJumpStrength()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java index 601517523..ccd93ac97 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java @@ -101,6 +101,7 @@ public class JavaRespawnTranslator extends PacketTranslator Date: Fri, 16 Aug 2024 01:09:08 +0200 Subject: [PATCH 84/84] Fix: Invalid heads blocking inventory transactions (#4969) --- .../geyser/skin/FakeHeadProvider.java | 10 +++++-- .../translator/item/ItemTranslator.java | 28 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java index 6f6bcb0ae..22786a4ee 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/FakeHeadProvider.java @@ -112,7 +112,13 @@ public class FakeHeadProvider { return; } - Map textures = profile.getTextures(false); + Map textures; + try { + textures = profile.getTextures(false); + } catch (IllegalStateException e) { + GeyserImpl.getInstance().getLogger().debug("Could not decode player head from profile %s, got: %s".formatted(profile, e.getMessage())); + textures = null; + } if (textures == null || textures.isEmpty()) { loadHead(session, entity, profile.getName()); @@ -214,4 +220,4 @@ public class FakeHeadProvider { } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index aa0c3eb43..163eef20b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -25,9 +25,6 @@ package org.geysermc.geyser.translator.item; -import org.geysermc.mcprotocollib.auth.GameProfile; -import org.geysermc.mcprotocollib.auth.GameProfile.Texture; -import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.NonNull; @@ -43,8 +40,8 @@ import org.geysermc.geyser.api.block.custom.CustomBlockData; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.components.Rarity; -import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.item.type.BedrockRequiresTagItem; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -55,13 +52,24 @@ import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.InventoryUtils; +import org.geysermc.mcprotocollib.auth.GameProfile; +import org.geysermc.mcprotocollib.auth.GameProfile.Texture; +import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.*; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemAttributeModifiers; import java.text.DecimalFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public final class ItemTranslator { @@ -486,7 +494,13 @@ public final class ItemTranslator { GameProfile profile = components.get(DataComponentType.PROFILE); if (profile != null) { - Map textures = profile.getTextures(false); + Map textures; + try { + textures = profile.getTextures(false); + } catch (IllegalStateException e) { + GeyserImpl.getInstance().getLogger().debug("Could not decode player head from profile %s, got: %s".formatted(profile, e.getMessage())); + return null; + } if (textures == null || textures.isEmpty()) { return null;