From 3e01c436c9af55e0b40b52efb60c486275031051 Mon Sep 17 00:00:00 2001 From: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:16:15 -0700 Subject: [PATCH 01/65] 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 02/65] 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 22c59c465fe8990ed984a75ceae17fdc3af3016e Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 31 Jul 2024 19:21:29 +0200 Subject: [PATCH 03/65] 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 04/65] 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 05/65] 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 06/65] 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 07/65] 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 08/65] 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 09/65] 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 10/65] 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 11/65] 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 12/65] 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 13/65] 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 14/65] 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 15/65] 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 16/65] 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 17/65] 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 18/65] 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 19/65] 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 20/65] 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 21/65] 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 22/65] 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 23/65] 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 24/65] 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 25/65] 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 26/65] 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 27/65] 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 28/65] 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 29/65] 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 30/65] 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 31/65] 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 32/65] 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 33/65] 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 34/65] 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 35/65] 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; From bc7866541008f222554d93cde7ec45c344ed429d Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 21 Aug 2024 09:36:23 +0200 Subject: [PATCH 36/65] Indicate 1.21.21 support, update Bedrock protocol library dependencies (#4974) * Show 1.21.21 as being supported, bump Bedrock protocol library * Dont print debug --- README.md | 2 +- .../java/org/geysermc/geyser/network/GameProtocol.java | 2 +- gradle/libs.versions.toml | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5e586463c..fb93a8808 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.20 and Minecraft Java Server 1.21/1.21.1. For more info please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.20.80 - 1.21.21 and Minecraft Java 1.21/1.21.1. For more information, 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/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java index 422fa3d5a..baa1d24d0 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -50,7 +50,7 @@ public final class GameProtocol { * release of the game that Geyser supports. */ public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder() - .minecraftVersion("1.21.20") + .minecraftVersion("1.21.20/1.21.21") .build()); /** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 795d351ed..f49de8d20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,9 @@ 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-20240814.133201-7" +protocol-connection = "3.0.0.Beta3-20240819.124045-12" +protocol-common = "3.0.0.Beta3-20240819.124045-10" +protocol-codec = "3.0.0.Beta3-20240819.124045-13" raknet = "1.0.0.CR3-20240416.144209-1" minecraftauth = "4.1.1-20240806.235051-7" mcprotocollib = "1.21-20240725.013034-16" @@ -125,9 +127,9 @@ viaproxy = { group = "net.raphimc", name = "ViaProxy", version.ref = "viaproxy" viaversion = { group = "com.viaversion", name = "viaversion", version.ref = "viaversion" } websocket = { group = "org.java-websocket", name = "Java-WebSocket", version.ref = "websocket" } -protocol-common = { group = "org.cloudburstmc.protocol", name = "common", version.ref = "protocol" } -protocol-codec = { group = "org.cloudburstmc.protocol", name = "bedrock-codec", version.ref = "protocol" } -protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-connection", version.ref = "protocol" } +protocol-common = { group = "org.cloudburstmc.protocol", name = "common", version.ref = "protocol-common" } +protocol-codec = { group = "org.cloudburstmc.protocol", name = "bedrock-codec", version.ref = "protocol-codec" } +protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-connection", version.ref = "protocol-connection" } math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" } From b792f72ec70706ce1158d3745fd164e73edf6ae3 Mon Sep 17 00:00:00 2001 From: Tim203 Date: Wed, 21 Aug 2024 13:46:10 +0200 Subject: [PATCH 37/65] Fix invalid ping version causing "Unable to connect to world" --- .../geyser/network/netty/GeyserServer.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java index a67bd8a32..5166bde4d 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java @@ -84,10 +84,11 @@ public final class GeyserServer { /* The following constants are all used to ensure the ping does not reach a length where it is unparsable by the Bedrock client */ - private static final int MINECRAFT_VERSION_BYTES_LENGTH = GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion().getBytes(StandardCharsets.UTF_8).length; + private static final String PING_VERSION = pingVersion(); + private static final int PING_VERSION_BYTES_LENGTH = PING_VERSION.getBytes(StandardCharsets.UTF_8).length; private static final int BRAND_BYTES_LENGTH = GeyserImpl.NAME.getBytes(StandardCharsets.UTF_8).length; /** - * The MOTD, sub-MOTD and Minecraft version ({@link #MINECRAFT_VERSION_BYTES_LENGTH}) combined cannot reach this length. + * The MOTD, sub-MOTD and Minecraft version ({@link #PING_VERSION_BYTES_LENGTH}) combined cannot reach this length. */ private static final int MAGIC_RAKNET_LENGTH = 338; @@ -316,7 +317,7 @@ public final class GeyserServer { .gameType("Survival") // Can only be Survival or Creative as of 1.16.210.59 .nintendoLimited(false) .protocolVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) - .version(GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()) // Required to not be empty as of 1.16.210.59. Can only contain . and numbers. + .version(PING_VERSION) .ipv4Port(this.broadcastPort) .ipv6Port(this.broadcastPort) .serverId(channel.config().getOption(RakChannelOption.RAK_GUID)); @@ -367,15 +368,15 @@ public final class GeyserServer { // We don't know why, though byte[] motdArray = pong.motd().getBytes(StandardCharsets.UTF_8); int subMotdLength = pong.subMotd().getBytes(StandardCharsets.UTF_8).length; - if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH)) { + if (motdArray.length + subMotdLength > (MAGIC_RAKNET_LENGTH - PING_VERSION_BYTES_LENGTH)) { // Shorten the sub-MOTD first since that only appears locally if (subMotdLength > BRAND_BYTES_LENGTH) { pong.subMotd(GeyserImpl.NAME); subMotdLength = BRAND_BYTES_LENGTH; } - if (motdArray.length > (MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength)) { + if (motdArray.length > (MAGIC_RAKNET_LENGTH - PING_VERSION_BYTES_LENGTH - subMotdLength)) { // If the top MOTD is still too long, we chop it down - byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - MINECRAFT_VERSION_BYTES_LENGTH - subMotdLength]; + byte[] newMotdArray = new byte[MAGIC_RAKNET_LENGTH - PING_VERSION_BYTES_LENGTH - subMotdLength]; System.arraycopy(motdArray, 0, newMotdArray, 0, newMotdArray.length); pong.motd(new String(newMotdArray, StandardCharsets.UTF_8)); } @@ -390,6 +391,17 @@ public final class GeyserServer { return pong; } + private static String pingVersion() { + // BedrockPong version is required to not be empty as of 1.16.210.59. + // Can only contain . and numbers, so use the latest version instead of sending all + var version = GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion(); + var versionSplit = version.split("/"); + if (versionSplit.length > 1) { + version = versionSplit[versionSplit.length - 1]; + } + return version; + } + /** * @return the throwable from the given supplier, or the throwable caught while calling the supplier. */ From 6801338ff991dcad75bac09ca4edf44547630a13 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 22 Aug 2024 00:50:16 -0400 Subject: [PATCH 38/65] Another route of ensuring /help goes through to Bedrock --- .../protocol/java/JavaCommandsTranslator.java | 15 +++++++++++++++ 1 file changed, 15 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 01da23809..6bbf0eb33 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 @@ -88,6 +88,7 @@ public class JavaCommandsTranslator extends PacketTranslator flags = Set.of(CommandData.Flag.NOT_CHEAT); + boolean helpAdded = false; + // Loop through all the found commands for (Map.Entry> entry : commands.entrySet()) { String commandName = entry.getValue().iterator().next(); // We know this has a value @@ -198,6 +201,18 @@ public class JavaCommandsTranslator extends PacketTranslator Date: Thu, 22 Aug 2024 13:16:07 -0400 Subject: [PATCH 39/65] Show the help command even with command suggestions disabled --- .../protocol/java/JavaCommandsTranslator.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 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 6bbf0eb33..f189658cd 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 @@ -125,8 +125,9 @@ public class JavaCommandsTranslator extends PacketTranslator Date: Sun, 25 Aug 2024 21:31:03 +0200 Subject: [PATCH 40/65] Feature: Modrinth version names (#4989) * Feature: Version names on modrinth published builds * Also change the fabric/neoforge jar file names --- build-logic/src/main/kotlin/extensions.kt | 9 +++++++++ .../src/main/kotlin/geyser.modded-conventions.gradle.kts | 2 +- .../geyser.modrinth-uploading-conventions.gradle.kts | 3 ++- core/build.gradle.kts | 5 +---- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt index 41e11344b..1b81f6601 100644 --- a/build-logic/src/main/kotlin/extensions.kt +++ b/build-logic/src/main/kotlin/extensions.kt @@ -118,3 +118,12 @@ open class DownloadFilesTask : DefaultTask() { private fun calcExclusion(section: String, bit: Int, excludedOn: Int): String = if (excludedOn and bit > 0) section else "" +fun projectVersion(project: Project): String = + project.version.toString().replace("SNAPSHOT", "b" + buildNumber()) + +fun versionName(project: Project): String = + "Geyser-" + project.name.replaceFirstChar { it.uppercase() } + "-" + projectVersion(project) + +fun buildNumber(): Int = + (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1 + 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 8a6602778..8584c13d4 100644 --- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts @@ -87,7 +87,7 @@ tasks { register("remapModrinthJar", RemapJarTask::class) { dependsOn(shadowJar) inputFile.set(shadowJar.get().archiveFile) - archiveVersion.set(project.version.toString() + "+build." + System.getenv("BUILD_NUMBER")) + archiveVersion.set(versionName(project)) archiveClassifier.set("") } } 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 fe2284137..d2e207fa4 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 @@ -8,7 +8,8 @@ tasks.modrinth.get().dependsOn(tasks.modrinthSyncBody) modrinth { token.set(System.getenv("MODRINTH_TOKEN") ?: "") // Even though this is the default value, apparently this prevents GitHub Actions caching the token? projectId.set("geyser") - versionNumber.set(project.version as String + "-" + System.getenv("BUILD_NUMBER")) + versionName.set(versionName(project)) + versionNumber.set(projectVersion(project)) versionType.set("beta") changelog.set(System.getenv("CHANGELOG") ?: "") gameVersions.addAll("1.21", libs.minecraft.get().version as String) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index acd6c5147..8d022271b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -103,9 +103,6 @@ sourceSets { } } -fun buildNumber(): Int = - (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1 - fun isDevBuild(branch: String, repository: String): Boolean { return branch != "master" || repository.equals("https://github.com/GeyserMC/Geyser", ignoreCase = true).not() } @@ -139,7 +136,7 @@ inner class GitInfo { buildNumber = buildNumber() isDev = isDevBuild(branch, repository) - val projectVersion = if (isDev) project.version else project.version.toString().replace("SNAPSHOT", "b${buildNumber}") + val projectVersion = if (isDev) project.version else projectVersion(project) version = "$projectVersion ($gitVersion)" } } From 3be9b8a183aa2fe798b6b8d19e95eb74ba096f0a Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Mon, 26 Aug 2024 00:48:42 -0400 Subject: [PATCH 41/65] Fix moving items from output slot over multiple actions (#4993) * Fix moving items from output slot over multiple actions * Refactor and use temp slot * Ensure source slot is not cursor --- .../geyser/inventory/click/ClickPlan.java | 21 +++ .../inventory/InventoryTranslator.java | 146 ++++++++++++++---- 2 files changed, 137 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java index 53b02ef88..9d6f4d3e3 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java @@ -162,6 +162,27 @@ public final class ClickPlan { finished = true; } + public Inventory getInventory() { + return inventory; + } + + /** + * Test if the item stacks with another item in the specified slot. + * This will check the simulated inventory without copying. + */ + public boolean canStack(int slot, GeyserItemStack item) { + GeyserItemStack slotItem = simulatedItems.getOrDefault(slot, inventory.getItem(slot)); + return InventoryUtils.canStack(slotItem, item); + } + + /** + * Test if the specified slot is empty. + * This will check the simulated inventory without copying. + */ + public boolean isEmpty(int slot) { + return simulatedItems.getOrDefault(slot, inventory.getItem(slot)).isEmpty(); + } + public GeyserItemStack getItem(int slot) { return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy()); } 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 546ebda19..afa11c982 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 @@ -201,6 +201,9 @@ public abstract class InventoryTranslator { public ItemStackResponse translateRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { ClickPlan plan = new ClickPlan(session, this, inventory); IntSet affectedSlots = new IntOpenHashSet(); + int pendingOutput = 0; + int savedTempSlot = -1; + for (ItemStackRequestAction action : request.getActions()) { GeyserItemStack cursor = session.getPlayerInventory().getCursor(); switch (action.getType()) { @@ -241,6 +244,65 @@ public abstract class InventoryTranslator { return rejectRequest(request, false); } + // Handle partial transfer of output slot + if (pendingOutput == 0 && !isSourceCursor && getSlotType(sourceSlot) == SlotType.OUTPUT + && transferAction.getCount() < plan.getItem(sourceSlot).getAmount()) { + // Cursor as dest should always be full transfer. + if (isDestCursor) { + return rejectRequest(request); + } + + if (!plan.getCursor().isEmpty()) { + savedTempSlot = findTempSlot(plan, plan.getCursor(), true); + if (savedTempSlot == -1) { + return rejectRequest(request); + } + plan.add(Click.LEFT, savedTempSlot); + } + + // Pickup entire stack from output + pendingOutput = plan.getItem(sourceSlot).getAmount(); + plan.add(Click.LEFT, sourceSlot); + } + + // Continue transferring items from output that is currently stored in the cursor + if (pendingOutput > 0) { + if (isSourceCursor || getSlotType(sourceSlot) != SlotType.OUTPUT + || transferAction.getCount() > pendingOutput + || destSlot == savedTempSlot + || isDestCursor) { + return rejectRequest(request); + } + + // Make sure item can be placed here + GeyserItemStack destItem = plan.getItem(destSlot); + if (!destItem.isEmpty() && !InventoryUtils.canStack(destItem, plan.getCursor())) { + return rejectRequest(request); + } + + // TODO: Optimize using max stack size + if (pendingOutput == transferAction.getCount()) { + plan.add(Click.LEFT, destSlot); + } else { + for (int i = 0; i < transferAction.getCount(); i++) { + plan.add(Click.RIGHT, destSlot); + } + } + + pendingOutput -= transferAction.getCount(); + if (pendingOutput != plan.getCursor().getAmount()) { + return rejectRequest(request); + } + + if (pendingOutput == 0 && savedTempSlot != -1) { + plan.add(Click.LEFT, savedTempSlot); + savedTempSlot = -1; + } + + // Skip to next action + continue; + } + if (isSourceCursor && isDestCursor) { //??? return rejectRequest(request); } else if (isSourceCursor) { //releasing cursor @@ -271,7 +333,7 @@ public abstract class InventoryTranslator { return rejectRequest(request); } if (transferAction.getCount() != sourceAmount) { - int tempSlot = findTempSlot(inventory, cursor, false, sourceSlot); + int tempSlot = findTempSlot(plan, cursor, false, sourceSlot); if (tempSlot == -1) { return rejectRequest(request); } @@ -292,7 +354,7 @@ public abstract class InventoryTranslator { } else { //transfer from one slot to another int tempSlot = -1; if (!plan.getCursor().isEmpty()) { - tempSlot = findTempSlot(inventory, cursor, false, sourceSlot, destSlot); + tempSlot = findTempSlot(plan, cursor, getSlotType(sourceSlot) != SlotType.NORMAL, sourceSlot, destSlot); if (tempSlot == -1) { return rejectRequest(request); } @@ -440,6 +502,11 @@ public abstract class InventoryTranslator { return rejectRequest(request); } } + + if (pendingOutput != 0) { + return rejectRequest(request); + } + plan.execute(false); affectedSlots.addAll(plan.getAffectedSlots()); return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); @@ -536,7 +603,7 @@ public abstract class InventoryTranslator { } } else { GeyserItemStack cursor = session.getPlayerInventory().getCursor(); - int tempSlot = findTempSlot(inventory, cursor, true, sourceSlot, destSlot); + int tempSlot = findTempSlot(plan, cursor, true, sourceSlot, destSlot); if (tempSlot == -1) { return rejectRequest(request); } @@ -699,7 +766,7 @@ public abstract class InventoryTranslator { int javaSlot = bedrockSlotToJava(transferAction.getDestination()); if (isCursor(transferAction.getDestination())) { //TODO if (timesCrafted > 1) { - tempSlot = findTempSlot(inventory, GeyserItemStack.from(output), true); + tempSlot = findTempSlot(plan, GeyserItemStack.from(output), true); if (tempSlot == -1) { return rejectRequest(request); } @@ -836,49 +903,68 @@ public abstract class InventoryTranslator { } /** - * Try to find a slot that can temporarily store the given item. + * Try to find a slot that is preferably empty, or does not stack with a given item. * Only looks in the main inventory and hotbar (excluding offhand). - * Only slots that are empty or contain a different type of item are valid. + *

+ * Slots are searched in the reverse order that the bedrock client uses for quick moving. * - * @return java id for the temporary slot, or -1 if no viable slot was found + * @param plan used to check the simulated inventory + * @param item the item to temporarily store + * @param emptyOnly if only empty slots should be considered + * @param slotBlacklist list of slots to exclude; the items contained in these slots will also be checked for stacking + * @return the temp slot, or -1 if no suitable slot was found */ - //TODO: compatibility for simulated inventory (ClickPlan) - private static int findTempSlot(Inventory inventory, GeyserItemStack item, boolean emptyOnly, int... slotBlacklist) { - int offset = inventory.getJavaId() == 0 ? 1 : 0; //offhand is not a viable temp slot - HashSet itemBlacklist = new HashSet<>(slotBlacklist.length + 1); - itemBlacklist.add(item); + private static int findTempSlot(ClickPlan plan, GeyserItemStack item, boolean emptyOnly, int... slotBlacklist) { + IntSortedSet potentialSlots = new IntLinkedOpenHashSet(PLAYER_INVENTORY_SIZE); + int hotbarOffset = plan.getInventory().getOffsetForHotbar(0); - IntSet potentialSlots = new IntOpenHashSet(36); - for (int i = inventory.getSize() - (36 + offset); i < inventory.getSize() - offset; i++) { + // Add main inventory slots in reverse + for (int i = hotbarOffset - 1; i >= hotbarOffset - 27; i--) { potentialSlots.add(i); } + + // Add hotbar slots in reverse + for (int i = hotbarOffset + 8; i >= hotbarOffset; i--) { + potentialSlots.add(i); + } + for (int i : slotBlacklist) { potentialSlots.remove(i); - GeyserItemStack blacklistedItem = inventory.getItem(i); - if (!blacklistedItem.isEmpty()) { - itemBlacklist.add(blacklistedItem); + } + + // Prefer empty slots + IntIterator it = potentialSlots.iterator(); + while (it.hasNext()) { + int slot = it.nextInt(); + if (plan.isEmpty(slot)) { + return slot; } } - for (int i : potentialSlots) { - GeyserItemStack testItem = inventory.getItem(i); - if ((emptyOnly && !testItem.isEmpty())) { + if (emptyOnly) { + return -1; + } + + // No empty slots. Look for a slot that does not stack + it = potentialSlots.iterator(); + + outer: + while (it.hasNext()) { + int slot = it.nextInt(); + if (plan.canStack(slot, item)) { continue; } - boolean viable = true; - for (GeyserItemStack blacklistedItem : itemBlacklist) { - if (InventoryUtils.canStack(testItem, blacklistedItem)) { - viable = false; - break; + for (int blacklistedSlot : slotBlacklist) { + GeyserItemStack blacklistedItem = plan.getItem(blacklistedSlot); + if (plan.canStack(slot, blacklistedItem)) { + continue outer; } } - if (!viable) { - continue; - } - return i; + + return slot; } - //could not find a viable temp slot + return -1; } From 46b2c032154502378c2dcd20054703cc69deeae2 Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Tue, 27 Aug 2024 05:37:01 -0400 Subject: [PATCH 42/65] Fix rare netid desync when crafting (#4997) --- .../geyser/translator/inventory/PlayerInventoryTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java index bc6ff2adf..21f45a5ca 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java @@ -123,7 +123,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator { if (session.getGameMode() == GameMode.CREATIVE) { slotPacket.setItem(UNUSUABLE_CRAFTING_SPACE_BLOCK.apply(session.getUpstream().getProtocolVersion())); } else { - slotPacket.setItem(ItemTranslator.translateToBedrock(session, inventory.getItem(i).getItemStack())); + slotPacket.setItem(inventory.getItem(i).getItemData(session)); } session.sendUpstreamPacket(slotPacket); From 8356b63f5d52e8f8e61a7d0af71c924843503083 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 27 Aug 2024 18:40:22 +0200 Subject: [PATCH 43/65] Fix: Structure blocks/voids in recipes (#4999) * Fix: Structure blocks/voids in recipes * add gh issue link --- .../geyser/registry/populator/ItemRegistryPopulator.java | 5 +++++ 1 file changed, 5 insertions(+) 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 f11b58bfe..55d44c3d9 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 @@ -171,6 +171,11 @@ public class ItemRegistryPopulator { List creativeItems = new ArrayList<>(); Set noBlockDefinitions = new ObjectOpenHashSet<>(); + // Fix: Usage of structure blocks/voids in recipes + // https://github.com/GeyserMC/Geyser/issues/2890 + noBlockDefinitions.add("minecraft:structure_block"); + noBlockDefinitions.add("minecraft:structure_void"); + AtomicInteger creativeNetId = new AtomicInteger(); CreativeItemRegistryPopulator.populate(palette, definitions, itemBuilder -> { ItemData item = itemBuilder.netId(creativeNetId.incrementAndGet()).build(); From 1b17c6bd8e03c8ebc9c8949b6ea7a5888e6dea43 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:50:26 -0400 Subject: [PATCH 44/65] Fix loom usage, and disconnect messages for all outdated clients (#5006) --- .../geysermc/geyser/network/UpstreamPacketHandler.java | 9 +++++---- gradle/libs.versions.toml | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) 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 e9c979b0c..adcaa2505 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -120,10 +120,11 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { session.disconnect(disconnectMessage); return false; } else if (protocolVersion < GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()) { - if (protocolVersion < Bedrock_v622.CODEC.getProtocolVersion()) { - // https://github.com/GeyserMC/Geyser/issues/4378 - session.getUpstream().getSession().setCodec(BedrockCompat.CODEC_LEGACY); - } + // A note on the following line: various older client versions have different forms of DisconnectPacket. + // Using only the latest BedrockCompat for such clients leads to inaccurate disconnect messages: https://github.com/GeyserMC/Geyser/issues/4378 + // This updates the BedrockCompat protocol if necessary: + session.getUpstream().getSession().setCodec(BedrockCompat.disconnectCompat(protocolVersion)); + session.disconnect(GeyserLocale.getLocaleStringLog("geyser.network.outdated.client", supportedVersions)); return false; } else { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f49de8d20..c564720a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,9 @@ 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-connection = "3.0.0.Beta3-20240819.124045-12" -protocol-common = "3.0.0.Beta3-20240819.124045-10" -protocol-codec = "3.0.0.Beta3-20240819.124045-13" +protocol-connection = "3.0.0.Beta4-20240828.162251-1" +protocol-common = "3.0.0.Beta4-20240828.162251-1" +protocol-codec = "3.0.0.Beta4-20240828.162251-1" raknet = "1.0.0.CR3-20240416.144209-1" minecraftauth = "4.1.1-20240806.235051-7" mcprotocollib = "1.21-20240725.013034-16" From 63e60bc93cfc50bdaafafde6f39f6ebbe3f3d93c Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:09:36 -0400 Subject: [PATCH 45/65] Allow Bedrock players to sleep on Fabric Fixes #5001 --- .../entity/player/BedrockMovePlayerTranslator.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java index 3faa3242b..6b403a99b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -58,6 +58,14 @@ public class BedrockMovePlayerTranslator extends PacketTranslator Date: Fri, 30 Aug 2024 22:16:45 -0400 Subject: [PATCH 46/65] Ensure players aren't floating in their beds; improve Bedrock sleeping notifications --- .../entity/type/player/PlayerEntity.java | 14 +++++- .../type/player/SessionPlayerEntity.java | 2 +- .../geyser/network/UpstreamPacketHandler.java | 1 - .../player/BedrockMovePlayerTranslator.java | 2 +- .../java/JavaSystemChatTranslator.java | 43 ++++++++++++++++--- 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index 4c67b882f..b326f2e04 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -42,7 +42,11 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; -import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; +import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket; +import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; @@ -278,7 +282,13 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { @Override public void setPosition(Vector3f position) { - super.setPosition(position.add(0, definition.offset(), 0)); + if (this.bedPosition != null) { + // As of Bedrock 1.21.22 and Fabric 1.21.1 + // Messes with Bedrock if we send this to the client itself, though. + super.setPosition(position.up(0.2f)); + } else { + super.setPosition(position.add(0, definition.offset(), 0)); + } } @Override 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 ccf2d25e6..f427b001a 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 @@ -140,7 +140,7 @@ public class SessionPlayerEntity extends PlayerEntity { if (valid) { // Don't update during session init session.getCollisionManager().updatePlayerBoundingBox(position); } - super.setPosition(position); + this.position = position.add(0, definition.offset(), 0); } /** 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 adcaa2505..5c48df1f9 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -29,7 +29,6 @@ import io.netty.buffer.Unpooled; import org.cloudburstmc.protocol.bedrock.BedrockDisconnectReasons; import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.cloudburstmc.protocol.bedrock.codec.compat.BedrockCompat; -import org.cloudburstmc.protocol.bedrock.codec.v622.Bedrock_v622; import org.cloudburstmc.protocol.bedrock.data.ExperimentData; import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; import org.cloudburstmc.protocol.bedrock.data.ResourcePackType; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java index 6b403a99b..ee80cac16 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java @@ -61,7 +61,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, ClientboundSystemChatPacket packet) { - if (packet.getContent() instanceof TranslatableComponent component && component.key().equals("chat.disabled.missingProfileKey")) { - // We likely got this message as a response to a player trying to chat - // As there SHOULD be no false flags for this, print every time it shows up in chat. - if (Boolean.parseBoolean(System.getProperty("Geyser.PrintSecureChatInformation", "true"))) { - session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_1", session.locale())); - session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_2", session.locale(), "https://geysermc.link/secure-chat")); + if (packet.getContent() instanceof TranslatableComponent component) { + if (component.key().equals("chat.disabled.missingProfileKey")) { + // We likely got this message as a response to a player trying to chat + // As there SHOULD be no false flags for this, print every time it shows up in chat. + if (Boolean.parseBoolean(System.getProperty("Geyser.PrintSecureChatInformation", "true"))) { + session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_1", session.locale())); + session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_2", session.locale(), "https://geysermc.link/secure-chat")); + } + } else if (component.key().equals("sleep.players_sleeping")) { + if (component.arguments().size() == 2) { + // Hack FYI, but it allows Bedrock players to easily understand this information + // without it being covered up or saying the night is being slept through. + int numPlayersSleeping = ((Number) component.arguments().get(0).value()).intValue(); + int totalPlayersNeeded = ((Number) component.arguments().get(1).value()).intValue(); + LevelEventGenericPacket sleepInfoPacket = new LevelEventGenericPacket(); + sleepInfoPacket.setType(LevelEvent.SLEEPING_PLAYERS); + sleepInfoPacket.setTag(NbtMap.builder() + .putInt("ableToSleep", totalPlayersNeeded) + .putInt("overworldPlayerCount", totalPlayersNeeded) + .putInt("sleepingPlayerCount", numPlayersSleeping) + .build()); + session.sendUpstreamPacket(sleepInfoPacket); + } + } else if (component.key().equals("sleep.skipping_night")) { + LevelEventGenericPacket sleepInfoPacket = new LevelEventGenericPacket(); + sleepInfoPacket.setType(LevelEvent.SLEEPING_PLAYERS); + sleepInfoPacket.setTag(NbtMap.builder() + .putInt("ableToSleep", 1) + .putInt("overworldPlayerCount", 1) + .putInt("sleepingPlayerCount", 1) + .build()); + session.sendUpstreamPacket(sleepInfoPacket); } } From 1ab3f1f2e06de06720154f205a2f5bcf8402bf31 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Sun, 1 Sep 2024 15:34:53 -0400 Subject: [PATCH 47/65] Improve command registration timing on Velocity/BungeeCord (#5013) --- .../bungeecord/GeyserBungeePlugin.java | 28 +++++++++-------- .../neoforge/GeyserNeoForgeBootstrap.java | 1 + .../platform/spigot/GeyserSpigotPlugin.java | 2 +- .../velocity/GeyserVelocityPlugin.java | 30 ++++++++++--------- 4 files changed, 33 insertions(+), 28 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 e2735c80e..7adfd488f 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 @@ -111,6 +111,21 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { if (geyser == null) { return; // Config did not load properly! } + + // After Geyser initialize for parity with other platforms. + 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 + // 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 @@ -150,19 +165,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { } this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - } else { - 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 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 7d3b9dc5f..ad56eda39 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 @@ -82,6 +82,7 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap { ); GeyserNeoForgeCommandRegistry registry = new GeyserNeoForgeCommandRegistry(getGeyser(), cloud); this.setCommandRegistry(registry); + // An auxiliary listener for registering undefined permissions belonging to commands. See javadocs for more info. NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, registry::onPermissionGatherForUndefined); } 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 c52927a83..a2d5c992b 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 @@ -181,7 +181,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { return; } - // Create command manager early so we can add Geyser extension commands + // Register commands after Geyser initialization, but before the server starts. var sourceConverter = new CommandSourceConverter<>( CommandSender.class, Bukkit::getPlayer, 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 413355813..8fa47f569 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 @@ -109,6 +109,22 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this); this.geyserInjector = new GeyserVelocityInjector(proxyServer); + + // We need to register commands here, rather than in onGeyserEnable which is invoked during the appropriate ListenerBoundEvent. + // Reason: players can connect after a listener is bound, and a player join locks registration to the cloud CommandManager. + 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 } @Override @@ -123,20 +139,6 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { } this.geyserLogger.setDebug(geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - } else { - 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(); From 74034f078338e3bb27092b72e9067e6f5dd3968a Mon Sep 17 00:00:00 2001 From: RK_01 <50594595+RaphiMC@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:08:43 +0200 Subject: [PATCH 48/65] Fix PARTICLES_DRAGON_BLOCK_BREAK translation (#5015) --- .../protocol/java/level/JavaLevelEventTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c10cfa018..f54de1b68 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 @@ -293,7 +293,7 @@ public class JavaLevelEventTranslator extends PacketTranslator { effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_GENERIC_SPAWN); - effectPacket.setData(61); + effectPacket.setData(65); } case PARTICLES_WATER_EVAPORATING -> { effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_EVAPORATE_WATER); From 65cb15400a4ac28d9a622004713b7ac59969e90e Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 3 Sep 2024 01:04:44 +0200 Subject: [PATCH 49/65] Fix: Broadcast port system property not being read on Geyser-Standalone (#4942) * this is supposed to work on standalone aswell * Update core/src/main/java/org/geysermc/geyser/GeyserImpl.java Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> * Update core/src/main/java/org/geysermc/geyser/GeyserImpl.java Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> * address review --------- Co-authored-by: Konicai <71294714+Konicai@users.noreply.github.com> --- .../java/org/geysermc/geyser/GeyserImpl.java | 36 ++++++++++--------- .../geyser/network/netty/GeyserServer.java | 5 --- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 8febf4d21..bc6108abf 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -363,22 +363,6 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { } } - String broadcastPort = System.getProperty("geyserBroadcastPort", ""); - if (!broadcastPort.isEmpty()) { - int parsedPort; - try { - parsedPort = Integer.parseInt(broadcastPort); - if (parsedPort < 1 || parsedPort > 65535) { - throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!"); - } - } catch (NumberFormatException e) { - logger.error(String.format("Invalid broadcast port: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")")); - parsedPort = config.getBedrock().port(); - } - config.getBedrock().setBroadcastPort(parsedPort); - logger.info("Broadcast port set from system property: " + parsedPort); - } - if (platformType != PlatformType.VIAPROXY) { boolean floodgatePresent = bootstrap.testFloodgatePluginPresent(); if (config.getRemote().authType() == AuthType.FLOODGATE && !floodgatePresent) { @@ -393,6 +377,26 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { } } + // Now that the Bedrock port may have been changed, also check the broadcast port (configurable on all platforms) + String broadcastPort = System.getProperty("geyserBroadcastPort", ""); + if (!broadcastPort.isEmpty()) { + try { + int parsedPort = Integer.parseInt(broadcastPort); + if (parsedPort < 1 || parsedPort > 65535) { + throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!"); + } + config.getBedrock().setBroadcastPort(parsedPort); + logger.info("Broadcast port set from system property: " + parsedPort); + } catch (NumberFormatException e) { + logger.error(String.format("Invalid broadcast port from system property: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")")); + } + } + + // It's set to 0 only if no system property or manual config value was set + if (config.getBedrock().broadcastPort() == 0) { + config.getBedrock().setBroadcastPort(config.getBedrock().port()); + } + String remoteAddress = config.getRemote().address(); // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry. if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) { diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java index 5166bde4d..efbd8bdff 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java @@ -144,11 +144,6 @@ public final class GeyserServer { this.proxiedAddresses = null; } - // It's set to 0 only if no system property or manual config value was set - if (geyser.getConfig().getBedrock().broadcastPort() == 0) { - geyser.getConfig().getBedrock().setBroadcastPort(geyser.getConfig().getBedrock().port()); - } - this.broadcastPort = geyser.getConfig().getBedrock().broadcastPort(); } From f8884568ee717cda9314fb40ae0c7913bd601596 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Tue, 3 Sep 2024 00:54:50 -0400 Subject: [PATCH 50/65] Gradle: avoid cross-configuration and enable configuration-on-demand (#5012) --- ap/build.gradle.kts | 3 + bootstrap/bungeecord/build.gradle.kts | 5 ++ bootstrap/mod/build.gradle.kts | 4 ++ bootstrap/mod/fabric/build.gradle.kts | 5 ++ bootstrap/mod/neoforge/build.gradle.kts | 13 ++-- bootstrap/spigot/build.gradle.kts | 5 ++ bootstrap/standalone/build.gradle.kts | 1 + bootstrap/velocity/build.gradle.kts | 5 ++ bootstrap/viaproxy/build.gradle.kts | 4 ++ build-logic/build.gradle.kts | 5 ++ build-logic/settings.gradle.kts | 2 +- .../kotlin/geyser.base-conventions.gradle.kts | 69 ++++++++++++++----- .../main/kotlin/geyser.build-logic.gradle.kts | 45 ------------ .../geyser.modded-conventions.gradle.kts | 4 +- .../geyser.platform-conventions.gradle.kts | 19 ++++- build.gradle.kts | 52 +------------- common/build.gradle.kts | 1 + core/build.gradle.kts | 1 + gradle.properties | 1 + gradle/libs.versions.toml | 2 +- 20 files changed, 123 insertions(+), 123 deletions(-) delete mode 100644 build-logic/src/main/kotlin/geyser.build-logic.gradle.kts diff --git a/ap/build.gradle.kts b/ap/build.gradle.kts index e69de29bb..6c456c21b 100644 --- a/ap/build.gradle.kts +++ b/ap/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("geyser.base-conventions") +} diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 5fe7ea3d1..1564b7f75 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("geyser.platform-conventions") + id("geyser.modrinth-uploading-conventions") +} + dependencies { api(projects.core) diff --git a/bootstrap/mod/build.gradle.kts b/bootstrap/mod/build.gradle.kts index 57f11b2c7..c43f123ec 100644 --- a/bootstrap/mod/build.gradle.kts +++ b/bootstrap/mod/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("geyser.modded-conventions") +} + architectury { common("neoforge", "fabric") } diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts index fd9d7e99d..56bec322e 100644 --- a/bootstrap/mod/fabric/build.gradle.kts +++ b/bootstrap/mod/fabric/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("geyser.modded-conventions") + id("geyser.modrinth-uploading-conventions") +} + architectury { platformSetupLoomIde() fabric() diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts index 81a35a58b..4ab005b4f 100644 --- a/bootstrap/mod/neoforge/build.gradle.kts +++ b/bootstrap/mod/neoforge/build.gradle.kts @@ -1,13 +1,18 @@ -// 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") +plugins { + id("geyser.modded-conventions") + id("geyser.modrinth-uploading-conventions") +} architectury { platformSetupLoomIde() neoForge() } +// 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") + val includeTransitive: Configuration = configurations.getByName("includeTransitive") dependencies { diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts index f680b1949..feabfdd7a 100644 --- a/bootstrap/spigot/build.gradle.kts +++ b/bootstrap/spigot/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("geyser.platform-conventions") + id("geyser.modrinth-uploading-conventions") +} + dependencies { api(projects.core) api(libs.erosion.bukkit.common) { diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts index fd81dad63..b210693c1 100644 --- a/bootstrap/standalone/build.gradle.kts +++ b/bootstrap/standalone/build.gradle.kts @@ -2,6 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCach plugins { application + id("geyser.platform-conventions") } val terminalConsoleVersion = "1.2.0" diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts index 93e0c9c93..05035e271 100644 --- a/bootstrap/velocity/build.gradle.kts +++ b/bootstrap/velocity/build.gradle.kts @@ -1,3 +1,8 @@ +plugins { + id("geyser.platform-conventions") + id("geyser.modrinth-uploading-conventions") +} + dependencies { annotationProcessor(libs.velocity.api) api(projects.core) diff --git a/bootstrap/viaproxy/build.gradle.kts b/bootstrap/viaproxy/build.gradle.kts index 254787743..c13862a27 100644 --- a/bootstrap/viaproxy/build.gradle.kts +++ b/bootstrap/viaproxy/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("geyser.platform-conventions") +} + dependencies { api(projects.core) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 190386667..b87490880 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -12,9 +12,14 @@ repositories { } dependencies { + // This is for the LibsAccessor.kt hack // this is OK as long as the same version catalog is used in the main build and build-logic // see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + + // This is for applying plugins, and using the version from the libs.versions.toml + // Unfortunately they still need to be applied by their string name in the convention scripts. + implementation(libs.lombok) implementation(libs.indra) implementation(libs.shadow) implementation(libs.architectury.plugin) diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 63bde189b..bd4560d11 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -8,4 +8,4 @@ dependencyResolutionManagement { } } -rootProject.name = "build-logic" \ No newline at end of file +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 950c0184b..093f0a8c0 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -3,9 +3,10 @@ plugins { id("net.kyori.indra") } -dependencies { - compileOnly("org.checkerframework", "checker-qual", "3.19.0") -} +val rootProperties: Map = project.rootProject.properties +group = rootProperties["group"] as String + "." + rootProperties["id"] as String +version = rootProperties["version"] as String +description = rootProperties["description"] as String indra { github("GeyserMC", "Geyser") { @@ -20,18 +21,52 @@ indra { } } -tasks { - processResources { - // Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge - filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json", "viaproxy.yml", "META-INF/neoforge.mods.toml")) { - expand( - "id" to "geyser", - "name" to "Geyser", - "version" to project.version, - "description" to project.description, - "url" to "https://geysermc.org", - "author" to "GeyserMC" - ) - } +dependencies { + compileOnly("org.checkerframework", "checker-qual", libs.checker.qual.get().version) +} + +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() } } -} \ No newline at end of file + + // 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.build-logic.gradle.kts b/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts deleted file mode 100644 index b6168507e..000000000 --- a/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts +++ /dev/null @@ -1,45 +0,0 @@ -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 8584c13d4..779d6446a 100644 --- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts @@ -2,11 +2,9 @@ import net.fabricmc.loom.task.RemapJarTask import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.maven plugins { - id("geyser.build-logic") - id("geyser.publish-conventions") + id("geyser.platform-conventions") id("architectury-plugin") id("dev.architectury.loom") } 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 410e67404..7a342783b 100644 --- a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts @@ -1,3 +1,20 @@ plugins { id("geyser.publish-conventions") -} \ No newline at end of file + id("io.freefair.lombok") +} + +tasks { + processResources { + // Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge + filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json", "viaproxy.yml", "META-INF/neoforge.mods.toml")) { + expand( + "id" to "geyser", + "name" to "Geyser", + "version" to project.version, + "description" to project.description, + "url" to "https://geysermc.org", + "author" to "GeyserMC" + ) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index dfbf9837f..7f700a2f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,55 +1,5 @@ plugins { - `java-library` // Ensure AP works in eclipse (no effect on other IDEs) eclipse - id("geyser.build-logic") - alias(libs.plugins.lombok) apply false + id("geyser.base-conventions") } - -allprojects { - group = properties["group"] as String + "." + properties["id"] as String - version = properties["version"] as String - description = properties["description"] as String -} - -val basePlatforms = setOf( - projects.bungeecord, - projects.spigot, - projects.standalone, - projects.velocity, - projects.viaproxy -).map { it.dependencyProject } - -val moddedPlatforms = setOf( - projects.fabric, - projects.neoforge, - projects.mod -).map { it.dependencyProject } - -val modrinthPlatforms = setOf( - projects.bungeecord, - projects.fabric, - projects.neoforge, - projects.spigot, - projects.velocity -).map { it.dependencyProject } - -subprojects { - apply { - plugin("java-library") - plugin("io.freefair.lombok") - plugin("geyser.build-logic") - } - - when (this) { - in basePlatforms -> plugins.apply("geyser.platform-conventions") - in moddedPlatforms -> plugins.apply("geyser.modded-conventions") - else -> plugins.apply("geyser.base-conventions") - } - - // Not combined with platform-conventions as that also contains - // platforms which we cant publish to modrinth - if (modrinthPlatforms.contains(this)) { - plugins.apply("geyser.modrinth-uploading-conventions") - } -} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index efba08c8d..166ffe9f5 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("geyser.publish-conventions") + id("io.freefair.lombok") } dependencies { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8d022271b..d30e60298 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,6 +3,7 @@ plugins { idea alias(libs.plugins.blossom) id("geyser.publish-conventions") + id("io.freefair.lombok") } dependencies { diff --git a/gradle.properties b/gradle.properties index 814529d6c..dd1bc915a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ # Gradle settings org.gradle.jvmargs=-Xmx4G org.gradle.daemon=false +org.gradle.configureondemand=true org.gradle.parallel=true org.gradle.caching=true org.gradle.vfs.watch=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c564720a3..f2a29ee74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -134,6 +134,7 @@ protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-con math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" } # plugins +lombok = { group = "io.freefair.gradle", name = "lombok-plugin", version.ref = "lombok" } indra = { group = "net.kyori", name = "indra-common", version.ref = "indra" } shadow = { group = "com.github.johnrengelman", name = "shadow", version.ref = "shadow" } architectury-plugin = { group = "architectury-plugin", name = "architectury-plugin.gradle.plugin", version.ref = "architectury-plugin" } @@ -141,7 +142,6 @@ architectury-loom = { group = "dev.architectury.loom", name = "dev.architectury. minotaur = { group = "com.modrinth.minotaur", name = "Minotaur", version.ref = "minotaur" } [plugins] -lombok = { id = "io.freefair.lombok", version.ref = "lombok" } indra = { id = "net.kyori.indra", version.ref = "indra" } blossom = { id = "net.kyori.blossom", version.ref = "blossom" } From 9dad34d0a82a7cb47b61016c15bb5e51dbcc2860 Mon Sep 17 00:00:00 2001 From: RK_01 <50594595+RaphiMC@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:25:49 +0200 Subject: [PATCH 51/65] Unhardcode PARTICLES_DRAGON_BLOCK_BREAK id (#5018) --- .../protocol/java/level/JavaLevelEventTranslator.java | 5 +---- 1 file changed, 1 insertion(+), 4 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 f54de1b68..5b4ff1de7 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 @@ -291,10 +291,7 @@ public class JavaLevelEventTranslator extends PacketTranslator { - effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_GENERIC_SPAWN); - effectPacket.setData(65); - } + case PARTICLES_DRAGON_BLOCK_BREAK -> effectPacket.setType(ParticleType.DRAGON_DESTROY_BLOCK); case PARTICLES_WATER_EVAPORATING -> { effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_EVAPORATE_WATER); effectPacket.setPosition(pos.add(-0.5f, 0.5f, -0.5f)); From 34f5d71e58ff0b07fafdefc1aed5169f3eb8e00f Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Fri, 6 Sep 2024 13:48:00 -0400 Subject: [PATCH 52/65] Validate sleep message arguments (#5023) --- .../java/JavaSystemChatTranslator.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java index 7e7a97a4b..27481aa26 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java @@ -25,7 +25,10 @@ package org.geysermc.geyser.translator.protocol.java; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.TranslationArgument; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; @@ -55,16 +58,18 @@ public class JavaSystemChatTranslator extends PacketTranslator Date: Sat, 7 Sep 2024 22:29:27 -0400 Subject: [PATCH 53/65] Cancel Erosion futures when disconnecting/switching servers (#5026) --- .../erosion/ErosionCancellationException.java | 34 ++++++++++++ .../GeyserboundHandshakePacketHandler.java | 2 +- .../erosion/GeyserboundPacketHandlerImpl.java | 13 ++++- .../geyser/level/GeyserWorldManager.java | 9 ++++ .../registry/PacketTranslatorRegistry.java | 3 ++ .../geyser/session/GeyserSession.java | 11 ++-- .../protocol/java/JavaLoginTranslator.java | 6 --- .../JavaStartConfigurationTranslator.java | 52 +++++++++++++++++++ 8 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java create mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaStartConfigurationTranslator.java diff --git a/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java b/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java new file mode 100644 index 000000000..ae283895b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java @@ -0,0 +1,34 @@ +/* + * 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.erosion; + +import java.io.Serial; +import java.util.concurrent.CancellationException; + +public class ErosionCancellationException extends CancellationException { + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java index 0b4f03643..88a7e0cd3 100644 --- a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java @@ -42,7 +42,6 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound public void handleHandshake(GeyserboundHandshakePacket packet) { boolean useTcp = packet.getTransportType().getSocketAddress() == null; GeyserboundPacketHandlerImpl handler = new GeyserboundPacketHandlerImpl(session, useTcp ? new GeyserErosionPacketSender(session) : new NettyPacketSender<>()); - session.setErosionHandler(handler); if (!useTcp) { if (session.getGeyser().getErosionUnixListener() == null) { session.disconnect("Erosion configurations using Unix socket handling are not supported on this hardware!"); @@ -52,6 +51,7 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound } else { handler.onConnect(); } + session.setErosionHandler(handler); session.ensureInEventLoop(() -> session.getChunkCache().clear()); } diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java index c8cbe384b..7202db449 100644 --- a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java +++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java @@ -171,10 +171,10 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke @Override public void handleHandshake(GeyserboundHandshakePacket packet) { - this.close(); var handler = new GeyserboundHandshakePacketHandler(this.session); session.setErosionHandler(handler); handler.handleHandshake(packet); + this.close(); } @Override @@ -198,6 +198,17 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke public void close() { this.packetSender.close(); + + if (pendingLookup != null) { + pendingLookup.completeExceptionally(new ErosionCancellationException()); + } + if (pendingBatchLookup != null) { + pendingBatchLookup.completeExceptionally(new ErosionCancellationException()); + } + if (pickBlockLookup != null) { + pickBlockLookup.completeExceptionally(new ErosionCancellationException()); + } + asyncPendingLookups.forEach(($, future) -> future.completeExceptionally(new ErosionCancellationException())); } public int getNextTransactionId() { 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 9cf2c0179..befcfa4b7 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java +++ b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java @@ -35,6 +35,7 @@ import org.geysermc.erosion.packet.backendbound.BackendboundBatchBlockRequestPac import org.geysermc.erosion.packet.backendbound.BackendboundBlockRequestPacket; import org.geysermc.erosion.packet.backendbound.BackendboundPickBlockPacket; import org.geysermc.erosion.util.BlockPositionIterator; +import org.geysermc.geyser.erosion.ErosionCancellationException; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; @@ -49,6 +50,8 @@ public class GeyserWorldManager extends WorldManager { var erosionHandler = session.getErosionHandler().getAsActive(); if (erosionHandler == null) { return session.getChunkCache().getBlockAt(x, y, z); + } else if (session.isClosed()) { + throw new ErosionCancellationException(); } CompletableFuture future = new CompletableFuture<>(); // Boxes erosionHandler.setPendingLookup(future); @@ -61,6 +64,8 @@ public class GeyserWorldManager extends WorldManager { var erosionHandler = session.getErosionHandler().getAsActive(); if (erosionHandler == null) { return super.getBlockAtAsync(session, x, y, z); + } else if (session.isClosed()) { + return CompletableFuture.failedFuture(new ErosionCancellationException()); } CompletableFuture future = new CompletableFuture<>(); // Boxes int transactionId = erosionHandler.getNextTransactionId(); @@ -74,6 +79,8 @@ public class GeyserWorldManager extends WorldManager { var erosionHandler = session.getErosionHandler().getAsActive(); if (erosionHandler == null) { return super.getBlocksAt(session, iter); + } else if (session.isClosed()) { + throw new ErosionCancellationException(); } CompletableFuture future = new CompletableFuture<>(); erosionHandler.setPendingBatchLookup(future); @@ -124,6 +131,8 @@ public class GeyserWorldManager extends WorldManager { var erosionHandler = session.getErosionHandler().getAsActive(); if (erosionHandler == null) { return super.getPickItemComponents(session, x, y, z, addNbtData); + } else if (session.isClosed()) { + return CompletableFuture.failedFuture(new ErosionCancellationException()); } CompletableFuture> future = new CompletableFuture<>(); erosionHandler.setPickBlockLookup(future); diff --git a/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java b/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java index 9a5b43816..b31f2b4f0 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java @@ -31,6 +31,7 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.Clien import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLightUpdatePacket; import io.netty.channel.EventLoop; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.erosion.ErosionCancellationException; import org.geysermc.geyser.registry.loader.RegistryLoaders; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; @@ -87,6 +88,8 @@ public class PacketTranslatorRegistry extends AbstractMappedRegistry { try { runnable.run(); + } catch (ErosionCancellationException e) { + geyser.getLogger().debug("Caught ErosionCancellationException"); } catch (Throwable e) { geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e); } @@ -1230,6 +1233,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { if (!closed) { runnable.run(); } + } catch (ErosionCancellationException e) { + geyser.getLogger().debug("Caught ErosionCancellationException"); } catch (Throwable e) { geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e); } 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 a6d6e6c70..6470a5f0a 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 @@ -33,7 +33,6 @@ 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; @@ -57,11 +56,6 @@ public class JavaLoginTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundStartConfigurationPacket packet) { + var erosionHandler = session.getErosionHandler(); + if (erosionHandler.isActive()) { + // Set new handler before closing + session.setErosionHandler(new GeyserboundHandshakePacketHandler(session)); + erosionHandler.close(); + } + } + + @Override + public boolean shouldExecuteInEventLoop() { + // Execute outside of event loop to cancel any pending erosion futures + return false; + } +} From c28522af6e91268c223941e842989c1dfc16cd64 Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Sun, 8 Sep 2024 23:06:36 -0400 Subject: [PATCH 54/65] Delay clear weather packets (#5030) --- .../geyser/session/GeyserSession.java | 58 ++++++++++++++++++- .../protocol/java/JavaRespawnTranslator.java | 17 +----- .../java/level/JavaGameEventTranslator.java | 37 ++---------- .../geysermc/geyser/util/DimensionUtils.java | 16 +---- 4 files changed, 65 insertions(+), 63 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 a8b8480fb..f7ffcdd0e 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -66,6 +66,7 @@ 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.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType; @@ -85,6 +86,7 @@ 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.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket; @@ -570,13 +572,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { /** * Caches current rain status. */ - @Setter private boolean raining = false; /** * Caches current thunder status. */ - @Setter private boolean thunder = false; /** @@ -2007,6 +2007,60 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { }; } + /** + * Sends a packet to update rain strength. + * Stops rain if strength is 0. + * + * @param strength value between 0 and 1 + */ + public void updateRain(float strength) { + this.raining = strength > 0; + + LevelEventPacket rainPacket = new LevelEventPacket(); + rainPacket.setType(this.raining ? LevelEvent.START_RAINING : LevelEvent.STOP_RAINING); + rainPacket.setData((int) (strength * 65535)); + rainPacket.setPosition(Vector3f.ZERO); + + if (this.raining) { + sendUpstreamPacket(rainPacket); + } else { + // The bedrock client might ignore this packet if it is sent in the same tick as another rain packet + // https://github.com/GeyserMC/Geyser/issues/3679 + scheduleInEventLoop(() -> { + if (!this.raining) { + sendUpstreamPacket(rainPacket); + } + }, 100, TimeUnit.MILLISECONDS); + } + } + + /** + * Sends a packet to update thunderstorm strength. + * Stops thunderstorm if strength is 0. + * + * @param strength value between 0 and 1 + */ + public void updateThunder(float strength) { + this.thunder = strength > 0; + + LevelEventPacket thunderPacket = new LevelEventPacket(); + thunderPacket.setType(this.thunder ? LevelEvent.START_THUNDERSTORM : LevelEvent.STOP_THUNDERSTORM); + thunderPacket.setData((int) (strength * 65535)); + thunderPacket.setPosition(Vector3f.ZERO); + + if (this.thunder) { + sendUpstreamPacket(thunderPacket); + } else { + // The bedrock client might ignore this packet if it is sent in the same tick as another thunderstorm packet + // https://github.com/GeyserMC/Geyser/issues/3679 + scheduleInEventLoop(() -> { + if (!this.thunder) { + sendUpstreamPacket(thunderPacket); + } + }, 100, TimeUnit.MILLISECONDS); + } + } + @Override public @NonNull String bedrockUsername() { return authData.name(); 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 ccd93ac97..5c477f23e 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 @@ -25,9 +25,6 @@ package org.geysermc.geyser.translator.protocol.java; -import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.LevelEvent; -import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; @@ -76,21 +73,11 @@ public class JavaRespawnTranslator extends PacketTranslator { - // Strength of rainstorms and thunderstorms is a 0-1 float on Java, while on Bedrock it is a 0-65535 int - private static final int MAX_STORM_STRENGTH = 65535; - @Override public void translate(GeyserSession session, ClientboundGameEventPacket packet) { PlayerEntity entity = session.getPlayerEntity(); @@ -65,42 +60,20 @@ public class JavaGameEventTranslator extends PacketTranslator 0f; - LevelEventPacket changeRainPacket = new LevelEventPacket(); - changeRainPacket.setType(isCurrentlyRaining ? LevelEvent.START_RAINING : LevelEvent.STOP_RAINING); // This is the rain strength on LevelEventType.START_RAINING, but can be any value on LevelEventType.STOP_RAINING - changeRainPacket.setData((int) (rainStrength * MAX_STORM_STRENGTH)); - changeRainPacket.setPosition(Vector3f.ZERO); - session.sendUpstreamPacket(changeRainPacket); - session.setRaining(isCurrentlyRaining); + float rainStrength = ((RainStrengthValue) packet.getValue()).getStrength(); + session.updateRain(rainStrength); break; case THUNDER_STRENGTH: // See above, same process float thunderStrength = ((ThunderStrengthValue) packet.getValue()).getStrength(); - boolean isCurrentlyThundering = thunderStrength > 0f; - LevelEventPacket changeThunderPacket = new LevelEventPacket(); - changeThunderPacket.setType(isCurrentlyThundering ? LevelEvent.START_THUNDERSTORM : LevelEvent.STOP_THUNDERSTORM); - changeThunderPacket.setData((int) (thunderStrength * MAX_STORM_STRENGTH)); - changeThunderPacket.setPosition(Vector3f.ZERO); - session.sendUpstreamPacket(changeThunderPacket); - session.setThunder(isCurrentlyThundering); + session.updateThunder(thunderStrength); break; case CHANGE_GAMEMODE: GameMode gameMode = (GameMode) packet.getValue(); 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 f043631b6..8dc94a165 100644 --- a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java @@ -28,11 +28,9 @@ package org.geysermc.geyser.util; import org.checkerframework.checker.nullness.qual.Nullable; 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; @@ -89,18 +87,8 @@ public class DimensionUtils { 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); + session.updateRain(0); + session.updateThunder(0); finalizeDimensionSwitch(session, player); From 723840c7fc13dc468f5bd3ae84b89ef80f9225f1 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:42:57 -0400 Subject: [PATCH 55/65] More clear message when logging in without a paid Java account --- .../org/geysermc/geyser/session/GeyserSession.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 f7ffcdd0e..9a50d7acb 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -40,6 +40,7 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import net.kyori.adventure.key.Key; +import net.raphimc.minecraftauth.responsehandler.exception.MinecraftRequestException; import net.raphimc.minecraftauth.step.java.StepMCProfile; import net.raphimc.minecraftauth.step.java.StepMCToken; import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession; @@ -231,6 +232,7 @@ import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -877,7 +879,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { return task.getAuthentication().handle((result, ex) -> { if (ex != null) { geyser.getLogger().error("Failed to log in with Microsoft code!", ex); - disconnect(ex.toString()); + if (ex instanceof CompletionException ce + && ce.getCause() instanceof MinecraftRequestException mre + && mre.getResponse().getStatusCode() == 404) { + // Player is trying to join with a Microsoft account that doesn't have Java Edition purchased + disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", locale())); + } else { + disconnect(ex.toString()); + } return false; } From 14cf104cff850dc06438358b5415af32d43d8bd2 Mon Sep 17 00:00:00 2001 From: AJ Ferguson Date: Mon, 9 Sep 2024 18:23:19 -0400 Subject: [PATCH 56/65] Do not send thunder strength if not raining (#5031) * Do not send thunder strength if not raining * Address review * Minor optimization --- .../geyser/session/GeyserSession.java | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 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 9a50d7acb..4589afe23 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -572,14 +572,16 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private float walkSpeed; /** - * Caches current rain status. + * Caches current rain strength. + * Value between 0 and 1. */ - private boolean raining = false; + private float rainStrength = 0.0f; /** - * Caches current thunder status. + * Caches current thunder strength. + * Value between 0 and 1. */ - private boolean thunder = false; + private float thunderStrength = 0.0f; /** * Stores a map of all statistics sent from the server. @@ -2023,23 +2025,30 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * @param strength value between 0 and 1 */ public void updateRain(float strength) { - this.raining = strength > 0; + boolean wasRaining = isRaining(); + this.rainStrength = strength; LevelEventPacket rainPacket = new LevelEventPacket(); - rainPacket.setType(this.raining ? LevelEvent.START_RAINING : LevelEvent.STOP_RAINING); + rainPacket.setType(isRaining() ? LevelEvent.START_RAINING : LevelEvent.STOP_RAINING); rainPacket.setData((int) (strength * 65535)); rainPacket.setPosition(Vector3f.ZERO); + sendUpstreamPacket(rainPacket); - if (this.raining) { - sendUpstreamPacket(rainPacket); - } else { - // The bedrock client might ignore this packet if it is sent in the same tick as another rain packet - // https://github.com/GeyserMC/Geyser/issues/3679 - scheduleInEventLoop(() -> { - if (!this.raining) { - sendUpstreamPacket(rainPacket); - } - }, 100, TimeUnit.MILLISECONDS); + // Keep thunder in sync with rain when starting/stopping a storm + if ((wasRaining != isRaining()) && isThunder()) { + if (isRaining()) { + LevelEventPacket thunderPacket = new LevelEventPacket(); + thunderPacket.setType(LevelEvent.START_THUNDERSTORM); + thunderPacket.setData((int) (this.thunderStrength * 65535)); + thunderPacket.setPosition(Vector3f.ZERO); + sendUpstreamPacket(thunderPacket); + } else { + LevelEventPacket thunderPacket = new LevelEventPacket(); + thunderPacket.setType(LevelEvent.STOP_THUNDERSTORM); + thunderPacket.setData(0); + thunderPacket.setPosition(Vector3f.ZERO); + sendUpstreamPacket(thunderPacket); + } } } @@ -2050,24 +2059,28 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * @param strength value between 0 and 1 */ public void updateThunder(float strength) { - this.thunder = strength > 0; + this.thunderStrength = strength; + + // Do not send thunder packet if not raining + // The bedrock client will start raining automatically when updating thunder strength + // https://github.com/GeyserMC/Geyser/issues/3679 + if (!isRaining()) { + return; + } LevelEventPacket thunderPacket = new LevelEventPacket(); - thunderPacket.setType(this.thunder ? LevelEvent.START_THUNDERSTORM : LevelEvent.STOP_THUNDERSTORM); + thunderPacket.setType(isThunder() ? LevelEvent.START_THUNDERSTORM : LevelEvent.STOP_THUNDERSTORM); thunderPacket.setData((int) (strength * 65535)); thunderPacket.setPosition(Vector3f.ZERO); + sendUpstreamPacket(thunderPacket); + } - if (this.thunder) { - sendUpstreamPacket(thunderPacket); - } else { - // The bedrock client might ignore this packet if it is sent in the same tick as another thunderstorm packet - // https://github.com/GeyserMC/Geyser/issues/3679 - scheduleInEventLoop(() -> { - if (!this.thunder) { - sendUpstreamPacket(thunderPacket); - } - }, 100, TimeUnit.MILLISECONDS); - } + public boolean isRaining() { + return this.rainStrength > 0; + } + + public boolean isThunder() { + return this.thunderStrength > 0; } @Override From 73f7259b6dd508680582418bbb7963ad0b460907 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Tue, 10 Sep 2024 20:10:31 +0000 Subject: [PATCH 57/65] Add proper text component parsing from NBT (#5029) * Attempt creating a simple NBT text component parser * Fix style merging * Rename TextDecoration to ChatDecoration, use better style deserialization in ChatDecoration * Remove unused code * containsKey optimisations, update documentation, improve getStyleFromNbtMap performance slightly, more slight tweaks * Remove unnecessary deserializeStyle method --- .../geyser/item/enchantment/Enchantment.java | 2 +- .../geysermc/geyser/level/JukeboxSong.java | 2 +- .../geyser/session/cache/RegistryCache.java | 4 +- ...extDecoration.java => ChatDecoration.java} | 34 ++---- .../translator/text/MessageTranslator.java | 103 ++++++++++++++++-- 5 files changed, 105 insertions(+), 40 deletions(-) rename core/src/main/java/org/geysermc/geyser/text/{TextDecoration.java => ChatDecoration.java} (74%) 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 3c0caa60c..5cac45534 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 @@ -69,7 +69,7 @@ public record Enchantment(String identifier, // 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; + String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(context.session(), data) : null; return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel, description, anvilCost, exclusiveSet, bedrockEnchantment); 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 b00dc9f98..86d66e209 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java +++ b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java @@ -44,7 +44,7 @@ public record JukeboxSong(String soundEvent, String description) { soundEvent = ""; 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); + String description = MessageTranslator.deserializeDescription(context.session(), 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 a393d461d..4a4167f15 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 @@ -49,7 +49,7 @@ 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.text.ChatDecoration; import org.geysermc.geyser.translator.level.BiomeTranslator; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; @@ -78,7 +78,7 @@ public final class RegistryCache { private static final Map>> REGISTRIES = new HashMap<>(); static { - register("chat_type", cache -> cache.chatTypes, TextDecoration::readChatType); + register("chat_type", cache -> cache.chatTypes, ChatDecoration::readChatType); register("dimension_type", cache -> cache.dimensions, JavaDimension::read); register("enchantment", cache -> cache.enchantments, Enchantment::read); register("jukebox_song", cache -> cache.jukeboxSongs, JukeboxSong::read); diff --git a/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java b/core/src/main/java/org/geysermc/geyser/text/ChatDecoration.java similarity index 74% rename from core/src/main/java/org/geysermc/geyser/text/TextDecoration.java rename to core/src/main/java/org/geysermc/geyser/text/ChatDecoration.java index 94aec22ef..fc7597cfd 100644 --- a/core/src/main/java/org/geysermc/geyser/text/TextDecoration.java +++ b/core/src/main/java/org/geysermc/geyser/text/ChatDecoration.java @@ -25,17 +25,19 @@ package org.geysermc.geyser.text; -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.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; -public record TextDecoration(String translationKey, List parameters, Style deserializedStyle) implements ChatTypeDecoration { +public record ChatDecoration(String translationKey, List parameters, Style deserializedStyle) implements ChatTypeDecoration { @Override public NbtMap style() { @@ -53,38 +55,22 @@ public record TextDecoration(String translationKey, List parameters, String translationKey = chat.getString("translation_key"); NbtMap styleTag = chat.getCompound("style"); - Style style = deserializeStyle(styleTag); + Style style = MessageTranslator.getStyleFromNbtMap(styleTag); List parameters = new ArrayList<>(); List parametersNbt = chat.getList("parameters", NbtType.STRING); for (String parameter : parametersNbt) { parameters.add(ChatTypeDecoration.Parameter.valueOf(parameter.toUpperCase(Locale.ROOT))); } - return new ChatType(new TextDecoration(translationKey, parameters, style), null); + return new ChatType(new ChatDecoration(translationKey, parameters, style), null); } return new ChatType(null, null); } public static Style getStyle(ChatTypeDecoration decoration) { - if (decoration instanceof TextDecoration textDecoration) { - return textDecoration.deserializedStyle(); + if (decoration instanceof ChatDecoration chatDecoration) { + return chatDecoration.deserializedStyle(); } - return deserializeStyle(decoration.style()); - } - - private static Style deserializeStyle(NbtMap styleTag) { - Style.Builder builder = Style.style(); - if (!styleTag.isEmpty()) { - String color = styleTag.getString("color", null); - if (color != null) { - builder.color(NamedTextColor.NAMES.value(color)); - } - //TODO implement the rest - boolean italic = styleTag.getBoolean("italic"); - if (italic) { - builder.decorate(net.kyori.adventure.text.format.TextDecoration.ITALIC); - } - } - return builder.build(); + return MessageTranslator.getStyleFromNbtMap(decoration.style()); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java index 152bf4160..0547a21c9 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java @@ -26,16 +26,21 @@ package org.geysermc.geyser.translator.text; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.ScoreComponent; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.renderer.TranslatableComponentRenderer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.protocol.bedrock.packet.TextPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.session.GeyserSession; @@ -341,16 +346,16 @@ public class MessageTranslator { // Though, Bedrock cannot care about the signed stuff. TranslatableComponent.Builder withDecoration = Component.translatable() .key(chat.translationKey()) - .style(TextDecoration.getStyle(chat)); + .style(ChatDecoration.getStyle(chat)); List parameters = chat.parameters(); List args = new ArrayList<>(3); - if (parameters.contains(TextDecoration.Parameter.TARGET)) { + if (parameters.contains(ChatDecoration.Parameter.TARGET)) { args.add(targetName); } - if (parameters.contains(TextDecoration.Parameter.SENDER)) { + if (parameters.contains(ChatDecoration.Parameter.SENDER)) { args.add(sender); } - if (parameters.contains(TextDecoration.Parameter.CONTENT)) { + if (parameters.contains(ChatDecoration.Parameter.CONTENT)) { args.add(message); } withDecoration.arguments(args); @@ -426,17 +431,91 @@ public class MessageTranslator { } /** - * Deserialize an NbtMap provided from a registry into a string. + * Deserialize an NbtMap with a description text component (usually provided from a registry) into a Bedrock-formatted string. */ - // This may be a Component in the future. - public static String deserializeDescription(NbtMap tag) { + public static String deserializeDescription(GeyserSession session, NbtMap tag) { NbtMap description = tag.getCompound("description"); - String translate = description.getString("translate", null); - if (translate == null) { - GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag); - return ""; + Component parsed = componentFromNbtTag(description); + return convertMessage(session, parsed); + } + + public static Component componentFromNbtTag(Object nbtTag) { + return componentFromNbtTag(nbtTag, Style.empty()); + } + + private static Component componentFromNbtTag(Object nbtTag, Style style) { + if (nbtTag instanceof String literal) { + return Component.text(literal).style(style); + } else if (nbtTag instanceof List list) { + return Component.join(JoinConfiguration.noSeparators(), componentsFromNbtList(list, style)); + } else if (nbtTag instanceof NbtMap map) { + Component component = null; + String text = map.getString("text", null); + if (text != null) { + component = Component.text(text); + } else { + String translateKey = map.getString("translate", null); + if (translateKey != null) { + String fallback = map.getString("fallback", ""); + List args = new ArrayList<>(); + + Object with = map.get("with"); + if (with instanceof List list) { + args = componentsFromNbtList(list, style); + } else if (with != null) { + args.add(componentFromNbtTag(with, style)); + } + component = Component.translatable(translateKey, fallback, args); + } + } + + if (component != null) { + Style newStyle = getStyleFromNbtMap(map, style); + component = component.style(newStyle); + + Object extra = map.get("extra"); + if (extra != null) { + component = component.append(componentFromNbtTag(extra, newStyle)); + } + + return component; + } } - return translate; + + throw new IllegalArgumentException("Expected tag to be a literal string, a list of components, or a component object with a text/translate key"); + } + + private static List componentsFromNbtList(List list, Style style) { + List components = new ArrayList<>(); + for (Object entry : list) { + components.add(componentFromNbtTag(entry, style)); + } + return components; + } + + public static Style getStyleFromNbtMap(NbtMap map) { + Style.Builder style = Style.style(); + + String colorString = map.getString("color", null); + if (colorString != null) { + if (colorString.startsWith(TextColor.HEX_PREFIX)) { + style.color(TextColor.fromHexString(colorString)); + } else { + style.color(NamedTextColor.NAMES.value(colorString)); + } + } + + map.listenForBoolean("bold", value -> style.decoration(TextDecoration.BOLD, value)); + map.listenForBoolean("italic", value -> style.decoration(TextDecoration.ITALIC, value)); + map.listenForBoolean("underlined", value -> style.decoration(TextDecoration.UNDERLINED, value)); + map.listenForBoolean("strikethrough", value -> style.decoration(TextDecoration.STRIKETHROUGH, value)); + map.listenForBoolean("obfuscated", value -> style.decoration(TextDecoration.OBFUSCATED, value)); + + return style.build(); + } + + public static Style getStyleFromNbtMap(NbtMap map, Style base) { + return base.merge(getStyleFromNbtMap(map)); } public static void init() { From ed5195a84211a9aa0e0c0a63b42cb3910b0cfbc4 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:48:54 -0400 Subject: [PATCH 58/65] Fix some instances of enchantment names not deserializing --- .../geyser/item/enchantment/Enchantment.java | 6 +++--- .../translator/text/MessageTranslator.java | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) 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 5cac45534..5a5a37e4e 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,8 +25,6 @@ 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; @@ -35,11 +33,13 @@ 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.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; +import java.util.function.Function; /** * @param description only populated if {@link #bedrockEnchantment()} is not null. diff --git a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java index 0547a21c9..1932d3e47 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java @@ -40,18 +40,25 @@ import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.protocol.bedrock.packet.TextPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.*; +import org.geysermc.geyser.text.ChatColor; +import org.geysermc.geyser.text.ChatDecoration; +import org.geysermc.geyser.text.DummyLegacyHoverEventSerializer; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.geyser.text.GsonComponentSerializerWrapper; +import org.geysermc.geyser.text.MinecraftTranslationRegistry; import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer; import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; public class MessageTranslator { // These are used for handling the translations of the messages @@ -434,7 +441,7 @@ public class MessageTranslator { * Deserialize an NbtMap with a description text component (usually provided from a registry) into a Bedrock-formatted string. */ public static String deserializeDescription(GeyserSession session, NbtMap tag) { - NbtMap description = tag.getCompound("description"); + Object description = tag.get("description"); Component parsed = componentFromNbtTag(description); return convertMessage(session, parsed); } @@ -482,7 +489,8 @@ public class MessageTranslator { } } - throw new IllegalArgumentException("Expected tag to be a literal string, a list of components, or a component object with a text/translate key"); + GeyserImpl.getInstance().getLogger().error("Expected tag to be a literal string, a list of components, or a component object with a text/translate key: " + nbtTag); + return Component.empty(); } private static List componentsFromNbtList(List list, Style style) { From 34fda8a743f7a6617aec63d299eb32555ae6f664 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:51:39 -0400 Subject: [PATCH 59/65] Small optimizations in Enchantment --- .../geyser/item/enchantment/Enchantment.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 5a5a37e4e..301f69a5f 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,6 +25,7 @@ package org.geysermc.geyser.item.enchantment; +import it.unimi.dsi.fastutil.ints.IntArrays; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; @@ -33,13 +34,14 @@ 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.item.component.HolderSet; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; +import java.util.function.ToIntFunction; /** * @param description only populated if {@link #bedrockEnchantment()} is not null. @@ -86,21 +88,21 @@ public record Enchantment(String identifier, } // TODO holder set util? - private static HolderSet readHolderSet(@Nullable Object holderSet, Function keyIdMapping) { + private static HolderSet readHolderSet(@Nullable Object holderSet, ToIntFunction keyIdMapping) { if (holderSet == null) { - return new HolderSet(new int[]{}); + return new HolderSet(IntArrays.EMPTY_ARRAY); } if (holderSet instanceof String stringTag) { // Tag if (stringTag.startsWith("#")) { - return new HolderSet(Key.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag + return new HolderSet(MinecraftKey.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag } else { - return new HolderSet(new int[]{keyIdMapping.apply(Key.key(stringTag))}); + return new HolderSet(new int[]{keyIdMapping.applyAsInt(MinecraftKey.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()); + return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).mapToInt(keyIdMapping).toArray()); } throw new IllegalArgumentException("Holder set must either be a tag, a string ID or a list of string IDs"); } From a5e45ad7eddb190ced403af42dccc109b33c8dfb Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Sat, 14 Sep 2024 18:30:25 -0400 Subject: [PATCH 60/65] Workaround for goat horns on the client side --- .../geyser/session/cache/WorldCache.java | 31 +++++++++++++++++++ ...BedrockInventoryTransactionTranslator.java | 21 +++++++++++++ .../java/level/JavaCooldownTranslator.java | 2 ++ 3 files changed, 54 insertions(+) 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 fb5137b05..86cb69314 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 @@ -34,6 +34,7 @@ import lombok.Setter; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession; import org.geysermc.geyser.session.GeyserSession; @@ -70,6 +71,8 @@ public final class WorldCache { @Setter private boolean editingSignOnFront; + private final Object2IntMap activeCooldowns = new Object2IntOpenHashMap<>(2); + public WorldCache(GeyserSession session) { this.session = session; this.scoreboard = new Scoreboard(session); @@ -201,4 +204,32 @@ public final class WorldCache { public String removeActiveRecord(Vector3i pos) { return this.activeRecords.remove(pos); } + + public void setCooldown(Item item, int ticks) { + if (ticks == 0) { + // As of Java 1.21 + this.activeCooldowns.removeInt(item); + return; + } + this.activeCooldowns.put(item, session.getTicks() + ticks); + } + + public boolean hasCooldown(Item item) { + return this.activeCooldowns.containsKey(item); + } + + public void tick() { + // Implementation note: technically we could empty the field during hasCooldown checks, + // but we don't want the cooldown field to balloon in size from overuse. + if (!this.activeCooldowns.isEmpty()) { + int ticks = session.getTicks(); + Iterator> it = Object2IntMaps.fastIterator(this.activeCooldowns); + while (it.hasNext()) { + Object2IntMap.Entry entry = it.next(); + if (entry.getIntValue() <= ticks) { + it.remove(); + } + } + } + } } 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 1e4c82da1..6ae21067f 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 @@ -31,6 +31,7 @@ import org.cloudburstmc.math.vector.Vector3d; 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.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; @@ -42,6 +43,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetIte import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.Entity; @@ -75,12 +77,15 @@ 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.Holder; 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; 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.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Instrument; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket; @@ -373,6 +378,22 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator instrument = session.getPlayerInventory() + .getItemInHand() + .getComponent(DataComponentType.INSTRUMENT); + if (instrument != null && instrument.isId()) { + // BDS uses a LevelSoundEvent2Packet, but that doesn't work here... (as of 1.21.20) + LevelSoundEventPacket soundPacket = new LevelSoundEventPacket(); + soundPacket.setSound(SoundEvent.valueOf("GOAT_CALL_" + instrument.id())); + soundPacket.setPosition(session.getPlayerEntity().getPosition()); + soundPacket.setIdentifier("minecraft:player"); + soundPacket.setExtraData(-1); + session.sendUpstreamPacket(soundPacket); + } + } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java index 8e07a7d89..636671651 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java @@ -57,5 +57,7 @@ public class JavaCooldownTranslator extends PacketTranslator Date: Wed, 18 Sep 2024 00:48:36 +0900 Subject: [PATCH 61/65] Update to 1.21.30 (#5041) Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- .../entity/attribute/GeyserAttributeType.java | 2 +- .../geyser/impl/camera/CameraDefinitions.java | 14 +- .../updater/AnvilInventoryUpdater.java | 6 + .../updater/ChestInventoryUpdater.java | 4 + .../updater/ContainerInventoryUpdater.java | 4 + .../updater/CrafterInventoryUpdater.java | 5 + .../updater/HorseInventoryUpdater.java | 4 + .../inventory/updater/InventoryUpdater.java | 4 + .../inventory/updater/UIInventoryUpdater.java | 4 + .../geyser/network/CodecProcessor.java | 57 +- .../geysermc/geyser/network/GameProtocol.java | 8 +- .../populator/BlockRegistryPopulator.java | 37 +- .../registry/populator/Conversion729_712.java | 141 + .../populator/ItemRegistryPopulator.java | 4 +- .../inventory/InventoryTranslator.java | 4 +- .../inventory/OldSmithingTableTranslator.java | 2 + .../inventory/PlayerInventoryTranslator.java | 7 + .../ChestedHorseInventoryTranslator.java | 3 + .../entity/JavaEntityEventTranslator.java | 3 + .../JavaContainerSetSlotTranslator.java | 4 + .../geysermc/geyser/util/InventoryUtils.java | 3 + .../bedrock/block_palette.1_21_30.nbt | Bin 0 -> 180490 bytes .../bedrock/creative_items.1_21_30.json | 6214 +++++++++++++++ .../bedrock/runtime_item_states.1_21_30.json | 6898 +++++++++++++++++ gradle.properties | 2 +- gradle/libs.versions.toml | 6 +- 26 files changed, 13406 insertions(+), 34 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java create mode 100644 core/src/main/resources/bedrock/block_palette.1_21_30.nbt create mode 100644 core/src/main/resources/bedrock/creative_items.1_21_30.json create mode 100644 core/src/main/resources/bedrock/runtime_item_states.1_21_30.json 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 3b543a943..1e050c840 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 @@ -54,7 +54,7 @@ public enum GeyserAttributeType { // Bedrock Attributes ABSORPTION(null, "minecraft:absorption", 0f, 1024f, 0f), - EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 5f, 0f), + EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 20f, 0f), EXPERIENCE(null, "minecraft:player.experience", 0f, 1f, 0f), EXPERIENCE_LEVEL(null, "minecraft:player.level", 0f, 24791.00f, 0f), HEALTH(null, "minecraft:health", 0f, 1024f, 20f), 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 7bb25c9ef..1cf6a794e 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, 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))); + new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null), + new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null), + new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null), + new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null), + new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.empty(), null, OptionalBoolean.of(false), null), + new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.empty(), null, OptionalBoolean.of(true), null), + new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.empty(), null, OptionalBoolean.of(true), null)); SimpleDefinitionRegistry.Builder builder = SimpleDefinitionRegistry.builder(); for (int i = 0; i < CAMERA_PRESETS.size(); i++) { 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 7afd31cc9..2e0c75708 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 @@ -32,6 +32,8 @@ import net.kyori.adventure.text.Component; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.geyser.GeyserImpl; @@ -78,6 +80,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(bedrockSlot); slotPacket.setItem(inventory.getItem(i).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } } @@ -98,6 +101,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } else if (lastTargetSlot != javaSlot) { // Update the previous target slot to remove repair cost changes @@ -105,6 +109,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(translator.javaSlotToBedrock(lastTargetSlot)); slotPacket.setItem(inventory.getItem(lastTargetSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } @@ -168,6 +173,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(translator.javaSlotToBedrock(slot)); slotPacket.setItem(itemData); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java index 5d6214871..9f3d00c57 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.inventory.updater; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; @@ -61,6 +63,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(inventory.getBedrockId()); contentPacket.setContents(bedrockItems); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); } @@ -73,6 +76,7 @@ public class ChestInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(inventory.getBedrockId()); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java index c9f313f2a..3d372c083 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.inventory.updater; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; @@ -49,6 +51,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(inventory.getBedrockId()); contentPacket.setContents(Arrays.asList(bedrockItems)); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); } @@ -61,6 +64,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(inventory.getBedrockId()); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java index 4474d420c..315b84c6d 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.inventory.updater; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; @@ -56,6 +58,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(inventory.getBedrockId()); contentPacket.setContents(Arrays.asList(bedrockItems)); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); // inventory and hotbar @@ -67,6 +70,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(ContainerId.INVENTORY); contentPacket.setContents(Arrays.asList(bedrockItems)); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); // Crafter result - it doesn't come after the grid, as explained elsewhere. @@ -88,6 +92,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater { packet.setContainerId(containerId); packet.setSlot(translator.javaSlotToBedrock(javaSlot)); packet.setItem(inventory.getItem(javaSlot).getItemData(session)); + packet.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(packet); return true; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java index 7441e66d0..1a46fc02a 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.inventory.updater; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; @@ -49,6 +51,7 @@ public class HorseInventoryUpdater extends InventoryUpdater { InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(inventory.getBedrockId()); contentPacket.setContents(Arrays.asList(bedrockItems)); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); } @@ -61,6 +64,7 @@ public class HorseInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(4); // Horse GUI? slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java index 68ee334ba..b7ef4720f 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.inventory.updater; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; @@ -45,6 +47,7 @@ public class InventoryUpdater { InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(ContainerId.INVENTORY); contentPacket.setContents(Arrays.asList(bedrockItems)); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); } @@ -54,6 +57,7 @@ public class InventoryUpdater { slotPacket.setContainerId(ContainerId.INVENTORY); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); return true; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java index a23385b53..f4f40d6ce 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.inventory.updater; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.session.GeyserSession; @@ -46,6 +48,7 @@ public class UIInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(bedrockSlot); slotPacket.setItem(inventory.getItem(i).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } } @@ -59,6 +62,7 @@ public class UIInventoryUpdater extends InventoryUpdater { slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot)); slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session)); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); return true; } 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 fd18c01ce..741369c46 100644 --- a/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java +++ b/core/src/main/java/org/geysermc/geyser/network/CodecProcessor.java @@ -33,7 +33,6 @@ import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.MobArmorEquipment import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.MobEquipmentSerializer_v291; import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.PlayerHotbarSerializer_v291; import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.SetEntityLinkSerializer_v291; -import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.SetEntityMotionSerializer_v291; import org.cloudburstmc.protocol.bedrock.codec.v390.serializer.PlayerSkinSerializer_v390; import org.cloudburstmc.protocol.bedrock.codec.v407.serializer.InventoryContentSerializer_v407; import org.cloudburstmc.protocol.bedrock.codec.v407.serializer.InventorySlotSerializer_v407; @@ -43,6 +42,8 @@ import org.cloudburstmc.protocol.bedrock.codec.v662.serializer.SetEntityMotionSe 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.codec.v729.serializer.InventoryContentSerializer_v729; +import org.cloudburstmc.protocol.bedrock.codec.v729.serializer.InventorySlotSerializer_v729; import org.cloudburstmc.protocol.bedrock.packet.AnvilDamagePacket; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.cloudburstmc.protocol.bedrock.packet.BossEventPacket; @@ -139,6 +140,13 @@ class CodecProcessor { } }; + private static final BedrockPacketSerializer INVENTORY_CONTENT_SERIALIZER_V729 = new InventoryContentSerializer_v729() { + @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 InventorySlotPacket since server-auth inventory is used. */ @@ -159,6 +167,13 @@ class CodecProcessor { } }; + private static final BedrockPacketSerializer INVENTORY_SLOT_SERIALIZER_V729 = new InventorySlotSerializer_v729() { + @Override + public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventorySlotPacket packet) { + throw new IllegalArgumentException("Client cannot send InventorySlotPacket in server-auth inventory environment!"); + } + }; + /** * Serializer that does nothing when trying to deserialize BossEventPacket since it is not used from the client. */ @@ -214,16 +229,7 @@ class CodecProcessor { }; /** - * Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client for codec v291. - */ - private static final BedrockPacketSerializer SET_ENTITY_MOTION_SERIALIZER_V291 = new SetEntityMotionSerializer_v291() { - @Override - public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, SetEntityMotionPacket packet) { - } - }; - - /** - * Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client for codec v662. + * Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client. */ private static final BedrockPacketSerializer SET_ENTITY_MOTION_SERIALIZER = new SetEntityMotionSerializer_v662() { @Override @@ -256,8 +262,27 @@ class CodecProcessor { @SuppressWarnings("unchecked") static BedrockCodec processCodec(BedrockCodec codec) { - boolean isPre712 = codec.getProtocolVersion() < 712; - + boolean is729OrAbove = codec.getProtocolVersion() >= 729; + boolean is712OrAbove = codec.getProtocolVersion() >= 712; + + BedrockPacketSerializer inventoryContentSerializer; + if (is729OrAbove) { + inventoryContentSerializer = INVENTORY_CONTENT_SERIALIZER_V729; + } else if (is712OrAbove) { + inventoryContentSerializer = INVENTORY_CONTENT_SERIALIZER_V712; + } else { + inventoryContentSerializer = INVENTORY_CONTENT_SERIALIZER_V407; + } + + BedrockPacketSerializer inventorySlotSerializer; + if (is729OrAbove) { + inventorySlotSerializer = INVENTORY_SLOT_SERIALIZER_V729; + } else if (is712OrAbove) { + inventorySlotSerializer = INVENTORY_SLOT_SERIALIZER_V712; + } else { + inventorySlotSerializer = INVENTORY_SLOT_SERIALIZER_V407; + } + BedrockCodec.Builder codecBuilder = codec.toBuilder() // Illegal unused serverbound EDU packets .updateSerializer(PhotoTransferPacket.class, ILLEGAL_SERIALIZER) @@ -286,11 +311,11 @@ class CodecProcessor { .updateSerializer(AnvilDamagePacket.class, IGNORED_SERIALIZER) .updateSerializer(RefreshEntitlementsPacket.class, IGNORED_SERIALIZER) // Illegal when serverbound due to Geyser specific setup - .updateSerializer(InventoryContentPacket.class, isPre712 ? INVENTORY_CONTENT_SERIALIZER_V407 : INVENTORY_CONTENT_SERIALIZER_V712) - .updateSerializer(InventorySlotPacket.class, isPre712 ? INVENTORY_SLOT_SERIALIZER_V407 : INVENTORY_SLOT_SERIALIZER_V712) + .updateSerializer(InventoryContentPacket.class, inventoryContentSerializer) + .updateSerializer(InventorySlotPacket.class, inventorySlotSerializer) // Ignored only when serverbound .updateSerializer(BossEventPacket.class, BOSS_EVENT_SERIALIZER) - .updateSerializer(MobArmorEquipmentPacket.class, isPre712 ? MOB_ARMOR_EQUIPMENT_SERIALIZER_V291 : MOB_ARMOR_EQUIPMENT_SERIALIZER_V712) + .updateSerializer(MobArmorEquipmentPacket.class, is712OrAbove ? MOB_ARMOR_EQUIPMENT_SERIALIZER_V712 : MOB_ARMOR_EQUIPMENT_SERIALIZER_V291) .updateSerializer(PlayerHotbarPacket.class, PLAYER_HOTBAR_SERIALIZER) .updateSerializer(PlayerSkinPacket.class, PLAYER_SKIN_SERIALIZER) .updateSerializer(SetEntityDataPacket.class, SET_ENTITY_DATA_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 baa1d24d0..d3abf934f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -31,6 +31,7 @@ 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.codec.v729.Bedrock_v729; import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec; @@ -49,8 +50,8 @@ public final class GameProtocol { * 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_v712.CODEC.toBuilder() - .minecraftVersion("1.21.20/1.21.21") + public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v729.CODEC.toBuilder() + .minecraftVersion("1.21.30") .build()); /** @@ -74,6 +75,9 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v686.CODEC.toBuilder() .minecraftVersion("1.21.2/1.21.3") .build())); + SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder() + .minecraftVersion("1.21.20 - 1.21.23") + .build())); SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } 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 22321246b..9603cba63 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 @@ -39,6 +39,7 @@ 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.codec.v729.Bedrock_v729; import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.geysermc.geyser.GeyserImpl; @@ -110,7 +111,41 @@ public final class BlockRegistryPopulator { 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()), Conversion712_685::remapBlock) - .put(ObjectIntPair.of("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()), tag -> tag) + .put(ObjectIntPair.of("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()), Conversion729_712::remapBlock) + .put(ObjectIntPair.of("1_21_30", Bedrock_v729.CODEC.getProtocolVersion()), tag -> { // TODO: Remove me when mappings is updated + String name = tag.getString("name"); + if ("minecraft:sponge".equals(name)) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + builder.remove("sponge_type"); + NbtMap states = builder.build(); + return tag.toBuilder().putCompound("states", states).build(); + } + if ("minecraft:tnt".equals(name)) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + builder.remove("allow_underwater_bit"); + NbtMap states = builder.build(); + return tag.toBuilder().putCompound("states", states).build(); + } + if ("minecraft:cobblestone_wall".equals(name)) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + builder.remove("wall_block_type"); + NbtMap states = builder.build(); + return tag.toBuilder().putCompound("states", states).build(); + } + if ("minecraft:purpur_block".equals(name)) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + builder.remove("chisel_type"); + NbtMap states = builder.build(); + return tag.toBuilder().putCompound("states", states).build(); + } + if ("minecraft:structure_void".equals(name)) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + builder.remove("structure_void_type"); + NbtMap states = builder.build(); + return tag.toBuilder().putCompound("states", states).build(); + } + return 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/Conversion729_712.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java new file mode 100644 index 000000000..3b8d6d4a2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java @@ -0,0 +1,141 @@ +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 Conversion729_712 { + private static final List NEW_PURPUR_BLOCKS = List.of("minecraft:purpur_block", "minecraft:purpur_pillar"); + private static final List NEW_WALL_BLOCKS = List.of("minecraft:cobblestone_wall", "minecraft:mossy_cobblestone_wall", "minecraft:granite_wall", "minecraft:diorite_wall", "minecraft:andesite_wall", "minecraft:sandstone_wall", "minecraft:brick_wall", "minecraft:stone_brick_wall", "minecraft:mossy_stone_brick_wall", "minecraft:nether_brick_wall", "minecraft:end_stone_brick_wall", "minecraft:prismarine_wall", "minecraft:red_sandstone_wall", "minecraft:red_nether_brick_wall"); + private static final List NEW_SPONGE_BLOCKS = List.of("minecraft:sponge", "minecraft:wet_sponge"); + private static final List NEW_TNT_BLOCKS = List.of("minecraft:tnt", "minecraft:underwater_tnt"); + private static final List NEW_BLOCKS = Stream.of(NEW_PURPUR_BLOCKS, NEW_WALL_BLOCKS, NEW_SPONGE_BLOCKS, NEW_TNT_BLOCKS).flatMap(List::stream).toList(); + + static GeyserMappingItem remapItem(Item item, GeyserMappingItem mapping) { + String identifier = mapping.getBedrockIdentifier(); + + if (!NEW_BLOCKS.contains(identifier)) { + return mapping; + } + + if (identifier.equals("minecraft:underwater_tnt")) { + return mapping.withBedrockIdentifier("minecraft:tnt").withBedrockData(1); + } + + if (NEW_PURPUR_BLOCKS.contains(identifier)) { + switch (identifier) { + case "minecraft:purpur_block" -> { return mapping.withBedrockIdentifier("minecraft:purpur_block").withBedrockData(0); } + case "minecraft:purpur_pillar" -> { return mapping.withBedrockIdentifier("minecraft:purpur_block").withBedrockData(1); } + } + } + + if (NEW_WALL_BLOCKS.contains(identifier)) { + switch (identifier) { + case "minecraft:cobblestone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(0); } + case "minecraft:mossy_cobblestone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(1); } + case "minecraft:granite_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(2); } + case "minecraft:diorite_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(3); } + case "minecraft:andesite_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(4); } + case "minecraft:sandstone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(5); } + case "minecraft:brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(6); } + case "minecraft:stone_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(7); } + case "minecraft:mossy_stone_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(8); } + case "minecraft:nether_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(9); } + case "minecraft:end_stone_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(10); } + case "minecraft:prismarine_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(11); } + case "minecraft:red_sandstone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(12); } + case "minecraft:red_nether_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(13); } + } + } + + if (NEW_SPONGE_BLOCKS.contains(identifier)) { + switch (identifier) { + case "minecraft:sponge" -> { return mapping.withBedrockIdentifier("minecraft:sponge").withBedrockData(0); } + case "minecraft:wet_sponge" -> { return mapping.withBedrockIdentifier("minecraft:sponge").withBedrockData(1); } + } + } + + return mapping; + } + + static NbtMap remapBlock(NbtMap tag) { + final String name = tag.getString("name"); + + if (!NEW_BLOCKS.contains(name)) { + return tag; + } + + String replacement; + + if (NEW_PURPUR_BLOCKS.contains(name)) { + replacement = "minecraft:purpur_block"; + String purpurType = name.equals("minecraft:purpur_pillar") ? "lines" : "default"; + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("chisel_type", purpurType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_WALL_BLOCKS.contains(name)) { + replacement = "minecraft:cobblestone_wall"; + String wallType; + + switch (name) { + case "minecraft:cobblestone_wall" -> wallType = "cobblestone"; + case "minecraft:mossy_cobblestone_wall" -> wallType = "mossy"; + case "minecraft:granite_wall" -> wallType = "granite"; + case "minecraft:diorite_wall" -> wallType = "diorite"; + case "minecraft:andesite_wall" -> wallType = "andesite"; + case "minecraft:sandstone_wall" -> wallType = "sandstone"; + case "minecraft:brick_wall" -> wallType = "brick"; + case "minecraft:stone_brick_wall" -> wallType = "stone_brick"; + case "minecraft:mossy_stone_brick_wall" -> wallType = "mossy_stone_brick"; + case "minecraft:nether_brick_wall" -> wallType = "nether_brick"; + case "minecraft:end_stone_brick_wall" -> wallType = "end_stone_brick"; + case "minecraft:prismarine_wall" -> wallType = "prismarine"; + case "minecraft:red_sandstone_wall" -> wallType = "red_sandstone"; + case "minecraft:red_nether_brick_wall" -> wallType = "red_nether_brick"; + default -> throw new IllegalStateException("Unexpected value: " + name); + } + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("wall_block_type", wallType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_SPONGE_BLOCKS.contains(name)) { + replacement = "minecraft:sponge"; + String spongeType = name.equals("minecraft:wet_sponge") ? "wet" : "dry"; + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("sponge_type", spongeType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); + } + + if (NEW_TNT_BLOCKS.contains(name)) { + replacement = "minecraft:tnt"; + byte tntType = (byte) (name.equals("minecraft:underwater_tnt") ? 1 : 0); + + NbtMap states = tag.getCompound("states") + .toBuilder() + .putByte("allow_underwater_bit", tntType) + .build(); + + return tag.toBuilder().putString("name", replacement).putCompound("states", states).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 55d44c3d9..bea213aa4 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 @@ -42,6 +42,7 @@ 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.codec.v729.Bedrock_v729; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition; @@ -92,7 +93,8 @@ public class ItemRegistryPopulator { 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(), Collections.emptyMap(), Conversion712_685::remapItem)); - paletteVersions.add(new PaletteVersion("1_21_20", Bedrock_v712.CODEC.getProtocolVersion())); + paletteVersions.add(new PaletteVersion("1_21_20", Bedrock_v712.CODEC.getProtocolVersion(), Collections.emptyMap(), Conversion729_712::remapItem)); + paletteVersions.add(new PaletteVersion("1_21_30", Bedrock_v729.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 afa11c982..3338d2e52 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 @@ -981,11 +981,11 @@ public abstract class InventoryTranslator { List containerEntries = new ArrayList<>(); for (Map.Entry> entry : containerMap.entrySet()) { - containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), new FullContainerName(entry.getKey(), 0))); + containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), new FullContainerName(entry.getKey(), null))); } ItemStackResponseSlot cursorEntry = makeItemEntry(0, session.getPlayerInventory().getCursor()); - containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), new FullContainerName(ContainerSlotType.CURSOR, 0))); + containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), new FullContainerName(ContainerSlotType.CURSOR, null))); return containerEntries; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java index 21fe9ca21..685d51fc0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.inventory; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData; @@ -139,6 +140,7 @@ public class OldSmithingTableTranslator extends AbstractBlockInventoryTranslator slotPacket.setContainerId(ContainerId.UI); slotPacket.setSlot(53); slotPacket.setItem(UPGRADE_TEMPLATE.apply(session.getUpstream().getProtocolVersion())); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java index 21f45a5ca..a276e4750 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java @@ -30,6 +30,7 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData; @@ -83,6 +84,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator { contents[i - 36] = inventory.getItem(i).getItemData(session); } inventoryContentPacket.setContents(Arrays.asList(contents)); + inventoryContentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(inventoryContentPacket); // Armor @@ -99,12 +101,14 @@ public class PlayerInventoryTranslator extends InventoryTranslator { } } armorContentPacket.setContents(Arrays.asList(contents)); + armorContentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(armorContentPacket); // Offhand InventoryContentPacket offhandPacket = new InventoryContentPacket(); offhandPacket.setContainerId(ContainerId.OFFHAND); offhandPacket.setContents(Collections.singletonList(inventory.getItem(45).getItemData(session))); + offhandPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(offhandPacket); } @@ -126,6 +130,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator { slotPacket.setItem(inventory.getItem(i).getItemData(session)); } + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } } @@ -162,11 +167,13 @@ public class PlayerInventoryTranslator extends InventoryTranslator { slotPacket.setSlot(slot + 27); } slotPacket.setItem(bedrockItem); + slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(slotPacket); } else if (slot == 45) { InventoryContentPacket offhandPacket = new InventoryContentPacket(); offhandPacket.setContainerId(ContainerId.OFFHAND); offhandPacket.setContents(Collections.singletonList(bedrockItem)); + offhandPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(offhandPacket); } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/horse/ChestedHorseInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/horse/ChestedHorseInventoryTranslator.java index f1a5723c8..ba3b7285e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/horse/ChestedHorseInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/horse/ChestedHorseInventoryTranslator.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.inventory.horse; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; @@ -94,6 +95,7 @@ public abstract class ChestedHorseInventoryTranslator extends AbstractHorseInven InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(ContainerId.INVENTORY); contentPacket.setContents(Arrays.asList(bedrockItems)); + contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(contentPacket); ItemData[] horseItems = new ItemData[chestSize + 1]; @@ -107,6 +109,7 @@ public abstract class ChestedHorseInventoryTranslator extends AbstractHorseInven InventoryContentPacket horseContentsPacket = new InventoryContentPacket(); horseContentsPacket.setContainerId(inventory.getBedrockId()); horseContentsPacket.setContents(Arrays.asList(horseItems)); + horseContentsPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null)); session.sendUpstreamPacket(horseContentsPacket); } } 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 6c2e02cd3..3195a6536 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 @@ -30,6 +30,8 @@ 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.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; @@ -167,6 +169,7 @@ public class JavaEntityEventTranslator extends PacketTranslator{b}{oh^g>v{Y5 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

YrJkdVHz~Mle@U@uoaxq4WA>+KOf4 zG#^h&oWdzjz19Jw_2mv#NLy4l2l0vs!x-BB?8SrZRa1jODRn!Y$W| zPY|7BRMe5@hk$|nVQ{mvAvTaMi>8b0Y}QV-@$*YPcX{u?&bLSHsquq6f8?wHC9F8$ zxdSnSDhpD2>2>qzliuPzJE?r=X(JNCy8OvDk&bB%z^zmdgS}2fgZaXyb^gSX$g?i* z_m@j0&!^=a;+Vv=CF%B52cPwb=rh40w031%v2|OKPj5#tCr|G^1TmHd0Hx<1H?!vT1N^>WCG1#*{U%I5QBxjb9%YCn{0! zu=VbYnO$1~<47cKe#f$M<_&}HHKpnnRU}-Hsc2;7O~LI9TE1197w#K8P8y~!PKWCg zNHZnLc|+sK;{Fylzvhco$I>}Zjy1?91LfR9R20W2olhNuYaJfR63NUj_t{W$XbM-2 zJdNxtR>6pV&kUnMP0{4U8yna#2)pThO=qL+3B?e%OzG6D`lY{pPr#63MFwkBS!EFP zDPxp`(^tHr&E{_Ya3+(PJ!bf;dY%`w@np6W zy)>y`KI0eCi`bwl%@B2ct9B*bTSsKP`jzj_NyvX537Cp+IUp8~R<@(+eg}~t8-Hq| z!AV5Rbn$x|yyc?11%JCU)KL6UM{mG>Z+9;~qZcF8!@W$DsNzcDE8#SIa@|Shap3Sg zWW~D3bzOt??%r`haw~vQ&qrtf<2%=-VYAdNsP(WF`xT$pCZUJ5@~O7!JB{o6{TAtO zzXUDowGVzN(E&g9WyyMTSFy{Pmh+SZ#_)3`1@?ZIy{m{<>jtOa*}e0RJ`Crh*{a|0 zvzT&@BgTW6KQc7Lngx&!@ux+GzEOI$P?r1Wj&I7&?RMSY%Ds+EJGi0lh3jTgSef_Hg8EN(vC zq-iVdn!PI>5N8^fmryM`!{Z;EEZ8phQEgM~@e^0YX1}n}viw$dUKhnK ze(*y_ES@WeugKZ<4E(GN;Asi|@bGgv6LS1I^+;s%{({z%6P zcKNr~*6W3zxzoE)o|G%H`X$0?j_c98BcznYjRGD<|7~LfGDGv3?P1x_H)jhgYH}?L z)%zLM4pH6OQwE$ViyDpv2t0occ!4K>d}7l=EgZ z*)!OoNaVT%1u_9IgU#T0?UIn0kQ)GBdFr*gH`<8RXRAEAb>4!zkq(j}DW+OEhk3p? z&5&a{t|EkGKI3`K_tcbEkIriB+qDZMbXI|955i#ut8j!!D%2$j-R!4qL2Mst)>c@* zoi&cgy=6*;w!kWq>F1(J)byip@>u*OOiyIAEjBf6KUqD!W%ey*IZJkY@}5I*iMa7y zOTnv=xRaNDj7^PJ4`-ivist{_1(#2?y0A+hHG2Q6K*&m^zLpur_(#4-YYO^Lol?uDcoAW??!D3UQ?bjLudbf}(>?N~(i zc(?IO37XB4KmUd++SI{ADf(1Af@aw)Bld%LCPR)M5*dRs9**LWVD}I9oo3{&Ubh?V z?uI1fA6B~^+<~v$x@RU|EFjDD?b0TflS{IFF@0OS;p7{N28gt~sy3tPO}5_6=f1hV zsi96smXV9)mR$Q5;(AhhK1MFSOm;FW)$EMqiBq!9PobPq6d{-Tm7_|C>v1@C3vPkI z4^n<*yf{?;@1t8CIOGC;hrcyqn||s0xo$|V98!{bwU3!&yND+IoCQd4<1=S+5L*Ae z6cfaApZjtC*R_JeKO9TcYzI9#SESDb>7}?>JwHuq0-mJV^-eX1yW-Y+yWLf8&s!-5 zGA|_Y61<^)OwPItqGc9#=PgmGBCSzG(xT+3#dTxB#bZ9gi&Tx#j6~Mp9yn zgi67<^W0-5G@JHBH%B=C-?K@5Im?bX!gl%ZKZbg5f75iO4^|YR^$l`CKN-y;pHCis zapT$X!kcgnb>R%m21EN*GW>Q3hqWj5DP{b+kp^wkIN#&&*f2w{eB6T|*||S5>z86Q zd+cP|Kt$5Xg^<^ua&MD;{#5HnP78d0FFv>HDlWbwmoTNf8VI~>o_Df))6F5)h}*lAk%fjQ4DQGKCF<2%j0~iuj43y8%I9FZOW{nE#0QzqZ9SNWwHs^ZZs3r z<$H_F_;`t7g51tdoV)REOM`uv$We3qJLIm2wD+GyEzbh^PV}a~-fD0h1mzc)&8QL0 zQr;9`^eqL(?4SKb)liFZ7wA?rug;LnboB^?pt!|;sRgvMFE_DB8d^f$iuSDqIv_&G zFhNmb5;1v9!fPL~PHPe18A zM|C$x8#CP2Rt~UzcMXuV&x~DqBWSq-h&7jopR~Zcgy`_32XfZB@$(ovv}g;+|1R@?(bqqUBM?`wP&v1)|hx8%N?qW@@Z7sNZl16_=CKlE7$OjsB}l*I%Y%UigTd z%5To=B1;S3lr_wBWna=qF-?7nD9O?**FfdoEoA#n?(5er89*w?^hx1CVnsvmP4(ow zRwCTcGT>~3Q6$?sAxH4)xW3R2Pi|e!(XeUc9S)^FP@?gXDiFo<$=>Em#1`zKVp9=t zJ|Y-3M$W=2#E@U%i@+Z>&UXTKv#c-U+B)(j?pX%-CpaLoAiV1amM46J9)Ws)9)n96 z=k@tfT)%n|1(y|jDNhBCWk@jb_|C*_JPn?6d$lI~bsJLs)}t3+=|9ZQq&(&|`(0Xp z_R@lnZB~O$mQTX?{MFE5(LSL6Y`M<~W%<~gBA!Yxq|FUDmfgfTe<2YuSVkF;JF#B6_1N9-yX>cF2Q^6Dr?deJ%V?I}CCp0+!WEF(V?l1;@c zfC8w_D4S4Q?ZC%T@0*Lkl_%)hU{Q z0>Q=W=|YXg0Z3h58a)Z%`R};w8tQr33Q_x$78QqY_8&Qa*ruw z0pwVF0RjF|j7)eBA0=}bCf`F0Iz_!C;gG8$U)P(X?QawPLn+^IZ6+8@G(&>~(Yj(2}3h$TMCsvYkCI8vT(`zTwl`#bAgR3Y9}lIbj&ZPMVFv8Uu@ z=GI~OC^6Y>_fPW!I?q9%@mqBJ9kH5Y5@H-EM_wr3hJS#S&_~p}l{7o)XNy=WG4cc_ zcg#VFk%D9#1(AuF9)5#MYib`uzeEw<-={z&!{E(T@84Z@A5AqV`-hnaWkX5K*tuWb z6Kn{@KCg7~3VzH`ZZyqB?sndJ)>HoE$(U59+ji%+n2=OX)R74n22mByYyO=3tQ8q~2j_`kzQHoocMPj53jN+EgV z?GQD2vEr#E+IE)pCI`ndApyel^G6~;VGnx}GGai1D0qk$Qn+|aV{rD@^GGPxuT z`~8&4 zaaiq6s;l`EU)C)jpOCVc%zk6-gV;&iVijfX)R}`zsO{KiwbhP( z`kHv@SP0kSQ}3Rm?KfdBstBU!$ZRrDY<lrg zxV|asNty7h6ZK2>>4+ui#mamqAe??mnfu0LDA$YPVoV%a|v$2uLpX&0P70AmoOHA&q5s;jqN6gxaP-uW!xZ%LYw zo>Wfs4^BsCi+5&#{0Zv5>wgH9lqy*t{jhNPd4UHmE&L4{(Dd=2Kq9t`RxJ-q&jPMV z=X{v__ZK*5W~+Ih(3&P;0}X1(aY_mn8xnJGrG%7f5(;HD5S5l4-_riCZ?lnna=mTt z-}KsSm$r^k)s|zD=_HnXwOnrz)u+lS2$Q{(*gOXl+uoA(npZ#)@IO8VHD#+42+2_P zdWhYVNp#?0N+r&4$n{MgBE#hH0%2h_6e0#1eA&j9;$%8VV(QRF@#qR}fHWDfJC=sX z?eS6=^`yIG(AEy{XZHNK404|x!u*cVz%H3N_!_W^4E7FPE;Nw=yO`)Lu_9mS+2n zZn^Y{UkRQojHFCuGIhxMFi~7YAz<_({ih|_F8*pjJL$;~UL!)*xQ#XOS`@8EHr;6V z^?GGB!1~NBpo?bqo=qppb&2FUxF%m%NkI(iV~2z2N7A&=J2pom_s@Bezy4HVAYv!& zs~>#w?zI?;(O9rWe-1VIiU!j*5SD{^#rM?$FXb_!QMCp8!S>S!3p`DIcQRa*8ycj#2@ z+2#_UlgEhKU#gPW=)edfs@RMpDCum+oht}=eaDn-iaBS0YT7ABldWTW5T<`VQY`;tyqoT46 z8rXU`tTiE zeYmI#)$z{Nuljj0l?^ao{xTriFDqfF^Bc6keFdyo>ii~HBp%AEa)QV9;`T0g9Y0*e zhU#3Uk87KMxKK&eT}iI?Tkf91VJ#YZKEn!G(IV%6KU!!oKsnFhFJ$hc985agoWPL0 z*xtNBZ zUnpgd&}k6aC4ayb5!cJxQZgGBRSYz=hu?o?ggU>b_Vk;oTFJP9+Mdo(O>JdK0XJ5T z4_?GE%{)w4WD1JeIGcx}Jb6tiD?dItBn*aEB1%_3>boP&kQqJs^6dXwU3V#OZSD!on(oRoJs>&d485pQH6<$>{T|*X`mn$x4*Ez zM|9HAc+PF3iYoyEa-*l6IdzMZMUgyLB~~f{`AD8v98KW>h0WigDOq#-TM}Eqo5qD- zJgw`cDri=ZJb&`Y*}Enh0^$K2@ z{GC6HY3jcE^!T>Rl-=3y+VWw=GX$z66Av7U!Ule>6SbLv57jg$@ z=rv9FISh%Ir@Lm?O>eSN(oOi;Ic>earSqcSeFXA>mg^2bPmAia2sf>m%KU-LWGTH# zR>~+7^E-R_sVn@d;ekHVPxaU&nSvz1x`gsm{bPZb?s{W3GM^e+jVWyit8l^_EVKPc zNf3+lDVnpW8Ji5jmJy73OeWtrX(rtPu?KN+e%1_WTP}#y?aV$gi9m1MwE^Fu&gxM6 zP)d#6yD2Y49zly`RNus=5XThCj)rg1Ubl+FAGYf7**ASI=PMY=lK7yIp=*B^neR2X zm48+$kEg%O{@9plK-M>fh^Y1ft0(%9+vd^b{wmHV`q2LRs3LY4tm44q{_xi2mrY>9 z^&QKvK"C!`Q~=*+Q4p`u9)6vB3ArvIra><%uGo~BfWkLhafF50#<@7ziCJ>>u6 z>MFyk=(@JFbV(y2NH<7>gmf$2-O?Z}AxMXWbVy4#NH-GFjdV+cq`)^byw9KS-&}k4 zs(Y<4jT+`};NOZr0M`NIp%va9o1{E!b39}gAJ zs96f8FEmJ|Uf&iK$XZ=Vv6dFPvn$-hYS{G;ck zZiV*0Y}E2G^=g6GA$!H5X`)G*skWt)68Cx4*43f(mQT??dXLhO`9E3U*-vzuLwT>31gY0<}9e9rxp+iGQ$ z*^96>)YgeRCVy4oa)4$w_8qsQLoq?qS0rHPXxl*?S^Gr3Ev!myc@rmrWDJe5bRc4g z%4>&?doI7@9X=xVozG>MxgJTidvC=@`2gVtL9=~o3|k5X@lr(=vx>R)lg+hMb;r-3 zzLm=BEws_=x7>~uLTW$wkN~Sjo?mUT?{yw8Pf`bLD;#WJ1YVN>z9hYtR!=ig1uKg zam&S$pG(*A0}lHtP`W3tYu6jt0E3E8P~UI*#@w<;ti|}AkFJr0_ zj+XX3ckmE!`(N)C43{yCaX(XC-pF|z`*UGh)MunTz463K<>ehA_e_8zk)CkS`duTe z6KLJ`T6>gt!}81w7E?H8i;`nFw<$v5`xuOv!LcMEnApzp%P=up^x`p?Z@TTNa)MVP zDW6YRIaJMTwIeaGN+(>rAm{q)hUzeE)~#J)cCd(FC1TP_bH>(MV|UZ08RFWw#8_q? z;jD2g4uF+X@Oz6UYk@rimYuC_gAp{_;Vu_#Pad!x-rFlKaeVOR*n3#hEjJ6%MJ`x{ zf~-WKEdi_&hmxTTt_X#)F_;%Mu@fv%Mz0A5PY%?LJ+tF6aOT5*A0KGoxCU_H_Mw{q zpR%<`A|V&p)hap}!CSp(jVy$_&Bv;ip4=nBygekWmZuwF#Htp1vOTjSqEcf;ZLIs$ zl~MX#Z0ptrT1jNZnE#q-AIa^t1E9|2pN-)^{DG66b%*F}!nR;1?*mJ<;ts{>-q{_B z*vbi0A#%kSbL0DU^CqHHQ{b>hp2`LXg16QkV2wb*V^z|oEQu6?ydbH&(t$KC$VIZP z>tAWb%`Xx$Mo!ocCO4#oEX?ySJh5dC+?$g-SLLIE!W)r)&qMLvRk3l8N$x%m4Mq2I z1OrK7X;z1Kvk6wC9zUe>4FZu~P3yvN)FHS19*KctF<39|Jt{u;bo|Xm$#!&u2GXlp zz&6WES<1tu@xMSd1THgFb8yc|B0Y^p`xET|>Qu!4kkU8pK=Pb|X8K?}14LVS#KKo) zREX$L?D6aLfL46KMH((rgSC=v;0`O}$kKDbQ%waG(z@C-*PFXmjzI-Zp5|l3^jhJd-o5Yj05{ zYiv7WJm&5_I+00^t*S#qnvn}P*lS(g0VV)n7<=Z#qhQfaOm^4v$czKXs0s)_Iq~9| zB_zS4faOjUU;Lc?2kwL@z2t*qMt#rG%be?NBHodOwab)~!<229EyUSEipos|Qwk{0 zD}E}=nyCgk0dEd=w|SuILKKSZy=sYD9_%fvmW(hEyl%0cg>PQ&p5G_NqqtDATIm9l zJo_%^W$z`@b&Dz5Asg!YZSFsP_OS1IS^`l~h!~ik5h~qyDba}&pO*&m&!V^yKSw7e zd()ev$o~#Z+jU+LmVHS_R8Ee+_y{clR-2_tEkH1}^M@j7PKYF0IcMX#YRqes0(#^j zM>5QGTlag`U% z6r*t{eT*zH96r`|7P>$vQL<%{)({U5-?J@P&&W-dLwg( zQCZo;s4mn}Ue@kqiQ2}p4RG*#ZNpQ9hS(@(!SNFTb$MCa_Nq4DD=3bR07>9ZOOwIz z0{qC%K6(6K_RZg-gn$2#l%s8upA_KA(w)lsMYWrkJ3mtk3o_dl4N(&5UGFo9&&(952rlAzMi_m*)bPH< z8t4AX^6^V?ncSLzx&C~vdJ?OKuv6sM?v4rFufH(gyjvG=g!%7uzp#%SAPaoI?wKz6 z{``85^_5dl`IzZFG36Oo;qdFzk%f5KAY+WrW|n(Q5xoaa&Fm(D8*3xVG)58P=`r-A zgeHy5aco;D%@>H%=EHL$=61)ttYm5a7F$I&yBHDZxz+wddy+6wq=axv^8TOanku-f ze<5;wX!Xk9?jUkEM^rpWNo}ljGr+_!wH?hsH`QLk z*neJaLUzPq-_zk{W&GSW5g0Xne1hmP*XfDqs|XT7we=dErLnd+!*R z*$Kqk5xEM=+YF$H;}bpVjhT!6&@I74i1tovO?djRpQNN?T% zZ?{QiAu*iN&^3?v>x#t9c0>-&@l5TmK+fYKZx(fLRug|rTZ-!HsIhK;_Ge{s zR`Nn5<}*V`kxlLWmhm7z1=OLZ!k0<^#U3&D=vX|<&O4sS%}$^onvs4V^v;jcGtS1R zX|Yg|FSR?be|%{I*Sn>t)zK@|dJBAh>|e0e`BN}Nh1emmpu~`KJr;N@3gx~1N+bJ$ zeYE3;tJiMH^EKmS>-WbW_xY#VWHo)JgO#*kXc_FQk-gpFMidN3Tx^I^Uz;JM?%nGN zHG<{ES&c{$^`i?tt13Vtzhe^%7st{|oDyke(<;JZ@vea7EglPcm;2%Gk9)*;ZeBC) zb-l`3{#(Woha6xQIs09nEUVLH@O; zvGKdV_w_`n5^+@?pSq%FaP-fVm@`f0_pkFBsYn)d-s`?^ zi|KVZiy|mEFU|+8Kpq*2#Kk0Is`{tXOD z2$Ev~6=p&b5i|Id;LP_g1$o6OvHxfd!j>pxa=s6a;kdwg2JUW}Wf(XM2d#S=GFaj%FE@Lv2z=8;ob-};3UXV?j$-U4Lg>(+s;oIXS z4X9d16KJ{%7!AdNea^N{$lbI~F6n@GX7fh{vnhOL@X50pB+ib8XoBn}iLWw-XWQwi z`neW3OJ9M-Qr&c&ziK<2$`K$R^7d^coC%#^k)Zw;;KzypGM|IPb>4nj-7$n!&c-uM zqX0x|Uu+)OP!ZfYF~Ii+Edo6SaO5)k^fyRtL#vjzskXz(905Z~#RmmWpvBZch6Q$R zQy^9Lrj_r(E+@L&(*r&xWH*B+dI;Dx=40zcVUNa*aFpTdRVw?-y>pXFFx@YNAS{2S z>@qf1qwxu;tj6;#dnl&NJ=xj!|Nme5xtohOUwog1krv4m)E7!Paf-^VcDIRJu!XDq zY<_iW$jM8S&_SV*H24M8^dLg8<@fu2ma{eGkMhUqATA ztxp>cnlU;t1qNy;zK?!4Qqo_E5dY4{d`O(pDc8@5_*cW|6JiSB@Pq!z!R`e-R zN8XTwxp${vBH7zC!zoAC1cyoeaF4_!Zoco6{VShoArT$y#x>V6K%Z*YMX?+!8EpadQt&SgDX|s5Cn$a0@R8ET@@=n-4iVa!n*bQmIqn~(G z;ONC{PDe;6ufI6tR)?SS2@#cp{^#Z z)X$=19X7<}e>^O_L}E9_Lozq^x{i|s0ehSTT4DroA?anf4L}q)w=R)_nuBye81iHE zM;@SqN(jZ2WDzKYGGZQbOOzNVgn9#jSq_$lU}hU4YQUHIZ4X8=Qh$?NsilFC?Wci3 zQKJ&3Q3od7U4cc!uu{@><`Vu&@Fve=!!tZrCe}4AOpU5P6#C#XsD5I3;UwWRDaWb+ zgxuIF)C#jxhkX_Tyo&EG7D;M_F!pO%kr4~WCl+edbOYPXP8?p@MQz{_bBCb8r+|xX z48_=IzPtReebV*W<;s}{qCo+p64aNA+J(^qyuI{Hk6$#P9p^ z{w1}6i>P70_Wb53EG+21PJLOi98-i({wWEg~xur%P1_K>{#`e*(O? zY~Uhm*c+SIt@uF)bMNNEScVk1#t~oI3}xY_CBaI`l0RTuX^~jxqRVE7T3j5jA{+j) z-#?*@7=AV0YkFT4vknv(@Gv*R$Y7R^45{JUW~~i(DA+cW-U}cd!*9cp;m$-PF1ZoXhnGRXi5&>M zi>8q>U#v@k?`^PY=7V^*swLE0hH|L0@17=oHkG>iFf0Bv$)ek=+y=AGoRpr*X;U_a z6A7RtoSUguDigSk@47b z#pJ>KFBPV+c_q^>he*5TO{vVJ=b3DLGf?GL=se!GxhVD`lF*C4t=dpP0BPi#9#(x) zZIR?aOv&?C4}4(7R53X{S!;svql%1YKjC*GJlT9Q57gFa-d2!qi2~Q#UQ>;NfC7L? z9O`R3()5lZzwJIo}3TFMn?j zg@DdqOi!$MH7HGl{VuOZkB<#L3hPoVp@opdG&dsTW6CT#xGQAXW#O0YF#C8b338M+ zkPC-TTn2mov+5neg!EJ#Zi_nIjI4SwrlTuz>=>AmHZW_7-y}<-PZXJuXJ)s)3E^EoU zwj843D0u5R>caKnlXN~U;oBEuSmX)KA7ie|RlRPLRArNwvN(wE&>52TO<2@c6&KM9bM9@Ms#ZKfWDke&v*-l4Uxv zRYH|l8ipF*z40UcDkH7u)%#+alHD&2`CAru+Q9k9e3Tv`{P{iBy8Q&BSjB9N|AIC( z{lc7F+0}K9Z0+i$Lhe*52O6>DvuHbeiNMV!HsCswZ+timaj;&Gkn0m-=B^2_e#?YL ze5047wgjy6s1tHseP(j0|JE^(KKad*3wWTiO_F6Isp*sK6{R3!3nA!YHXYaz2LYZSjtSNjHpCMq4Ji~M@u70)bcJ}hG)zy z>h@v4k9xzf?R~~4Oo^+-jy}!;UO!0mb;9k9_bJlXBXQIStsnwQ;q#n7fW|&VTSTFc z(MC+;o}PgjO?_;5WA9Nq6iwJ8dX&kT_X+X4bkSy+#RYv}nVLW2cj%^_uJJn*W!ZiFdaRmcew24e~gKE@`5B<6UYd~T|>2CM0-YM@)9c(Q^irkZ?Ny0 z&Om)f8N{j)L7StU#>Fo7b{HjoS;h)Sf~{F3z(ZlH1S{`iz)0P_^V)T0f0G7x&{b}G z>w&(l|3mKKopy4t^p7=r7Zna#l}M(+Yh%Or1=L&kh)p{iBEaLpbzRO^CVl{x(!0># zyC1^_gnvUt**9d`2*$?`i`_optAGs%!}FqfA*>%v+V@4Ko^nCO$u(>ib zCENE+R{qhB|JaK`w8?3i8MK&!qM+g&VazAQ?Kw!2=_xtNnYb$8=3t?bCr38yXu&=Y z-t8nhXmKahQYsO*1kDEWvfeEbj!1!*#WH_+49GaSgQPGc+ihYKAOMf`Z%->;g-BsW z=)(~GItF363rJb<;`w|T{?l{L!kcyb*q7PkU&SPMUGNFrk{M|m{bmR1Qx-R0R=*^4 zSnfzbRFs9ddz5ehNbQNyC)~ZpP{Vci^HGPRCX&Cp3Ilc`73>A>{F7sLg`t9;mbwaU zNUj^1WkeKKw%7!QBi<&&kyCM!T^X0XTqJ+L3nYC0{o9BbBlRMk+`$+L{<#^s?qT1& zCYcbgzBj9O8YU0LY|M=G#51Sp&sJRc_>1h3sGJo24&g*@skLK({^mv>;Ju`P(J^Bw zN0Q$Bvz_eN`(`6lu=0Q}AepIj;5^UUHFZAN%8)htt$Zu1Iuz-pIp6We9{=~(2+VIz zvPOrtE&A>loQeG4S&j#ox&G0m0>aesoU6R4n3n{gF7}e{)zXs4-7)bTe<^Ns&|-Fm z1!qvDjF6{g{L=Ujc5v5fZRK-IM7%*mrquVCDGI$D1V08@C)l`40oYJZUVU&h0y_54 zi50Uy@_Ay%6;|_!5GL3)Y;}zprJ*ebc4O_|c7ECmK(%uE+Lz1NRyUd$|D;jddkaiM zBKeY^eLmiZ(es*$%?%?23*Vd)J8w^9YWkhS4?>oo33itG%ank>>fIiZwZ$Xc^RlkLw_ zBy|yG$J4Yy_g=~KLZevUg~~yw-v+`kl;%=KbAo$JzCKsut$;OEcf56&BrwSCJu5U~ zKVBr7N*TAs)V@lL|lGfV8 zMif2Z6K!D&AHDR4PeF%i8%!c%^CA1EQe;7E&HaXy6J8MMla?ECB9rwG=ZCEP4QA`$ zt{Wapq4?5W0c%aw1HPGbIK~p?6AmKAf3>m+l&&Jg%8+{VIP%`TXBKMa)cfRagy202%c0NUxi5K#Fmwbv#4DQX%`p zFo#m${6C}w{*TVLo=#a!+Q>C&u@KbzTH_YtI- zlE;dtLSA<|d%@pWE}wITFKx9nxG`?kA?+W}JTl%U^WtT?e(6|0I)_KrJY6O7i)QyM zpZbtz1eny#S53}iu5Wb(XPs=WV=CNRyL&cKoFC#-XC^!!Y;z=Q>(#T2lrq!|2LCd? z^u}TvcDe#Vrg~9}!|S$0ta||xnUsfe`)dqr z9hlc-HI^@{o{;3D(EZrH=Z-~~ z9YQln{WzQnK@|+^io*q-F?Q8t&ZFj^-6(aAVI}b*Tcaz1n-04XQbv#{|4}}E=PaPDqHVPr3*2ug z@45LNy4O2%RNg)tl)Hj3^c#l~pHdFA^|pW@x*HS-$>gSm2L)Et&M1vpO%s1$dBg0b z+8{xm#LuXc2Unub`4eOjZI$wf~5j-A{(bP1c3wXY&evv@0?YV84@kn&+aAc9_s;Kdy< zLdvGlaJ%*UnOuPYmrC7kqd-~1ruWwR5qkZ|%}-{Q&VZL=0itLPga$y70D!p9-Dk3a zfCz85-4jCE=KnZ9#$1m*g=n%bw%d-HbWR_w`3chNL|!<93Sskp@Jl)rYNzBmz87UR zSZJ0Gc}^-25G8krBLJA=U2Xf0^`ZjM+HV1*L#Tp6_=K8qAdlB!r=}s7o(ZhaLYQZr zcS<&~yJe%TAfVkmqx)YiwS?cH7=o_v4~%EQLg|+$E}BJ6&I3Uds1Lh@qWCczi z7p7oitbC}xa0B-}DIZ%1fMV4*C}jSxZ6zl*PqoHwUP2NB&vqFu#DbGp;Z@s1C3ykLAYM=Viw_m?AGa=n} zM*6Xk1mVSD94BS z#vpX{#j+$Zpj(Dptwum6K@pUYnteVimAHb{|5OLOG8DKg!W>R63aPfU{;SCWBDW~e zgt{H)c?HX|9JL?|-8;b{CkO5+^EmhQhWMOJy#@Q%Z#jzNfAJdX=0q5QiMj}~)h>Gp z0@r>WO9J!~))^4{)vDkLDin!qxTMu0KFI*}xx(eT%t!Ix~E|XdDDZ9Zc=oS8s>kpP`h08E@Wyc&8<`BKF!hR4M zJ6Ie8@5o+3MSH9{+Mfy`JOY;iyeA8UrmrlIR|z(Nm7?whrt+*C5>~Cxq~?D11QHH} ziR|g+pa897)!LhyLHMNo@X`KXk?FRvi`r^ZL!OQ-mW2+=1CmM5MD@# zKlq^vZ?{tN_+P0hAKvjYLn4V*K8_h-PYv4hx7Ytmc*C;iAq-eB@8miEyq7L|lG-*v z0E;4x;v>YSdqB#d|%=R*k-7`K`1)6dC^OLE%=7Y7eJ8Z^?`>6 z;(4ZJMdP+nsGXEm*%kLSWTDDG6&_}}Kt+eT=rVd5?=}GkDsCe5#V}xgT6Lfj>bXcf z9+ZBdUWEK!1iCE1I!D~3$F3Zr2CQnYrrBXVNE_MZdpb8Su!K>vONR#l1&S*OZz7rF z*Rlg3FNcy|EAA5zLz}f5q6z*p2di@!awirDjfb+!+HT*xH&8Rz_x0ie(~18I*7#34 zH0%m(=xE)X2_PrAzZQi2)|Ubt`auw8L-XHq*xc6RGQcuDHRX?B0KK0TD$LeHRR*E$ z5b~bJ?FlTT=H2^&@aLkUph1ccV{67p6cBT2HmrusVm||XE&S{q)Wi>@ny4MsGTDB| z7D@v%OkX!f;0utTjw{OxW*v2*urMNynA`}TL7LUd7ki@Cfi0r%i{kUv-PYRE!?6c89VsUL|1OidvfaX^}>wNz3s@jas&y&Ls{_${l%?c9!LRx>#beT zkTv|Vi4dO|w{_a|S$ z0Xhq*4)8w^kj#Vv@g;bAZ`lB^vj#@#zI}zBtu!H|z>Eqb3LM>keJGaH6d>!EN|+%& z{huMUpI>?b@3RK-xI%U#x_<@(r31F>0D{192zmK8G%@dHx@U)k$AY0)1&U4a zNCqLY6xpb=V}#UtT9zs@CV_3Jn+ zJp*VSsAGQq93)7f*}O4)IH6)PXxxt=3oYfvg&8K~m#nqoS1@g=?cAC9B!E3pA9DBE zYc5ELpH$f86Xe*o*x={KGsy4AU1-o|Erj&^M*&%u`Z|vkP>4kOR;kj!5`v$O^jiBr zW?+f;oCS{y$}kExZ61%H#i-nM+Z}@STszj7@E-#hwy6f5slK^_RmaF=`mg;tImsaN zE2W3VWDpiEOP*T)^?vVWG3^E8OCULuz0*V*|JlECTF;v;kAFN!Fz#-OL8%J(f@}ho z*l$$Vmd}u(G=twbO)XJC@?2S6E{gx7AN{Y9c#q4fek1ed8IMP~sh+7oC=MxVqyL(! zUwJd*vA45e%VxO4p8J6_&hy%9`bi}f9bjJ>^Ljl0&l|x09_Nbh_g4_QOMl6z|7-1o zE~>}YhQTwmk!O5dw#H&Pa7Zh7(Md4+Q6VlGvY%vN+Bm!K3goV^ujE9j8$$#l;nD8< z!v8#&uVJG5kBrQu%tQW1Mp-d;1^ADZH@-7w4uOb9$FBKbTW{ZHc6czn@P>iY;0t?> z_;~#B!DxJBB1QyVSvm?0V;KIRXwjEv96QagtnJC;M7;O?CKQ%6qOlLfHw)2&UN_8DuvRM+J zp@*O3Pm_PNqzwilzme(p+?c@g&O`*?CWs_<+g~fed+bMtKm;XIb9*8_;1v@?iJiP%LV=;a$OPjnF*xsBJqJ8 zndZa(i%K$A&1?vr83N2?r-ra9IOhBBo1VLf2gy9+H%Knn>_qs+Ls8~8=PwTMgQVev?Mb@`=9e!s+4m{g08osLVr$axhc`2Vo!YJSA zkh5OfqT3)6AM_hR3Q6R!lr2_ym3oG&=b2pM6F+^AbBO z2X_5dIZ!_a9V^586?!FzF`*;S=3|WVNjItwJ9FsmUjRX&v$W$NAFI!zSf^?~)nbrv z`-MYO-io8#Ok)I6SZYMh_xdBf;hR-!qU>dzNYf`qbj^rxGzprp>AGKlyL{4C-9>@I z&5k$jvA?;Ximr0cBm26th`Ix|L#Iz5wib{k`sP5 z=uZ}LyVIhi0_xwFnzhG6|{OM3% zC;p!An^q-6c=qiwaTiJ&FI(#xLT_&;U6!GZyQUv1K}glHtHYK})-5B`H|2M(g@l+i zSTBuSl{G1QCA;XdvXVWIa0pni%(CZ*+R93XgH`;DhNc7Lpw#Nz;*oUDx)UN0E`4Zp zN&}Uxuq4-n)m{V6B^m!|Tkk^r#x>N6I~vvq!;Zr1^58O>h)W)FI@88rn{G5I8;AxisJ-`3P7PqvUT+j$m4i>qhi{O?Lyk z_6h1Dw!wTwEExakn(YUcqJpK~uYr{gkJSA0Nz+ItHQK9!$=@TO)sSWKauZuy09+FL zN%{e>6B`!y7ESAD`slJ^a!&jIKsEd>GsO+|w=qz|KefzxA(P|M+C5^26>OoMvAWKk zP)3u#J|pa#zM5rCH7qFZ%V_Ec3zIOjoTinFOwit^GjMdkhpUyBEC9$u$EE56nQdfL zN8k#vS9>xXW{4A8p(WayFXQYYq2N>3JyX#|&jK5uGqlTFK8Dw3v%5c|kE9W)E?Tzwi0Ws|Ef(|UW zfz+hYtwC3ww6j*$D-^t?Nnuy{tZJD38LxhrJ=;l@dwOKw5$Lde?lH>q^(|=m)b>L3 z_j@FgXMs}Lw~K;>O8PrLL@tE0<1%SYoQL`3`W#$87E?>J(cGZ{7U&cLN5CqYZ6^(a3^G z)-%%qKC9Y(TRvbiGVeerh?0)!ezXGSt{cNwJ)m-q&HdR6bB(4Bkfp9#^Lk&@Np`J> z&U$&eSYQZJ42T9rEsU^n6Ch#+l;s@!~vXbJ%n!)Nzjl$X`@U2}sdEJUdsH5}J zHcOa0~D+ zOKk3M)50if-T$&F$Y!{mLtTl-i}OKNxC34pHq-CGAa^(E%Pq49RQ{l^a+6XJM9I!99YT?I-!n=pLnr|g$0aaAc}@Q1UTT zXUmKW9Ww3*+jA$X`F!u<_YLNuuQ|3b=^TY#N7<4FXKl~e$2KH&h}TprcmMm*@q*dh z*Yg4Ksj=?|gf;6!benn@iQ%IK$#HAzi&siZ9 z?T9CDr<@Z1ti+l1*Q;@T{Wl#9+5AMeNa5T`6Bcq6g7h8FIL?&Deu@pj)O3XQ%oJiw zYa^A`?RD>Z1hx8Rx0I{? zz*SC10vtT!7tJTgu{JzbPt~a@si_5jv;-JR;D~j0q*c@4mPm_9J1k7Kpm&5(fBIC& z92t%SE;Ym{d!TiMZBYAtVjll%9e^kE6#2)8VH0FfMMH>o)Jx6SPcftVL{mwSuIe*p zZfsD{a^YB;9@zfE?-Mbt?{gYx+kqt&E2QH4T%Q`?4;GH15B>EgP*SYD@A?4Jn(HA@ z^b#=1fhEgdIfOX>R8%SJ}6Axu8cHVaF_BQpWr-r#<#>U)LhFv-|?Wk zA9U93AzWf=nNLC?>tmKyj*v|)0{!$ipfV$`d!ON_>bW(r(*0K#_p;DkDcLAG5$LXr zDKa^Z}WKJaj@26Tw=~?AM7Zm&XJ|8(x><$Bf3=8H%z%B2D z8h~@WwH|hT;W&7S*53$oIVGt9`aDUy%n8UVJVDmtwo8?T5ZJz%YKVLP15}^H?Awqj z=mKU;301B8?l`v?$ctvSCS#A(Y@e^)qEYgszbKa6J zJ^Xx7sv(axyopx{G+S}}3u}=X2Qo3_ddClB`w&)|*VA!=7z$KEV6)?QO&%!4=6cch zo4)|OoZWoDtM~UW1K}6zS6bwLR6w=ds#p+(quV@HERqvJXg@2H*4z|Yji&TUV z2J0*?okCB=A>^*?cn=+^p%5nY^!fn18L-l`ZghHtP-5Vus@KV4Ah-z68{iJv)u643 z(nVv|5736;(Q8~vdh<+s;3N$@igQjWK( z*mtq*)M@lyHI=}1BOq)(-sstqJ+A@|aTIQgM+e^>g*k>vxri#E}r z%=EFAIcxz-hf~}~;j!c&so9V{>V+EHA<>W!gVujnRQ@uW&YMVf_GF;lPhE2sQ&5Am z6`9d53ah;VWhzc56` zCwb-k7KD6>EaOUh^!X=W>IgO1gIE;g`k|0lTh7^tR0gX$U*@m|lih0N~YbFgAU5;uB6$H9gY zvQ&QXm&F8WkZNUNb4_I5e*m@{8d574Ta-5d3QxH`*SYJU%ZBEhr*@eP5Fo9uCLT7R z*cTW%9Tudqz?4K-yBu;r>JMe?WdYj(jNkLQ#UF`FjZ8BV3QB*N!vcb>bMj$4@llC< z+(kp^C%}IiBXxO`#Xh1jFk$&?REyujz}0NU9)pH*ZU}ZISu00{??Bb3KEdH19$S1& zK`X9-Bkcl|al2n1y6!ea&q1`lUl=Z8Dgkm0eIEb#7e_uqDmHI;%6ov^EN(yKy=zE6 zgTxZgC)Qb{U))f2)Xp!>hc+=qfR;OEj>rzz1fihY%Pk!0A4ApMui||Ug*txt)@Kze zru?ZDyLmv~UuIUB_W9v7A0!0RlDfjpMgcA#TeXqZN5X@#i4m^{3qd(4oGGLQN( zc!m3c3J4&*2Aws;3my)#^yh2117;`WfHE0llou&7w0!eJpzL@(8vaArM*i!vZ7@RJ z%)OMj1|(|8?j!+gyHGxazR`Z3V#DVkXt~DyH{NAGH?VtAlu)Z*9P<3r)DQf}?ukv1 zTS<|Ij9D%oFq9=Afjeauj;kAj86cR?do6W!NWhFNHIt1YUr>keT~qmM2?{m2t_kJC zL;1Oz(Gv=$hXMRNJnWo*-fAo>0%cPxvA3+N`Z+SBXgb)NBAixFqA_13& z^k1QZ3;B%CDi87pWzEd|B1GvlXhrWY@zLgGtH zBN=eDYLk7(gQflh&X*vkaiWPcaVO z#{+SYY^T)K{BM2>6=0FAMZu)0-msz&m;wDf+G)wq7@b^0Y6HBr04y)7ey4}%Odk12 z0~!E3t1c@qprskRw@r22Od$R5oe!(TLj7GB>AD4w4hyWbCDD1OT|HYkIP4x^7v>Mr zAd?O!t@XdD2q7`^n?6z~3WpED^o)?wilPE@U}jkX4?t|nWcfq>v^pOVW0gn;{KLN~ zJ&*!{Se48xad%2=~{zV!}IN^zig-ZT*{Q6j3Lv5;f9~{ZR0`2Uq z#yilb>;ScOFpCgD-WV-JQ2a|C7>qy+F0j&1ZM~n@w_rgMKgvG<adCpk{+~hX$XHLB>Y$9rk5zlP{9Qi zn+ZV&sciQ=!lo@T$SD@nr1=31>=xhUh;b0+%TH5p85NeoXhPp{HDEF`Vrd)JkmLu@P=H)3lC(94lWb=Lu@ptZ{IDQ_Xs@=1QcB!{JWFVhDCb2`QYo}9yy|&$acip%kBHsIMe-2J{d9mH><&hKMCe3QiUgU2b zwuWn}j>hyso%6?GbEM$vAg~P#khmu}0}4cQLC{$G%o))%0?vfL;f`?*sSi*I(EU@N zb#>p`0!NUC*aJDl*bR%T+&?j7LZ9GV*p&n{L35X;LtzT;f4ulyjh&JKibuEbyyuXa za1orTi+YoLypW^T`wVT5X9T>h#f(nqjBv)fhXBU&A*eg<*c&pV`StbmVdNPgyu#8E zpHBREn4qd&76vEYVL*ZC)EOv3v+B!ELJmJia0xGDdY*VPgkpT*^l~8hZDZn2ng!-9j*)T6IOekyf zT?r25FF{6XJ~q6=jPy;x}9%K$FJU_itJ+l%~t~7WEMN^=$Vj zk5%7rgi!FV0SPS8m8D;m?`WZ%WE(XT&g2q9Q3uZ~k0+b?4lF#zXOKR z^m_V#!5_x^1lY8LjYzYq;)PI#P6KiRyldITJ(SA+^P->dAk;Jqj`okyuW)}C+-N{Y zdmM>mVW$u}ow>6^w2$Y--c5lrS~Q4TQrN?9!I4js9ot8G`Z4E`Bs(p*`ALBWHmHQv#2~?%$%_%|~ z1fv)O{xE}ythZXQoU#KJcV31FYg3yl#bT9=1*JaZvW!~Zr^TzRvJTs@3m+?bp#1Cl zWAe^i>U=d!v(rotnZR;1$R;n_NHRgR0JSzsz2)rktFY_0ZIy4X_UTPr+FUp6jG>?{J?HSHg%I;n~shzW}N%}RGm~>t0Bu4*EI&i3s z+}SWto(lb|71ub$UcdTLT;$@@j4hS3DHMu*TaLyiq<1nS`tVb{KLS6YGY7E07UCO!Au5;bZhs4@G7 z7f!ONf)lRm>Y2xVqWAvZtDLRnx}_+d0Y{DYo{Fy5TSB~fpFQmPC&%+`weWp*e!izU zpPkPQy&`asjE0uK&%8>!?mv#A>2t4NN(^K=!;U!FDJ5go+@c~}FIv=Ks2-AH>wSsa zd6>14quFk;bum~tL3d3@y`SGAcVZQ|lH+uJzY8eXPyXqg?c$br{j|5? z? z?(Xl|eSZJ<6S*dnnS1U`CRz4m&o*uGS~eSQ&q&ttZZ_qw-qdO9nU~}tD|U+AEA5+0 zH6J6qc(`1NLS@Qw-yyc}_4;jb9T-kJX*uC6&28~D6HC=!nLl4W67h!$3AAaa;wF5C zW62&0;%Bp|a^4~-#4d-Ww6g#WyhQxa>4vq@eTgZEn7kOVhI}ds_^*Y5doe=D zNlK5T+)+7+W5b?UJhQ1JV7}V3lP|ltqq-2srlnj->`UL7M0ulCRfud9LpWpOP%!o{Q(*zw8l)K&xUZ&aa%=fe1MVx%odF^}@??l@-Qg;7snV>NpB95~KJ{$Ifl_=la z&;%*iVBe0zsGHiWO%B#2s>ezcaLCVjn7e(_rIz)nW4~b*ycvExKM|B_MO6s0&0or% z*to_)CV3)^ab^_2=}m4A^<+oQ3eS1-{kRwmmDRr$$9zqSOmehtDrSPqQf=#o$!#LS z>&X(r2k%i^!YzRFH4`V5-r84AG|NPGnc)X_c`7ENWWEY5=apIfZs$*V0X&O{eFf{8 zv`<&XL{%DZq$;MUh|{m=IZaYxj2*A4T95cKu8RwF%%Ha`3RH`H$HXm~mWX?GkUXZD zZPDVd$dU>5Qeicl&044+bpd^8jFVUMv0i~Z^5^elzGq{(R&*43bQ~VGXVWCoU$2Yu zYSi}m^4KE~iHs_EA7ROKr3F)&68x)3q&vqC+0V-1;8iqU5bB2pwU$2SSPr4t#iIn~ zB%%n1j?O#0BZCvUApMEzB3z$=61Y(={?TZtxZ#cYH&fjnim*E28h0W}pto)S`ro#} z7FKM9eNGPX9D4c*M*%Xpm$45t@%JJYolL(cY^AKVRUC8trDj0b+tr zdxF$+RQd=kZXq~SZURK6^25d2MD=tl+Rf5z8ghPiS)ZjYDt~7&WL4SC4NxOZ|4c3% zqD-NjGX4G4g2TStIdGrO>CSVptwu%32i62NT1DtjxiO@Csmyy`J4DCK>bzv< zi$^V~Yq4OilmG43pwaq+WEW|g>8euNd}a`eX2Up)r_=thHUX7UJJ`^nOn|ti;F;V( zD^3HrgZNfPDIQNLZ^DvtmFSXPPUg=36*4y2(f#pexSnIo4!E4g6DKLz2rX|S)~{)8 zVO^Nd=+7`(8)*#dJKIg7(1m0bW=YZREslO}=26*~mxK}u)p4KcL~$_t^|xk`WT0nNi=D*)@JA@DvKdPd78HFIG+FSkQ_&;Vhn6WHlS4 z=z%;e(bY-f(B!JGh!vqh-s?;w3?!)>%t1dO+it3)| zko=V2=5wzs`%=0sPa6iamLsNbsz(v5Xf-=Hkj|qvlrwY4wt-QIsbWv<pfKds)E!>`oSA2ks6ddBjHJ4Z zp=;LPisO|>E2uM6l;<1>*m`e>d6R8zq zc*NNo*l5MkP#hTukCPN+WKltDcO}MOERW%W%l#Lj>T?(@4_meo}Y2eDp%nP zWzx`W&5yUlPJ-ma1p13+z4G78@l}U?a&l_??)RBAKbV9y+xZ8E=mQagUZ>~j(zO%^ z^^!5US9gCz34h2vYTU9*_f#0vyS_hLF_sOY{!N1!PD+D~>s0lZoZSaE?d|vXiMHxP zD{DKi&3!0#YUwSQ6OEpo+C-Q-hxhxeF}#7j4Q__i0h4*F!>|+EQ0!it#^=P4 zW+l=?C)|4GAp{k&xZvljX0@};5}c=XPw{Nbw2O{+D4kOEiP38`BqC%xk`X-NoBbN= zRiJXgZ zLLzY#d)?T!(cekq4T&+|gPnTcOnQ7Tz%}DUrmvS7-1K$E_js@uH|5WBWgx+(vijU8 zIl{;lvI!3*gMNV8aWX}B`NQ2axtSZ`ibuTzk!YV&2r9GpWw_{1KR3e7jTpBEGBjS{ z&;=dyOStZ?k0SJ`h9qOL!YjPe#!TN{mSr{H3}1m?=BD~ay+Q(0ZmrkH3)RQ(I)oRM ze*B?O@S&IARV(NVZUamv?!rgwO8&wxyT{8*713FRiP8MB${|EVyV5t}85JBCxjpH{ znMRk^Kjr$Weh#J_?o{A2gc9NGO4mDvT72Fd2;^2`c%+R*6QmtUF~8ZfM7jU&xi|ZF z4##~u=NyD+ds4NkeH-Ly&3Sdj*8Tz^p6JXg_h`fu%@9S0Gr&L#6a-?es{62IGW zI(2iy4dMmjFlk)0a_%2hqQ-d$GCjr3N0tIHW=s^Dl%3tPC}e9j)jjc^9E-Qs&bc*# zSyqdTxjG`#M~L=w7mo?&x_y8Dy<(4fTpOcS(feya9o zd_}!&634l+*$^iaUGE~*U}1DOGQTt~u8%iE-zANAZup?KGRE5rwaZ=8Wm0?UOb$wo zD6_&-`0>3Y#L@7~O=W0f2T`q4B;)stdl?*h_8}QsDyU8dQn;mDLL!-@Jhk#Y3rN6UHo z4o87x4o@!g)ajp@g1u7rx)KE&==Y}Od)L|_ozW~DV{(*o3EfUSe; zQ4em8Z>mFL+?SGp%fN=O0+YZMc;lVQ8k0)}L+*sppnK>S?ozYaz!J`)h#Zyx4GWvo zY0JIBE}2o>odOQvg&p%036{{+N-bY zMM;TVT8R*5uBCn?(0rb$=CzY@vm?9(XK<5XGDrIb{&&c%YNq-X4Q~ONimsdS$t%UC zAh=IeHLC>NCw1Z+3)tYRP0X+aZo^i-t<8RXVEmZjaZlmi0XNLEC%ee9|3FKBgc(Ik z2x?fgZYFsFIG$JfCK^@VIX}RSKmPOGDC>RDql{~K%Tr^pJuW^P@N3=t3g^Ob8t#xK9nGY3{5PW#$XY_Lpuy8IPYDb#OLxk z+Z*#ZEu&YQ(8egm`97b-X}&YnXm?!iC2Kb(rvdof4z!Rn7c(oy)O&=^(j*Rd_U#t= zc1G#a=)Z`PDWQ+>7N?q9lGT1kt%^iHOQ}2-y-XoDlNdbYt?#}-hv?>B-+XLE!V-F> z?HRjkc)v(@A{kxx4pFQ1I>CVZ>h2O1p@Mv0VX!T~@?)&D8NZb> zWV7bsNzolWh{4UCT9)w4a9^Rzo?Kj ziMBXOF&hN~uA<5?!#X)XZ)G+!JGHud9<%kDYFdr(#GUbeQZyg!oc-77oy5R_&nUky z)fqgabnBthJndQ1T3SC8BST$mi?C?=?Y|nn8BtI+DID;P1z}J{YQNlNbwR(^+%Z_^%`E%;$#0c1{BR z$rcl)9);Qh;h@H(Aa0__2)k)rh^I%l`ZjK_p)Hect2ezpBGid$=YBY-QSOu&iUjn% z)4;$h?e#GZ-q=M3eo0a1@tu@s(Oq@h$XXNUOK%#5VmYd{CsO7;jEEE+}5V5w|!^? zUla_NAX6^KwV^hR974#im+F_#iRibVc_gXZh?lXJ-Y;Pun)zl^3uE+C$3&V>=y5L= z(jq#s=gP)MvwRG%1jhfYA)bcIYpPju)4iZ~cJNW&6H`N!XurdyOW=?nNs2MO&iV3o zqR1n-7(+2o(MFMZ98Nu0TWwPhUq&q`h4`qdy03yL4|bU8$&kSLq3f#E6!;8suNJ&I z@%F%-T7^){H>baadYY9DsMG=~Pm5=bW`N2rprW|_f&r+oa?R*qP1 zu;}Rqsm!Pa6ZjZevjL74agM4ChouQ*0Tod|aW~JD7O1@ztu+#};mS9gf1w?3sK92&!IleSkL?GVfbSq%WFC&gBn$52h|$TrO0i*TBq-Lk;{M^Fi^5( zj4d(XPOs4*XK*LQx3fknwZMPx@1*m^+6R+W43JR8{PaE~DoVCx(T?k1%z z5M)9A%Y_~V*mG}tU+qT~5?9DftItS)g(+Y7B5uJ9I#B1onF0-cuz6n+99=L+Yq&mZ zWFS8yi9a%>K&S>O=(57}FeKqS#pXVH!GatLT~V&K!GerJ?p?`}g4CN@__Tz;K|DDR zdM$h$teY(>gZb8rh%Z^)*b7uXiK($@SM+S%YyM3 zt5+ZZ6t4W^S1O4%45d%zUlS&_fOHwRoEH%2|KoW?AsU*! zB*c(jQNL`Hl)(sAu2Pw!h(JQD&A$j+1G*o@g;FH5fG_1h##d*g9e~t?eYW{`nv(c@ z4-W}D*z7)r1-9Nc>`;pZ249kfJC6=A2^u@!W0!k>fdz$X+m;4XVZ{pg{(xmWMA0G$ z(#$r>t!PT~3;~6JIo6sK4ECI_q0`GB6u1xasCzosB;x~Cz`Z^RTs9X{4b##~ zKa~jy=HT25Whog9JtAw|y&u38?W;mq(Znf4{0LCQbv+TdEhAL%4=UAZMLrU&C?+*z z>tYH}o_z~*>)m~mB61B}%|T-0jcAn$1x4OPAsV&p9R(&~J8EAE>}a2`qn!ghyRwn# z0j(kd;BHsj++lDxwTkHfhSxXoUcwG(qHMe$Va8+_1|vc$_t5z-nz|M~U{wPl&Invo zjVFMas^TuC1F{?>^!V4UuBG__%uoe}&R-yKC{+1&^A-xg%a?N5&+Z}1&0%2I--<_W zz}?810_cq%1mHQp9XYl5Pq=Q6d#Wtzs66;iOK8ZxWdw{;?Qo6ACHMoF!V;TDIgnAS z%leb>|3xlTozGsOO$D9-u@n#f4bTj9wLu+RUV)M@TcZ-KiI)wW687Thc2+px#FdU} zjr`9*z1S@a>Yd^jF#d<#K9@bfz+oDfl)IJ#HdyE;+XSps(nt6&45iO5pYGt`!D8)5 z^u7}vv1?GYMRupK|DkQj3h&Np=D7M!GGq1xVQBbe}Is;MAF+ zjwAd(4x}>>=K&7J>2>^n{@ar`F@ zc+L0@M94D~fW*{=od!|I-ik{oz5v@!eNKADT9A&QUHab&RVOjH${q!I)qr=Pl9S{( zpsWCux&kDxC@5=j0ps|b&}`Sd`QWo`1MP-1BZSH=f#Fv38El%7Tl*p`86 ztgn{g0A8uih^IE-%|h#7FM`gu0Qi zbWmr3-@p4e;M){A71M>*50swSYV*##uqruh|DZl)xpT&Y{2&V)ZM%bEUq6M=fgD13 z^@t6F@rSK?9=B-MfDB5`ltzJk=fB7L*X(4a4R+Y{7qwMP9?Vff>*q>oP^e5Jzxcm^ z8fwX%5U1b4l|jf$8?zOL7*GT@aR;5iEk;nX0jc8bLUMWuT4T|^p;)r>U^38$P}sqz ztLP5<+6{myh;d&Id=T6A@35k&_SsHFIl&{wYUef@aKRPjRl>e|2dBTpi1gG{gA4Vf0ox%0QO4!x05#fAv_keg!Rc;HBe8%wPh)og%T{ zv4Ibw*PQKL7{HFx*7d##{UFo>;=jTlsc&Hyl?|SWrpEKy|FP$5_idmLJYGt@WmA>{ zj1K)2Q^LM*?QbgdUlZAMeNJ%k&UDYj83fnlPw*e`cfdPLSb&?I95V^9WVm10i}puB zn;XWw*E_rSz)6O=$v2#Hbq>74chTv+Nr3CkS9?=1JRp&BjXvLA6!9EA`;Twh$FOxl<8d z^x$KIFH6H7DX3UhaJ?H?ia|yM+A(KD zfFF*u14WQHkLQT@AHQ%%od{qCyR7D={;#wK@DjlPH;cYUUsn3G!7F7i%BffTzujcw zRQZ$E*?(uPFPn-cAmYv0O<6O5Q^7gZvnV0C8{KcbCSStx4_nrbWS~+p*zrnbT?z?a z_-~jEj9uU$++R9ZL5CReuA~)*|6_cNZAKitg`cY{eQZ2H0`I|-tWtK%j3}T(<pntt@bstCre;#PZhY$Z3x=mVQ7I^;` z+EaLZ`Ogtf7UX~4ZxFthpbUU!iZJC&W2Hb4%=>Az-~X?1jy4S`AWw3CwNC69N`Mua ztugkz()r&(;`ZJgoaIYd(nA28cSsBMf5ctht>cH$vFBLs;CXJv-?sT9cva`-fhfDLorxKZ&MGqRO z71S8K{QXPxD~)v%vg5wIx1HK3x6+ALyITXE3-qhg6dR?JzW50$6)~w1J{o2W$rNF8 zIQZ|9J3Im0?6I#779lE#>8Igo9K=ZP4a_DzA$^EV1AdV*d|}OpR;Cmvqs&l|fJMLW zkX(!!eN=+7GG|&LVu;=jq>%TE7XrNqM!JlspemcVSQrb_v_jlR@LRdcn$PWMp)@vy z?ZH-#KwhS4x9Qchbvf~sZ)dHok;DUzeE`o)5$n7{#+cBTMcW6FJ&J%vCHYMgFH_)u zm{?(aXg(p|jTk%s_QF}eQdRJ@D?UA)wRc~;=aX=6#lE79SH*26me*>M;3GOOBvAAm zCt`{PXvcbe(`{uRn$#1sT*Mn-H0e998g6M99A`9n@5_J|Z$r|WjsfA^;rSrq%uNST zHlMvS9WtvPw1{l7a4WVBgHl@S*+aN+vBl%xL7WdScr0ibJmAR;2gYdPhH)yA7u!CGc8vNgyaO<1qCef zANMJ`V`Fh8zvK&+6$DGXgK^lujEcBsGTYEI7$(A%MB0LbL34ST!FI>y)W0d|K zECB;kn|~aUa?NyV%XHQMj3te<1qoAIwQ0`4FYV5iOu@++IN9Uc{fC~YzLbbR9LwvS zFbTZ$uFulX*R==h)nUPnS%<+8HrY9`=8!dQLGSW=aC;zPZJ zAwC5J*6HLQyFO*nA1r=|=6?U2+aOcdG1FsZn^D3`W4Y5HFz&yQHQ6WXBU&Yj^(yBh znl}03S?T!sRBToH%D)tab-1OYBqs;FCk={^DFXv%L`tWfA{?HIN0hWDAODq}pW5JFkyW^_=tCx@*>x6`650UlJ6PV-0` zrUz`rnJ87-ru>d2b5r~Lh@bR8!yVho8kF&mh1&_PCK_Fe1UBhgGL_EXkL_coucdq= zbPoh=KLG3u> zKy#kWkQ!FA6~ID2bpg3VzFW&FK-~jHNR*8z(B?bsT9KR zR7~u%_$(B1)}Q7-Gy>U0m%mZ*c_`Dl{&c4;T{kbDEuH24UdWN!jc`bPRjGhk0zaLa z+}X?x`QvjNO$dtbhEf`uP{qA=PB;XD?m{}mr6>UlGZYhFaykSYCf{rlpy82{tXURtiX5DJBcHdp8!YoPf*dG!&8 zJM4`-Tms?&7#9s&dyRV80NJS*o_v*ENovdq`%ekayRTzF5zi^U+>3} zsI^CA?~nG4ESV;tVKyGKv2MpbOt}wcnu_)MSA2F9JpIAgRigS6{#Anoo0VQr653Ih zLs|ooEMD?PelO2?O@F&FNCl_*Q=6;xKlG(8gwG^%e)A)d8X%RO9-i~R^Fe`iemE-T z>M(29_9MoC3VdzZyGk1_@<SHVX=5oC3?%N-_ zyrz$=^n!lE4hUPuT1ZR&|~8!qx)Vc?^4j=_Hc#ae&E-bV#weSe^c!U{nK;U5Bz z3yD?j@VOY^IuLMUaf6_U={`6$*TLH3?A@7i>nYB#Vjd^Ji9!)m{k2ZkC2Bw%(vK77 zY+Y@dVM2#cpDdo8Y{>Q!|CENVM0cAJ2Ubk|x_ScB;=R=*!7R*iA`T8)E#Aw`<{u+@ zunfH015%(;pBlt=U&0jHykEsCm$k--wLKX%IPCcQ+YzifB*4q>`PGL*FqJVn+RXTl ztNnIyo7d5QD3m~)e=BvEJ@UOJ;kHzC6~Ma?zzByHl64JU^VRM&IF^y}?uQot^)seV zAFE336TqfrT1_+kaZSWlLEj#=)B1O2ObEA_y~*jQNT)>Es=9qUckgy^L@1BegqzOD z3;H~NrbutiHG8#%{RV&(v1YT59-ow*hE2r2%12`?i2MUF7+xUT_~Vmvwro|e?XWV7 zR0%e`Z(GP1+jEOyMll*aL!?i+3OP8cHmz&3lmLPfK)C75ahwUxI)iqdHTyCLC;$9! zrq#Gg|Ao~W*6GZ<@CWd2uvvj*4aNlP<8bA6`Xdz@5bJ3 zt2_o6Y*?7g88rSHOv7TGjD4V1_ zQ&W9#<=8*aqdI~1@M?Q&q6O!hCKz^=VY{JsD&)+LMFl(5YVGAt21bz{`*Y ziu|O>#X(4K^`}{Yi#-F^#h~2^!CMB_XQEFIRdJ5MBo4vX zj^%+ij9-&T?9KQ`#2NAucGurC{{a>0nwtc!F8pg^e2h5ZPoaP%KEil?({+lg(#Ho6 z#UK*Pm5EoMAU}>o*LDh(*LXGXBR=$0O!B$XkruBK=I_|8-=g~2%a@6M_l|Z@$~_6% z^^pS4@GF$dOJcOxS}8(_f5queFb@!>QZ7Oq5}}ZM7Ub%tA5+)-hc@-rF^IbBKs$|= zH31vVGG& zV+lAyPz;ck@p|eL(0$EOej1taI*VH6QAZSi>=h(e9vqn%ipn>+6cmzmgA$*F-(J0l z#$Jn9iTUu^d=eTdRG=7^7|~`ihn*1B3lmK!mATW#hL7B&BW9P40M{>gyPI=rH*1*9 zIi}a2&km2LpZX%&)n{{4Wo+Lsxc3PSi!SwF6sX~1dJv=FD&8h&_tbHQ6CV8at(SYg zhS;d#wI5zd(&rSoPpqRPO##HC9)7E?FsveGwXYAx0)l(2y3 zi2kCzWs<_WHtQy#_6in?L#>4!YA4;#qf2yYUL2bD3Kob%N!l~se|y&aMXr}BLN|r{ z7EcPBEZfM)fAN_n);Dvp@ge#(+J4N!5QR?Oit$P@LUB$^B7dn&V~t94^%%Y6pY~@$ zP2gQ7Xd*Z=yHQnnFe3DlSOuJ&VeunUVJqs1jOuET34sE&O|>hC={h|EVc3pZn0M_@ zxCw#YTI5Mq^lyi=OUT6%W=wRD4ot#U~|n$k*V-+kj07B1F%vMb-T6Tc8K zF`4`I@B3!yQYIunEllAEPMfCUU91yU$u%Y)fANKoO2gYXj6{8S(}D;^ z>({qS-IIzhgC$mgU~}2?7egRVy*?g)j^Gq5gzvjyiX3F=p6vGnpEumw6M|u}Q#Bp7>Pr?;O;P-RNgW z>8cz$>yupQmk#iXL`zr9eb@p%SsD%z+!!IfgI{Puj~H>y^b6FFiNTlp!)jfKt%j|F zHB=qXop8qZ4!);`lshr@eT}Z6p1#_jb!+`0E5i=_yqUiVQ+iiOh0$7B-_J!OG>D2T zN*Z%@HDj6-2#Kl(SD%+;+&3tFj++m!B^ew>Nkln^)pGp#d8+?8o@w-X){E_tbFKcA zn}1x+jr%C;vL(KXOMe-e^DFlE5ib8Zb6DHE-*mdwB4n6uFkj~x4JR+@FF%Exb~`%Z z^tPe(2x8QrI?{J#`kb0?rbVOWWYUsF%C7TsGV$~+ zC2}yDyVSDme(LqZvbWp8{5(Uf|5df=*oR)0u#XC9)zd9#IK&rZ@Uk37^=3{*3WW*g zWbB7k@^InCSLebaE~7pklMf3W z*)@;hl=Uy2N;dI#J#6v1)jk39A1aT>i^hGaOQj^apW0jX8Ef6Oo2Gngh()mr9Umi? z-_~xBkxg_LZ;cMbf;aj^FvYIcW&^z)vF8*WG;Dt@Eu1v+kjFU+Y-iza5Lxlnlw5DC z!A;8?T<_(TU=n#|;kK!Z6$~O(ckM_&;Amu!%siu6&VPUV*35}Xf6gEnSU5R-dfHpU zhm&_n)bSXd!XsUO@g#NV9`)Uvt;IGdk^5~&RYMg~^Gwb!tYl3qPz|SaiT26eVUaT` zK)O=FB7kOcvz>5_o$y2|v_NNnZtM_NiVj}%z=s&Q>E5}MjRT66jaRGg;^~clreR+0 z#Rg$X#Z!7XVTN*Uzyvv1ZDtO2Vp_6T1{QF!%J*&OV3~$l3-9Y+6vL4!W?Q%vH&WCx772#Y@_66+$BI?G60yNPZW ztc~@k3?4PQX{NK3=szJ^nyxtfA9@DxaKiO1%+K-$^cHAH0+<(?FDcy-f@DxxuqS29 zqoxg+j-#UgV9z#RKmy`&6c%iyXLqfF0r7W!%p`MX^!%b4AHx^v>X8nZwMdwZL;S5L z4V%8`(LZzln?}HtFq;#vcR&-S0Cj=9xPnj^tPEY$pA|9YOA6 z_`(_M%06@3zM!Sfg2sMrT|}}^nMzEPVaEM5H9gw*H=~FHTp6!rqT>1SUlg?uCO^$E zBz!P)wFAf%vQ}ohQcg)6 z6_Izw2o(Nc25l6i_TeYMH#C$>Ili_6noFHv8aa0-D2(1X=uY#>4 z5^NhnXZax01hHQy7iXD)rUPN@*S8aO>?lCvg+!u``l`$j5VMlRe!UW*JP{-LorNAl zp4x76{Ry2k=wT8MxNrKXcXB)L{5sq7V7}!zlTerXu5S*$%;&}^K>s;;(Y*2n)08Q^ z<8OrbZEM5<=gF=H6N|C^1vnaxo5;P)s z;%hio^3Q#nae_uizQNjG3NJy1e0jx(zp7s1mR*t*f3_(jb1EJTN$Bb@e9xyhz$czx9{pcaDp%HYPKDn38n9rL%gvp1N#&lfV>X7<6L(Xp_>z z#8qGizO9iND)EizIF`5iA)*hx7^v|Z52G|R^Ye2}`YIk9Ccp0f5r=A`A)3IQ2&nhg z(e&j#zwW(}#2)_!zIz8#6@K-_6g<}H(WY|G%BZFQk!}S z99o&j&|RE>R~MhEs9C|;Qb$Sx;c-fjsp^N%+xo;9ydMs7Z!E-^qj|VL93aik86)JO z4iLXgtu+cv!;%aT6Rf}Lz<*H00%W+28;GSQx(G6l+;1;xD&qoN2gS7o1ZA0Cwo_RI zp4K()w|^w0(h2HPXKRsU4{7GLjBkzu;xbuZOvSF-$0yLcGwX~-J*7>IFV>#gVlXo) zKa(8F^$vXj?JS#S%Z(z|W7o~0vv6{wLo=jKVSJ z-{ahcgxyx3=HG4HM->zGPl!o&?66Y&}p?m{O*6VQLZ5l^jcIC)_s|n>i&{ z(@F-sHpiJ$qLM_k0Tl6#Hv*y3WV1L$q4%aQ3*#O{S%tkc?(E}ObzGZ9iCo`a1sF`d zXuHQ?o2-peD_Sg%b>a>`#+Y>8oS3D5B0Kj@;>}sGWHI41u&f&Dk)G-`WDRZK8Txs< zK-=4YGBhD3I~uiz)QZYG@P+W~G+kYMKpH!EjApG{W#yMluI#lj8?{F;O$24J4p|(X zGEW5Q1@ZK{oND7QIpqqg63ZM$#D+F4yo&>K<4_wT#)rbU(@LtcWn@F(o-@{x!n~D> zwvI2R)TVi8!co>${_O{!r6MDez%dGW(x6Pw*5L2f(FW#g^{P57!JY+*nUa3?FJd1cdk&r^n{7e^O)`zgQXG*~}3vD2uZc)}YnrY-N-38uUWoQKcvz z(}He}#NAsiHOmw)_aV6jf2ViwLv0t2>d2oW>Rz*}%tqFOEpGMjOi%(Fuxayi+WOhYx($S`+7sUK~;Xpm-uz2^;U zjjCMISATH}q_M+1Bg`hg`C&%}D|=TAp8~}@ljsE1S9zuTJ2$nc2!3EmKIE`6O(k(= z0Xe`uu3Ful#b_@%aPY1Yp&q{`@z7~wmou7Ou}fg+bF~Yz;9;hL_rs^&yDv(3Vy(az zW8Kf+p3 z{5>w-Te$Y)Xusu)6+!~e42Nu#WX8JI?c1CO6opnr_LX>{SPZ2wxi2H6qTZ7HlZ5*ndGc8hN3Rp^FbrAf*ra%}RCYTUTO~XgsCtwcz!*l* zG|aCOmc|8C3#50xwehQj22?v!GglI~dSV3BR!Agk2Wxk9*QyjTJPtDIZm4gq2bCKp zRi2j}Xds@6ly$I_Iolf%Jb7LF05=}S%lPqJk14>n8ra(82*I}+hSsc{z`~1hWTcj8(g~wI()m?TlmBm1fw6&!fIDXwg zBgBlAOGx0ZMON#*c5-|m2$y8!z9IOA$gdG17~uAZt4j5FVIRodng2>N{0%t>%M0To zQ#$DO#q3E~99s3`Usj6)V`Eoxm9IrDkotW@QD(Te&a z;@m$_d)x&q~OeC%yitN20SR_!FMChj5!v zY!G^9%U!B#AUW`T|7fU1uN1l2V5I}n(>PJ zrQTt3GjYR$T;-7YAr6iu*od4^RVfq7!37$`QOy?vQh7P*H|3T==%A)+AxR*x(`m zb=U59f0s&FsUnhWnOQQ6TtUSCu|cTX!r^S3<9h5YJoUp4nq{=Y=iS(y&qve63lJq& zcLkXYVPu5kPtyq&vtOT{stom#ho1F?U{wsOTGsolpNg#91D4ks-6!3s-k8BZNZ_;X z-M4G*4E2NVGkg1=U*Q5GAEO!Qlq@#*7_YRs$Xf9jm&H4I=fnVlme_@b|@wfh`TJ_NPALiNw;1W z%N97ZQR!ILvPMe+bm%8T>(i~v9GOTn9m1#eZhlyrA4p%^tp3X<3| zzsjY}7LyYSEQB2Ai=m*0>)UOh@0L?>iJ%eExP)t)MM@`PRWKC_;n`;Su(ReVj(yZd zh8VPs%o~Q7+AblqPNZ{dP}K^*_QWEIZ<>Bvf^W$&A>>xcpe>C0Wq9J%b+8b&FI#*!^+Hq_Fg+8~%8N z*L9K%2~F74$ThZqm9a#ks%luCl_mkHP^06P-VlA^II(=sPjVmkKGP;)pmzST2fgG_ zgh!5yLkD<<`=(4cYtUALQLX4Q?>T&Xm=(=~@nnUC@LvAyQr>P6$T4H;CkyA5SiXYbY~>t##x zB=y|H9h4sH`kxHvs%l=zUn+CpEvaLyUJAr!tYWpf3F}1h{|R=4t9YHj)zg13RDQN9 zE(ke^-Cf%M@Let~5`N1kI}mlf=4VZf4SW&~s!a{cz9VHxnU!*s*Rci#9Yx~W)h&1L z1>2%4Qt)5@1dX`kC@Ex$wFr}Sm@`*dcJFk~ogt^*&DOO<{^*fsCB+f7vL2dJxuGDOtPG;>9{v&la$9W`Eb)7RiZ9@RY7+Q^Q??&qtYBECY2B z5D=&cnpmY7fIAv~ej63;rwC}l?9$_1UA7AREZ-rN;ozc%xyb@$DCXLvAkj36<__ov9a$3qN zTQ0hRJ6m%tM8|mKQ4s5&;-E=mufmj(M%o{Dw^x)B&L^vnu%|6P87?vHgGPoZ{D}On z<)QyR3@7BlkLttqLtHV1!z+`(d9SO#s({bnNYA~e#0&?e@lb?N36qStfmjSvP+3Lf9}h(_K6c-=wjm?13d4;Y+6D(nC1h;p)bAoI^0D66q)WYCWFfGhRjMa+BX~ z@T}fTp)O1J`PXpMAJQbc(Vm80JKi{{t@mTt*Y_0Vnv4gpNbm}AC~y#>s<3dlWoU== znlfFZBd6P95rU*T@K9xyq*$;w&#%_GM;V9op1GVZ-xb0ZD7oRSSBW+^+e<`9mdCU- z;L)4$`-ItS7|g6bjuAC#khp5T=@+j(55$k`p>c_{XVy zbBDDm+|zd$j|*GT zpIXy;(R@E%xvO46Na}?d0j*lv?j#p0;JuqZ;LTegN^_K(XHf1~+0=3pqm5qnX}Q+A zs{3+=s-Gnl^~^}(G*SYqKsEV|p!?`2Z@8Sla3EaIx%tMYt7^m7o8?ew+=W2z`dtiW2jdZTxdx?h1sUyfRsBftLqTXMFys zdY(3=bb~=F(g-3*mxzFXq;$7*cT0CjHzF-v(jnd5@X(#o{ho9D{lC25_S&;Eb5HEd z=)>;Lf4x>A37)vIuHFOA-OeFLv6@!J^I1SAZU;Kc`@jyXFt^xR0zCR!@S2Z5LD}74 z+I+p2Xq{_^1!|~d8sQ!8Tv1(XZ4LP<#p;rwbGZ%5)=A*t-O4vd7N{9K+PYr|0uSh_ z*Q;4qq84jGz+dOq0Wevg2(lD6`LIf1v#;IZ6UtGnzR0G-Ey z4WC0FC5XhO$>`wk_t(Zn3aBQN`{fr`UmEMzSuzCl$tyiXc%qWMxLHEO=uHU)*Q@ftHt+NW!e2Pb2|XdQpPjWM-%KE7@ec^GndMl|5wH~veV<8e z28&N=eWtFfmpf#Ga-tQ0ukdqsbUvQ)u}mBJ*x5sGuX|voeE{5S<_DcaghW1JwD6VkGopbinJftbc!F~tua>A8*skNa@V=_qIM%hJg=L0?!_BVJ@r0EA zrC51Db=Tb0Hdg$4E09@2ma4x=*YSXVZ87TXkRu07rJJZ5ZEAJ`Q)jL24(A4^(s^s% zVGaQ>Tm43n9L&s0p1j9RywiIlGdtrY>sw@-Bh=D^`{ryHj}uk0ZBko#S0%2Q>5ec4y_>I8*%>P6<6ne=)A*FZPcPVUXP6cQPQ|U64t7r$cs-;4GMT37dU<+-o}3j zX5?`{QX;L5deYBV{PYV9iDT3Qla<_SCuvFKy3{qFN`?=AUaap*@z3wiDRUNF9HaAk z5@mbozSMjThBTd@SA?P$@Bbbkd?^7aA5GQGHtr|cny?3IQ zMCk7mJD(Paej_%XDwp2pzxm1Yiqqq`hfL9DCt^t?>0+SDXYVgT65CJ>f{24N+Gl## z=Kh2I$)7xwzoVKos$akUq6P3>-{t;HqbTi6RpjcKOjJMvXz}aU$(~1Y^g-Z~2+?TG zt%UAbjFQ~;I;y730?xeH;pJ8cUSTcU1-;eqDSi1t&xpu^LV;sTt;P;FaksI{D4$Q{?=}P2e(dySn)n8p4;-u&(O*Kxum6 zv?gm4hsWtLFQm0p#EKf+J{Zhu`xHlv=5W>3H4e32Um!k_t05#8rx}&xnAQ>#3LG+a zrmioskeV96SwRol(ynoi8gpm$z0|}g8hC4VyTIh#?5)Eb0jCDfhrmPe9t={M zztXf#WabX>AA1BcFOvxN4IEMZ#s&ac?WD}J3zO@1X8_eq7=pUW2003>p||m-ykEfI z&tHXI($poO2O}r1*^)@cF_6J^Wp5;S{JzCpfjz^$Hst>I@zct3m*|hwFvU!nr2cPq zkMmWNopJ;}9XBbYCyX!38~<2Dd}Fv^zuOnraIelE^_81$?2X4oN@zQ-pQGJhD7 zPFf~BO~vTR8|5whjpvEy4a|JlKB@dVX(}2`Q(2ojrIR%MtV1)kbinigFx|s6#4vpF z6MjAM$wd=4pR}q>+PYLN!j`l8duPRtCtrq7etZ9AR9+qNPop8F9f}_0_c-M}$ly&@ znhgE9c?`EcvskuF;gWWm!E5b7^Lx52F-A^crrZD2qQL_%5{ z>n8v6TrlOmARCoMlM$RXJUrLc?{lKiTz{SP?}%a-KGA5;q%l1yehl4zW=SKu7wq+< zy(jOK|3ltA={HcQ$NTQq``6zzb>!82oj0`8z+Wj$TcUfdMYy6#ua;$5Or2VaFX!)# zzRT_)JnZK2-b#0i4=W>1y)OmEQTsbhz_IcBg!&VHV)iFCsr}ktsPCERl#I?yJVt7! z=GAudl1KK%u|?EnxJ*9esb{nLKF@xiB%JH9tNmhjbj|GZ_4u)-!Twb7q$)_Jb@{&zsEeai45tP~@Jbp8*$$KbPTOJ)Cemu$WSAAI36)p09`D6OlfSO_s1-mn1y*%jlp5ob)sSmZAdzEF~4U z%?IC@9fNT-p$~&MwfQ^oAsLE&hg5HWbl4Y*ZFb=1v3DzsTBU~n<@>?K&-h|zsh&by ztmZMUc=e+sf(c#RAy_ z;OA4`*8+>TSA|g@-^RPV%_CRS5)%1b$16=cnjn%nxaE^%hx}Y9n#o?=2 zbxkIjeA^Y;V_Nd*DY1|YNw4DT3Uf3TijRxYu{*OlRtP_z#ZfYSH%CLI_*lzyg*LMB z=z|vF^I+U{fAZV`bR4w&i`BA9XLzBJytbOGO6T{|FA@ziOUqLg68oaQWMLVoeieQ)0G}w6uTB~|I?SAZ}4yIhL;-uZpEh;&nkP!nZ#rUJ$>l0SIz3diJK-DT&|1L zsj?K3H!<1}K0~NCG{A^|8Z8vK$XZduhve`6QVu1HRl3sDzuiiWjm~%ZMb_Fm&ND;q z4`w7f3be5ar=Kasb!nWL(I)MEXn#1Xq@SEy?{l31nxM$3h5b^RAG zB33JB(@sprV1D!?A)72GXk&d1m`%Y9p0ht)v4aa|+>kqr6?o`ca7JbuF*8pYe*Z{T zR(9_Zp=;}+>(h^WopA67FM>YN>LL@@H{gfSpwGmRrz2R*grIb#diG0(y2iq z+x*qlt`+I$L7lVe*A5wEf}Yf)*1tzWiVQ>_lqL1Pe`+T5^FPibpLs;)-fnO*3lsWf?iKW9orTDL$D6=A>iRUU;HE1?WbVNxt!(7xL%t>Xo#sIH z@sF~VHDTYU$Foc{DN60B1A5UrB-#W^dl z?KsWUZ;e^Ya3L0+M(hXthCD6pn&$#>KJU@0CdYX)&TX%5RwH^j8iwc~*K(d9insd)$Qc;Yc&CN>&&V&9kMyoB=!!weLRplqj zGgYJPw%*SjmVvRJnk{opBL}+$HI3Am)T@V!QiX+&Hwmsj&GPYVLveNU^J2@tr5<$& zIxMdgIiz$yb5K>4ayb3FoRreNW#H?*x0W%6$r*(^HPiHaz<@JqWJq0)g6E5iqk)t@ zhTcrv_s`6;L376=?Ii;}W#+Z1wV!WiNj$6RYJ7Tww379Lr`8?nc#chNw|lk*Q@R7S zl0(o6-MOMhw2~)xCEWzC53^zu552GH`49}wq?t08UVCoZep_%9lk|>FTt&VkOjwq^ zOAHEY^HkLQwq77u%O)&xH(6Ms_WR3Sm)?a;NeR!*R=9BgPnl0y2IA|Ub9p%K1*IcJ zd>S{@15Maz+p}z{j-iJ2$U}D3&bb&izm+!MH6Nr82%-WH)#Z;XT&js-8)>OJs&95`k4 z9dOa7_#Ai@2irK_YT6Ey;w%`)?~rnBVFg%S^6% zBj`p>=Hue9RR=C4Ul z8X7vRTmvk4yNVXzTZ;0zU{Pu{h+g%2?is{2hRDaJ3%pdJY9)wXg?xK@&n=3u;11UE zd*9Dx17^2;EB*KU6;p`%^Ocqxz)sY32%2uStiW07BvAmcbva5)ouK~8N6MK$p!$%If zbbs$iv7>YHPelb3J~U}#0^iq`Q+PP60cue)Cad1DZ-BJ!KcwPVc1z$?@kU0h02jEv z!sT6=K-VU>Atw!fVIF*WAY9cf+)uPmcj4(SuM$}F^(W=~PB3p5y}3DL3+BFLDg5bw z+o<{EnRv6_Kxgklq{L5p+Slzc(dS&ZPt$cyFp7~D#Z?7#6TO6GP*3mgk7@dlem#m6 z?1C5VE0t^!kwJa+$eyeFExg)$Dw*Fydyg3LOYy%T=(#ny18-@+T0%W-zL|XjJfJPD zEj<)}0=^`UL}ox24Ll&;O7muX&Jz`QZ8=>~t)8Y4`sUGaV*Qyd+W#=74ZH}= z@7+ix@U&yvZwVh|&+`%vT;I|o5mW)b`Hhq`u3*p#ew(h|OCr&GS`II*y_{2112KDC zJhSfw37VWS$AJW8@1~tWgx4glME>BnNXzSJk!SUhubmK@q{9zdKxSK{56euDptOU+ zWZ=E+Bh&_`!)Rhij}X@g7Vt50goAQFO@NTp2|+WL#=a9IOwiiW*MmU&8+b56Qsx^?B8Wwx2E!_(N3X)Y93Cip zNn5dlA9(W}o)>;*uL*jp9f^p|y1^S#r1sQH5#mKz#Fp9oj+od52#&gP?C>$Hzzz=$S5CCV-dJQAoYShCTRJ zIKDFPl@K|EP}*n}20r?Z`sKD#mjYZb2!DYl0VyfRKhX!s;qsvw?v$MZU!s>@(y3j% z`=?Q*wKHUWnQ}v20!YwwutFgO+KtXa3$S!Ak|)h%KP(i4qdiyA4nCI7?=8VPM*_tH z?TOPj@TKl_uPUuJXXu-){p=8+##=z5j>tADq#8DC?J^i-VMW7a`v_V_=Mg&#zAum3 zK2)LzzU?l(B$znV{Kg@ZaS7FpkJXqFQe@w)@dR3?o^YhY22C}d?ejno_ZnS$u%Td3 zJ7gdS)O~t{G@BZC2L(^wgyWzpB*`&^-M$U9*wMm`12?2)xK_XdWC4fGW>(;_dK*8_ zCrOr$q#&~l)iXeZ$rj+v3kVHp65F zoZ*M3Dm`Dcsn$~lkHn*{r6l?6LpRgNAke58T2ldMuwz;pkB6qEzytsARKvv@tks8z zb{b*`X)S|#=(EwljVt2XGLoM?-+~MXTph3Qo_oH?KuTg$ZE@&(4jc=YLHIOeRQyjx zdv^s>XrZq$(N!c!yQDNpX!4IXH9GZw!V?`PJ%l>7P-x);2Ipq}6OQAW!8xcA!hR&3 z#rm`&bq%IwpPeG(iVqM@+~-v!w=R=E=bM^9)To%I5NaC;Y`TPS4!EA*w*S`jnzi&mcwO z^|W9>r0lieqb!bx7M+0CUC1DKzq4JPuR=)|R1dGRg#2*s;>I8i!8a;N8G+ETE{1;r z6#iBh<$l_Fl}*MEzpYNEHfIQ&-p*49A}yzYJRggmo!bpuRzw9{Kc-g=o&6U?#duwt zxc`b=ywqv^%m2{N?s8M8D0 zH(=rT%E4EG>VQvB9N?5!YpXzByEM!1os5GAK(M6MVr_mB z99%;E{bY6*bSZG=a`1=fKPBJ(oKyc74o`{)CeNOMs=&kIj-??krHvM@)gsPz8npPg zslUHB`$BL7eo)c?QTI(XHn3(;3N*5}QuJIdL?P2`XkJ|nJVgP7qW){cuJZ)PI;NQC z_e9gvxzB;H%rnYl5VcN20xChn^Hg$>&1{j}>M{ldf^C*%jSC81e>U@mC`j4_2Zx@k z?<2_10`3lF?|PxAqfpx$HH8$6Oi5T31Jy#=nhxZGrY`55U(JnKZ`VblJ0Vy^D3;KG z))j<#ox>otORXd94Izo48s#W}b&gO^FS;EPKqD*lni$N2apLlXs0?#6Kkx{ZvznPy z9F*|PZbH4<=-)<^>Ftq{fRr!6|5FX@>u=ZXuQZI$;T{Ejycbe@gIbx!I=q)UEr|db z;}xlDTH9NAAlNdHun22S&yC0b`_jOfdKzr-Z`B@&Ry9Q*66E&j@}1flL;_B`7pP7M zDtBE8?>d);ft2)npDPy&+Bj#8WCu`uE!JK~KoyhS-y9!+yV7 z@e^vc)Zaif24O$xz=4**!i&{8=+s7n26lSn*!x&mUE_NZUegaGRK0h5}W(4>G)?TpVmKtb0dB>#4* zDoDd4r`q;kh42vU%`?IQEFYUKprBOF#SuWIc*&tb1s`JdIfOcaPx<=?aL)@Wd7h#} zvoxvF7El#Suy*;v1Wv)vOht7;h@xHG-kHLNK!t{#BqLRF1+X-_ae2MHZnS0=gTVY2 zHp#sv#{n&9-Wn*Zf< zoGDq0bKbY0kKqqDLjFg;BrGRiaW6zt2Q*lESd|Y@|0bjR=lb2VdKtqbBqsH`RtM-R zF&Ts!*O)!X-ZBcC?GFF0603-GEa!lHe1bX21)lS}oo}0*0B_Qw^={Ltp@F)F{-(>ZJ$N)okB*%|WuT9#{YEpqWsWu%WB~%; zv%N^sg6zY{WaC=|-Mj+i=4}H)C=9Omc}@N^k9P%&RzF#Wb|QG8*Jz#&;S5&Wzbd{l zncD-Us#5#3Qy(bN6_RUcfq9J9+meND`ClI6v@Qlx0OJW*rc_*O6(gBjMo&L5gUO1J;oE$N+JdPc<78>WcmN1gsM!8?ef&?Yxn;hix zmk>{jIj4UH7DWB2-uF}#Ql!Udv;3~*{gj_sEB=w0d&oX?iC_lOWV=QOSb@Foy6B}L zOoHo-bJK(P;C(h!Z(K&}3LyQR%5ua1r+RkR=%T|7;e8I*a zU{EzfS+{Kl`D&V38FK-q}Y3M~CH2%>-c4P0r!yN|>vtfn2oTAG0r9^XCqkABB(#h<|g!rYSW<6l$* zUIRJKb=Eft``37ETf$~MGT%cR-MZ`Q=|BJTTS`@H(Lv_3qF2i0gt)3s(nm`|nC*D( zs30|*Ec5%$(J?`uG&!ndSP(cTvZL^QC2`0MDfTd4ptJ%v{=rfzQXs_AQun&9)IihJ zi;*T+oeFH#`H#SzJ*vcE!M!3-c;qqh{M)}ktu;~qiv{|D=eY8-93(S_&cz>S@Bn&* z2V&0kfq%JUyujBJZ%`h08DDJ6(d&N!rKu#fBM9*!81$#WEL`=4?JN~VY(=rY}rH4-A-e+J`m%e$`gW({+V-N&7h&u|JC zv?=U-C6tXzA`SWYvUFO6ct?Ujzt24P9W8~;bDhY#Dp6ixxH9?ts%x1aufEHhh5v|r z=J84=dOen=S%ji&r0`uOiQ?fXSGl3p*YWpdCRBbp%HAy^C+RM7&(>f=Mwj15jpknP zi4TU-dYgoa6vyV=(TV@+AnWa_a8b%^uj8^@L8(+$d_AhS-V>PpWh-0NIZ~=bu_xQ) zb@ua*_Pyq?{J&RCF;2-j+{U=J#L;9Wf+7t98qx(ywCi$zB4oeX$7Fa<-NX&8Gh`u= za&>n&T{N2h6fw#xXX_rek+U@L|E-d>j%FU2bj^$A$C2}8Fn{m(nMLO1k-yVJs8NIw zrXR;`x%*33USiq?x*AK%pRM1Iel-Ru6#vqVK+b6_qa0#11P8Koq8p`Mdc$e$?Rr`n ziny8bg+E(u6f)F_{!Zy>eGv=1|1LVQI1c>O#9%;TV5+w+92MVA|6PbGERHVLcq*{H zSGFkXp-EG4=|`|uk}j518;82&L)FzltI7#x&6Y}NKS>HE%nG=Zf~5PsEyg|)zWRiV zpPKbB3C4U}hTtRQfz}qkX|mp)_qK1Wqz#=FgvL{lZoko{4Ac|-*jQRNoLIa@khMLs z(s)WUAUaOMJO6i}u8V{X+4lYEwHrg4&xZDri$giz_RkEiy<=#(ZI+b6++2? zKEvDJbm(Y-KKPqQdt~(v`KEA)cUXl=PzgX`+c75$@Andq4WMR+5~4DyNvJ zfD#cSqQ*PQW!tk4$vi}1&@#2M);p}!~F;}E8NV*Wfk z$@gf}`xeEdIG$mBpBc5Y<6g<+pvlRxkt`Stwong7yN(|!8jUZ6vh5w+lx%x&P!m(> z1ysIi7{a}f8C#~pS++Dg$}{I~cqi=iu#AkBjAe<4d*8j9s$%SUfnh%0b`(_@Zjo`f z@Eud&+|d`YYx@msjrlX@=9Xk<1tNZuba25Zr_U2 z`ms-aO3G!7A18l_sgNhM_Rz4OQ`B**^2o^JO=Ut=-J~o0z=!w@Wwi*}tGJElk}|(+ z12!*(PQ<8b6}P)@Q0GtS;em&x54i?D1AlRktVZ741`h6+F?YMbfdo%_w_j27+Xf_+ z-fMe(Nh7nn*f+bh3O458pwO(?>|OP}ctX|E4>bGIGo`j)yVyIt(BQ)do3OS?E!2vG zd6hd9aFKoQW=!UmOYG70-rvRC{28M*&$C|U-Af;pety%3LM0EC3nMM|CmJQUP(l42#m%M9}C8;S0dIa9!E4HZ#0(Y4#a`wbG+4P^|xz{*4>*+IUNmWSHNnAa0TOCN82{yfN85D`)K@JzIAB&3~}!W+Ja z%49_I^d$>+1RF=0kr3S&5mCgl*3O5UtTU%jFWeMtqt90t?x4q3#Suc=S8RJYMzgE9qKf7ScfkZ&H5edXb1SGbnXuTS@r?) z??%qdMrt_B`rl(WF@Bt?c#~k?ag%sw=zHbL$i88AWl=2%PMW$kHzp*fUAcrI!Dc@F0sHCfL-k|ds2 zLw2O(Y_lD*j&MpH)l4^Aif7I_$#|^#k3<*Xhw18PuC2#O;WRit;#X8)JrtJNaHb;PI#ewW^}#!xKCk}vw6g*lREHV}txI=yo2x`VEHuGX3C$;v?3NJ*Hlxk=r1y!Wl@-e|#%&$OXK9kgoHgeEqnYfyvZ(W$f^CC(gs?%L6jDWAjg44@(}v z$|ln7OKIC83kiH!n%(Ww8}^&Ac1H@*hXe1KGvgJW=C;j|LB(LHL!kSDEy!ybIQQZ=#S+LamvsA3_6 zZ5abBKhLbg>(;#|3nXm_Uxs`Y{+d3ZKI7H>g9ul3yRlv(kg+_fmWNH8rM_e4$48Er zl{ZOoZsPj5;V9<$PR4jqAGuyuKJtv&mC}z2?_X$jjh-2y>wB_qnB-)UiBI;r}bDE zQB=cY4aS50*_2bHhT6Q=H!3n8xiYTI_F5YSFhhe>rwmqCMt}M8on&Qrl$eh_*|Po* z;s)Gz*3+{Q&SP%L#K`p7Y-hB!Ih`>}3 zo_>fe-=J`02<`ik5CyF_xNFqnGP;5b7m`S1l}PNz+ejh-Ngtox=ZG^)xW>!(JluHE zPjr7FK>Ra}JVZlom1q+pbm}($xV>Wg*V=Yeg^{onj}5Kh`4sw^#3wnKhZz{HcPCG% zqzDEZ4PWHQi?fl;;Bnj3b=4w9nWCw_(A6{hammB=iA(&mD*g5GtFOmq3`s;4!=|3Z zH~9w3qhG6%R(K~loJN^9g(sNATc&jmp5rI||9`ijd^7LO>M=ysr9d$mMEzS6lf~Y7&Kkj+u^8|;crNc)x+A7*RLqC3*%_!FrGu?_Q$f%-h zYiS`38~Pj#cRi@G_)#?qbDrM0H{~ghs-XvH5|-??MlXtRo;o8l7xhOkDsrAqIyx8` zeX^t>xkgM5;rlaf8)>9=JPfPdtjE ziX7yw2eNG=+r*Yc5X$}?B2H||jmS&9(ZBdsqDK_D4JlMxx9)byXxLr-~g zJaDRD}fvp9xl0QBF1AV*KW5VnJwc9t1 z?Fj{0O7z+nK`=d?Bi-QihOED33$X0|rG=QE9VAgfpyc$<{GdVBp-CY$VBiYYf;ioX zPKvcp+opIopGv2sc!4}4xU2KnAhR%bB{x83nR<-q1JO13-7vL*2-5i)G#={>^&m-T zY3l?9K&$%qxby&=U$4EnYIH%{579SSL8~hkheiQ(@Hy!M)^ZG?d^D^_HQ%TIutQ`6 zSlN(zE<;9#@{pcA>{C0C<|j$>SdjC-r#4Ojeo@ihuJ2hrhnQoDP8mYq6fo1qp~0M! z6Y&4FfLsG{46<*aDq6`YdMur3s|XSgIR}4GK&$ZYK0DL|5z8@EYXr`E{IC_fdu8+hV{abay1?CJ?vfOoZ#DgE}T>3 zChiLRMrb9ujR;SCs3#VZyZ35!bNA(@;P6!9p<@C#)hN--zA8AAYd5vq+ z4n4&c!FzkN-g4#xKnu(W&yzy5lgP*$5Ge8*=cj5MMv&V08YUVPAVV#dzniY0xtg-$ zk>^x6>eBR(B?KI9{s8}-24Jbs;5{V(&FWcS5EKgN(QO;PLNw=&1|b+l$8Lm-!rWLN zP=t@HF;s@E3GYbO>k$74P_G;9a(Kmz)DIWbiuv5#BMLI*@ou6AWC$4PO5fxe8lb?p zrlhc>{dPQ!A#1N}Pfc5VHD(hCr(a?~`5Eqe&})`Yakd0O)e1CReD(*KfbWrnM~M-S zzk^1NcWi2%+hg$#S z0~sjZ-$6yb=b*`^fa8hlooCsg3q!}~KAT)?~(t~&LN zzlA|q5)TEgW}6rQYr&?}U;8AVK?MB2%=RlFF1fM4euqPWFpvcN*!T`e;Jv*!Oinl?xr#-KlA$t8r`udFHZziL~~p93IigZzY8zr`w&~a`MYJb*~$A7uTC!dsZr}>pak64+rL%v{3&o&}3!XqrmT*K;Uv&*4{ygv;_3& z=j4MS9iKYKZU2ksHZm48EWC;65=1yg^>wf7TIG&TQyaJ)br>W%(4-C6QC9mt}njP7^r2GjJ& zP#EZ@;r&a=DJ95rP}1AXQP6j-`Ik2I&CMr`BR&AT;ShK90H-re4^x7?ns)gun4bl1 zB%fE@*m9ndXuH(R-3%1B*=zyXZ?%Z}9Lk6FJd1DY1p+{7W_Lz*AbXX0XE$@~(W|Xf zfR4{~W8#AItDdaZRrQ0L8IT00+eR-?wSGSVHz)bd zv;ZDBa8!OXvRBNF1@7mS+gf5M_R*PSIiPycwBuoz`-CF4Mm5l#Zamu$WlW%svOmpOyC}jXZ0lN|p#Rf^Wa^Az>pSgx3T6Oi~btrN{`6Gl+inkLi zV4fbgPDr2v06osY5sui1R)>~Y|rxzzQP?=(& zHq-l`qlYc3XIMEPK11;2hCm<;mYx=-d$RJ>wq^nf9%3D{^5l1c!wb}kQK($?MHD9n zfZCIno|r?Wd*~wZ3DE55f6IPY+#vnW%^WUTLQs!j>0+%2t-nB7Y&*B`1Gk80!7|xD zTrH=rPku!Mr8Utzw!$n`#Yh44DSz&M%_O92B_Y=aEyLT(LfR3QYT-9J0QL1W|QNE$e}TeXzB2b{!k#F{h5 zRox3EZ7s*_6LqK>uY1SYLg9G8%w7Qnmf$eYAvYDYIUJcA1}B4jiZ`EeS$kc!&Bp83 zGh;$^L2l9I9N71QQfkdM_m7~QQbosawrL8baHF-pq5mof|B%lOX3yoO*B1Y)xFQ=U zr!XroXy0gtK&GA8CK%-NZaGy!=GXqu&k? zdX3j$5?|X}`V(~wB_AHApD|$mw>fVq9N}z^MH~w#d0i0|Z>Vn}zTy38xjjL&#iJnZtH zmB~HtI-h{>U_~Te`#xrEYUhGA6Zi1>*f>*XtNkr_ANL_L58bjm45(Y z*FU2{Q}JCk;h<;V^2zY~7yWq!W&2c%z9hgE@H5M!)eQd`*>%JA9&CSn`IWXLAZ)Kw z(aZnaNSn`o9sjGSmw(zOK%tYib;V3zC-|*NbVx%U+!%XSTuW`|NXnpw*X-ye%TPm9 z_8IoCur|s$l-m;k9Yt~JsV#6^fL9(n;r{GEatP}1MCVg>L;-f~m@B%^{-85JoaENgS&? zGyLXOi1FtPSLKE_JKLnN@p+sn=QqqSOxdIk)I{#G%rYwRiUzs!G_$XJfup~~2N{e$ z=t;*IE*!lSXJ^9_HqHrKsEhIzL71|n=2Nc^#>AZQU~QTl?J)b=eV#nBx;d!x;X^fy zvB=?!k(j9b&Tx6FqHB6Ma-YNYkbGgt-`}h`Q9&E9_e=T1Rl*+m4 zLaW>*m{IjX4j8{1s6#>SS@qo6lprf=ZVM?#kS@%o`zQyLNBnt)#T+6t@484C6x2hU zvQ*_iPj3Om9DC-e_JV_L83rdZkU`Gk7&jucylVGKxFYk#oMqC4F)O6Q*jL@}nUD_s z5r(Nc8D)OE94cL}>-s?sJ+ih6Zy^X%KA$gUKy946_FOu_Z*($u!)lN=%dWXir6AK_ z8jWgQPzSbx9gaZnPOM_sxP-|>0H}@pt|sMc2#m{_>M|sdUXLER45XvyrCsoW7F8R{ zdO|$&t<%hBhKDbRdy?&v=AW~9tA6wyY{`=C{j$uBCVMP}F8+>3BUFfB=E$w{`iVK) zT*3ATnxXL6b`@F}@SqT8^m7mOoz3>IRosDiRq>WRbNeD6Wq&x>MZvD{Cr0SMi)4Q3 zt&~#<6*8jQM)YS{_6ev_34Qhkqvj6rBC#Vd`=MZtfix>`#8Fly)cb`h(^x=nmA@xi z=(q&p#N#(RU9t*mjPZ*qB>BzHl7)%`$weOmnPD(3eN4}}Bg=0;Ux+7$7qn>b&@(PH znR8ODb?{!}R7OVQQVaPPRMCHMP)(F0dM!_%-TJmyY0u+=xC(3fp^|eVH}m2Jb5`H* z*@afFTa`CFSM~yRu_7&d6&P6;v3J`Bm|yFZwT|JJx&JBtoRU=ml9G88!wKW#{eoA!z1K0rnVJ ze*1{MPH%wgSU6D{NG!g7^>7V#596cfKY4>E2n^oAjSn zTuK8|kvg(LI@#0Zmnw3>Rob&L!f%`Ay5BZQCKHHtI+n;~vyO>ML?1VY-7k0Gr0aA{ zm0va&%*7f64wZK9#*@KbQ+g=t|K=4W{Sf4_Kb7fotXM%elD(VEB;smbNtZxI-5f+! z-y&11IJcP|zu}E#-Rwf%@~%I zDQ%oZk`{8p>`Q`>xzv}K3C$;Mr5KFd7SHADeRUHHQ=hdRaO)~ZRC{GdRLU?oOyP1p zG42YJtU#aCW^^VEuc#02F8h7CR@8IA{jv+s(PdT@ToXr_6ni(VA&QYM&!{cilTuI) zuCr4si#FcrBnqZlirnRbct!Na&X$4SgwsiReHQG6!wq_t{i61YDg8^lpOk)Ot)0+u zB#qMBW>o2BV>ZO>!23059}BMQ;2Wvus^1MHk$_G6UI7FyPv%0l8OcP_m8#G*d?igU z4~^5u>13F{mG~3RD+X~BD0DJ3kK81R&Z@a*sAgXeyqdTe>CU!El(Iz0R_q}y`kQcz zPhX`-F8&~Dx6>Uepvx^)$;%hsPA@T=KAdOo;`>g6B$hMgu1*LkR$F>3rrJFKE8&tE2CsObPJ6SOt5^R^+CEB=K~$w zm*b7U*KY$IgkgMK`Lt4sK6()OmCkOaPO ze3-hHEXeKSiO*fg`{ET8*+#3g0H#js>kl&^zgY+jk;5+pyi%53>(j_chpdb|%tu1eT|5NhpR1pr)Ws*;K~{D-XMB)XuCQz9(N)a48Gz#E7q+_Z zB^QcWLO6#?gYn3tV}=()psjhSt&@jyww>@rj;)oxtk6F8di$e6l^1tfkkqm*Pt~&5 zOm-YW@F|CzPXLN(4nVw?zhNx`O7ibTqE15CH zEtM5AwH21@g{-!ZeM8TW+QaB;2p`kjnmSEo5BH)kO9$&4Ws(j!8Ik<1B%??PE}#1d04;ReEp(z+{M5tG(>aqY_Zp!)?{P54SRZ92$CF&SfEeu|)PT zh0h4V!zVdJHC+VE!mBc4T>$uw<+Gi@dQ{8f1E=pFAJPqsg27OoE@f7FH$6cDkG{Ge zy?P93@@vwGjBt7wv@*}OnFC~H_+Dph-*=9H{em&vximm4cXS15Rim370V2L^*Y`?+ zA>Hu}QRT=vIOvAj#&Jf|P0@CxDL{DeVETjdA4lVkmG=bvY8yuQ@2Myp%m$ucDrkRq zbPH;8SI+JPLa_aQu**--krq{nU_e?6X`zN%{j+*#usc&}K2MWyRHNuj0Tig$dqv?R z%%6bFK{VyB!zPJ$LE26;x6O}`{Cid6NxJyvYX*NgxV)D}VVuh3uUmtE=)E8Z2PV3o zlZh{>``g_SM||U2ICQ8FZn_e3yv_aInRUg}&Yr27Lb+u{r~WPw*4aY6e~I_8tUxv} zz4P)lkYCU?M#AuC6G*kn#quj_f@nG2mP*^a)C6@z|<)6;HwBT5-0d{c$AzQb^ zS4s!Bo*zaJ-#H-nP4AP1Sli35)kKMe5b3DH2(H}M?m3y;uW)`Jb2s_QzPf(A;)XI* z<;979CQLf6=a`YRdl}u4coUQtz;dh#Bv{^=XO+rdv+Mz`?V;+H!r)F`q8}*N8X<&j;Vq_l2MTd zLkS0e6y1RwNEpl9UkUdX*#I*Q+QIk`lJiz22pZY@CxN|!V^mnk?2Bri0=W7Td+t@R zD%_T?-3x*CGP+V@?k0Wf-Vu1~gm4&O^&bWMF(2EAascA=lZ8WiFj=qMFu}efB>x2F z-aVaN#{={F$dUNxc2UN>_uNgwxHY>}E4CaZu^Cgo6Y1h>{dvM(s>H9R#K-OlvN6ah z%g=Zdjp*z{9Ge*Sk=TX1?(-!2`f!YsIX@s7P!##6T~HI^%$|~v+t42It^atoBdCf! z`btfJj4A`>CZ?&B8$zDpx{hDmO5080mVaTChQ z9!~zXqdUb$W8FMNYrgTs?^@a)tQ)#Bm0kT%dIeQ6Pu$#bv0MF<{Z~I=!Ru7u4mEGo zVEsIk$8lJZ8b?_Crujj-Q@c_=l$>I?laHD+&gDKlbv0PKlez%xmPOv@W{`L0rdoY zG7W>CMC~5-*h!E!UL+AhacUxMWhtdl_WRL@lFc1-*D82PL>16`VY8Bfxb8c`D5koP ztVr*a+d~|@$7H3oll2C$LvW>fNe)H(%{w;z_=+ykm+*nN*efKKDFL|l?-9tfpUr48h?k4!;O#`XgI6nHVQ zs7D9hqqIeWwGY(x_r8Z{Ad8l~^L13Z@&>}zKEsCH32UUH3J|ka7QXvl);Y3em&A}M zU-T9~aBwYKhrTP+ArmC%N`klvE_ zHV`&fK{Gq@pP_R1-En=&5XoQ#wU8Mi#yx4v4cYv_6jxh>$pJU-y;MdQQa`7u?T90d z#l-lKy=Od_Z!-ZYC%WvC^gFexFVBD?^z1K-s#Okg161p036du$OnxAZ^&~oKYIT^4 z(>VNr7QC;iRX}!pbVqy8Z~k}|mjE`ll&ZOpTmC&`08k2dTNL8*9zZ)I$&pk2k;tHA zEmw@?U10b!i&x+056VV<>mZ9L5RTTtShB|zIq_?$tu~Z=HpRD2S^EIls!LMn6T-8I z$Xuj+bjRMaOW2%fK=)m7Lv=qg55O_yPIfB>rs?m9qlrhTwv)sw#i@l@CxBz&5B2-} zvrsNX6&qt>+T=CsPtPE#regOaoALGOG|Qy~vlI<(-=u8bdAk46zl}>A$lV@GDEpgx z_eIel!RA>a_ix17ji@5jYy{M@p|6%kpUpFr(MJ0O0Wdk23#Z!`jZikfqKo!Uy>1de z*gP_ISmiW8ZJ(h?9(vX&KIZVWQlTWOn_~4IxxX6&^djOtip!V&{7*U2^_TT8ZoYM@ z+r?mxn^G4YSh03!m$rHS?D&JuxS~Fr!zWHhq16y;z|aYlkfDeips%WTUKT<=r`Z5g zr*9wSL;gN>{lE`Qyj~o;9m{_e{nFH-Km+upk)|l(AAKua+eG?LQYp`r)L?(XC%wHN~38kD$&TC2n9vxu>#$Z+byn*1%~mOG+Iq@^hW^_IIWKs-!_Qo(=Z@^*xg{H&Xx&s~K0u94VGpUn+r z>n};dWI=ziq6uP4l;xa7YKGI?ITXzRMb7b56u#+nUjej3xsEf;W_po_JqGg7)t|I0 zG~k1%W#4->4f1T-hl!h`%*8)?>s05?4`me(mm!c-hj~ouqjY}VZ;IZCf5R~b976cz zRP2Y{{=T9Ra3;xPtil0M4V~sUj}NHaLo>;FJp`V?jwnhhFCx)-!~T1f{kV8N8W94j zM-*jU9`1di3crZ({LwCKuw(t~v*$utfGmtJaYEcSSNmHiO&74!S=9y!Ok@8Smq~hl z>gpLbJupX0>8aC?Ov*g&xvOa;Sho*vZ3_>z|F@)gzCkZqOJzI7wXg=e{z54x=#;e# z;Ce!E#lt6bT5fTJDaHPr^}gEY-5&dV#?{;rMSuo(OUL{$Y}YAFnln*$NfPc$+8Fz> zkQxa$l%6>Q)04EB51VeL7>XZ2v|-@%LoT{AbECkkSgs+N&{Jl*1m4zc%yUV>c- zovUJKA|!IeubK^SYMekoU8Oi+iVR}az^YLRlPE0=#~*~Epx?Pty8nWygzvmYk@<`| zJvd4wk@KPp)ps#3Wt1eD6JNNORr=|_eFCO35Y)7}I%(ta8c7eclXbHN*h(tVG2x3f7O#}Jkp4Ah&EAv*%9D=JV_!-F;imTxPE%YlnX!1wqnI*@ND z0y;5b4%ae8=W#jLe*TmTL%M;OTs#&#!U^{RHK%d&UByT60&T%KZClyhdmrYugE8nd zjSICl?AsIu2)t>wJ1&iQ*fs%H71SCfG0E0c3Vm6vwp5(IBUfGA?}xH^sk9DcVpV=q zDisTTQ6CM0Lu`}^p`ZVY2qL9v&@5-~QBl}0N10{2g>xsBxJ>v(HAX1XwtmrhSeZ7|!6yXG(X{j@-oIC}T!)m6OEgXgCqL%ZS9eXrG&-54 zUeJna&x#bWar$cxk2hl|rWBsR9P{1e7{T~2p5^fgUzj?`I%33I$C*+^Rs)m`*4x{) z+?>;%Ppr42?u(yp4L;#`H<~E1dNX=OYa+@*j+ui|q*%L138R(hw=D~67*`-s}>C|dQQ^X+uJhxmog@M)fBnI z@PQ68j_cM5^6unpW? zzb}tx-|cCeEW+#4(T=Nb+JcaC-IgN3UWDx{NvPAOlDyXSZ_b}tnzj`2^#5gHQ3P6j zvfOE9#=aDTJ#gvEZL$G74S9AL>WvqJ!Lmpqyr8Xj^Z~M}AolGxl(`XTo9a^7x zfoL^A%UMo^LCx8QN3dsj;O`nlc?^;t+rK65()(|6OOc4~V2_eGCh!xLi7)z-cVOKt zplfBvFO0|~5O}N}No&cSPIvW46bBv2d^Ms3@{_Dr{>?sd@9ocRt_Rbn&EUDWp7ny$ z?#%Fm$nKVNz#nzx6jB22KKrlV=C>Sff@BORcz7aq4%Z;QiYmYp+3t?b8`VwYd3vCU zH*xTje{d2YXgVqyK=ATJ$OrJsIIzr9 zA@G%fWoT)kE6=(|e!G@c1-VUg?ZJu`**T?Lcu+ghMq!W!ViM~=^zsMMm;}MD_P71 zw!`GRHfk00DCYET^3VR2MFqcD~(+8Ze#25EP+Z8##`d!!q^(csRx|u^Iy~f zjy9k^h%)s1u6!d6YM07?8aNGqy7jDriWB<$BLGlrnHbRinsFsn4tFAcHiiO&juHNnA-Eg%lS||s%!}RwQOffj3fk_a* zVxFwQ*u()o$*jJ1eq=xP@#aNJs1g!dhhS0nU;{?|Vfn z-iUS(3gMFODii3RCM-t2_o&C5qiB40A=E?Skk`Ti#794|+PNzp=w(mp?JDAx@yiQk zcx~0;2&@jzpP_Zm-Sqn#f5lFP+?-OqEA}2k#X#9#+2!G8!XNdTGKS}lC-`r8p8+(yG`xewDU2o(IA0P`0z$VUig$ERX z4x6W`Sp@E10|A$_tqrU{*6di{SXz|je4?yv4UEjZvJdxkS=cIjanN>^@j%(ncqrf3 zV&TPCyY}uvum?18L(>+kk7%uu?j1BNNCSOF^QummIe_9CRkrE2Bn*&+0<7j-^#pxb zU9M{DlGOkOtnLhdhoSzksu}qdjkN3nShYwS*b4lxnrf}Ky~gY=qv z1H4F`R9uWrD3LisuJlLAZX$^ax5h(*HVcOovP!`n$AdE1W9yef;1p>%8dmA_NYWM2 zQI`HE^4wR~geQJ$_QIGaOwHe)nT#yFL=f}Be|QOBvf{L;S%qT?Kyw)NUFk;F&gb)K z>-l?Z9yX=Z?0muAK1L5o%ibC3gnqyO7E!5`LuGlfTM+zrh3Hc$>JBf z__vMm*`m)MF}`r5`{$R+9?+YlU{yJ^%t=JKb%7_}Wj9$w4IyeqDX#B0KYEPwkeYI~tUQYJoPnz7}HI|d7t zC_5?ndZB%xOvC|Au$s=}6wbNV!O{9oK#qzgMSk&GaHPmAoRf%)rx`^+4vQ9XmlXbd zO-ZjnNj^I4Jd@Wv%b)c1r-v9^oG4iF!vFqN)PNTL@2PI8*wKLtJlAd=mph6mJrHem zlL1!EQXV44Dq1J$wvepGDSG}8P0-0Qj!SFc`6@W1()(gf%Gm{F4`%gKiK#48)s#HH6-r>xx3~~NZ$FU!@#x--k4l& zXa@CxD>hiSk0T&$j#smktvg}o4=I1u-vv9xAmVd8SKuT_Frjb7UNQHlR-uUBGk`{* ziEC{!1kZIy;EpY$(g7laOAI+bJPA_rtvu{i4>{j}FVp8qV+&!eWa^J3C@c|uXeVpU zGY+wGh{b|p7iRlhrUDhRzRUSNb8{rRtKp8<(zsa22#$@1gj$FM)hHy0rEX^jJ}6Y5 zIkF7VL>SxK$S0w$&!MpSmlKa!SOSMjT@C>fJ4Vus-=?elwmu)ZscMIV1pA$hJqmD1 z5ah8rX}UZ{_^xKL4u&^%&0FoXN8%ixXe>|4hO1k0+PX8ol@R$|eM{vS65%Q6pICp6 zyHo^A_HFByS`=RGfe(qgBn`ENMD5B}Xizilp26`OqdGAxghwyva=_$3}C_X$&^CwV!ZOo7=RotBhM-#I719@=Y0r0 z6u2z^4?a=rTESTG6w$Njkj^WnhUhkJ!v^8=VwKLf_gl+oE-aW136MIL3 z==pigPqKakgZT6Nw9;wr(<0-iwlsK3Q=D!Hi0s|amkdib0j*$DJJ~G3GLYF%J597B z!tVnAsA_or!lyp9wq&cix;b7(B?eVA|C|C*1+O;5)>WS$yM+ys(w(L zDvsilp8bSy+Y#2}?avg0FH47l)(J^gRWT#mHLGBzCO30KogcP}_R#x-n zNW(|kDzgzV+VDO72BmN6m=uX;3Mt~Bw2~+fIcmqMG9z9XFIKjol73=O(1AT2$0b>< zs$_w_ecxMsEg;JL*>cybf<%KDF}~DvD-`nE@TY?3EO{bf<%ol+$74xrsXy&|z*{PB z93fa%XPdcp6sCW6O`ft{x>86~GCkXwmaq^qtD>ng^_5e%*I#C?LeQ-&YR|+T0e}qx5zD}53E4>i#lFGZ@9?DU9mCTCmTM%x& z31Gw1gOAj2m2h!9IC^M!T+PE1%3&5Nz$5x*n$(|n`(+TK4h`E}e9@}kkNs|}5R(Q< zr}Z|}kKFb2$_RlF+T_ZEm$1&vP8qBd8yDG;e=5#{jaCUQvh!6OQt+TxTR!#N_acCi zpG3`QBC3?Ik%>)tt1?tcrS|={oz;+P$RT%#VaBBzhVsv?f10^D#p!ZpeJn`@`_7A= zueFTe2l(*p~vkEp5D(Q8_q7>_fT7kZ@tnr`r9?twjso>P!iu}YI$}iQ2f*4<9 zLh>!N$K0w7f~oil4`?C@EFF6jO9`H5YI*6g46UN^b_`bvF{2o7N@b`lR_K3PQm_}{ zy!sn7*EEDfJAdCYBs7ukqu$K@vs=8di@qH{O(>5_%8+URdh6dNKSyvCWSVIHywxZ6 z=HblC;fijxw5qrkT_ooJ=6X$og*)u_M2veVh?QCF7=Txs;h9Jf{5m=SQCoG=?|V%I zr+&tcY9H{%y9ha^8sKC(tnWO~R}|Vwuw-We25$T><9u*U=8b@DG+*IG?&uCzVUULa zENx4GFZ0I1wGFN`y2r8E3cTw<2m7JE#Xqa}sCd^9y=plF zV-W8GyUBTO%)8K7WBHvp+$%vqA9h7Q>M5j{!=bUcx8a!R7(RXCR;ttZeNV+(eSZ-9 zV^4*CWpi~{(4NTV2~VHg;eAgfXUPzh^Duhr`{HOnaQ7sZxA=lRC%b<3in+0Zm*y?G zDxcrjY)Yquv`J$lGtMtIW^_o>X|;=bPgTi9Zd7%T0y<5yDkBlAv1$<`R|p^fx-mtV z9hImY4f1vwhWEqUZZaliG%2@Jeth{WLbSkDSoaPg{7;-U0>!@qwz_HhNt9cKcWb|a zmv8Ue#Ri6)KNsa=vo5hsxePJvI}scC1cjV0GzIWlRN7`9Rf1C&I?UtuHR8CYs(4{U z-JhpRHr=`9`|lxOb*ollNxvQ=d^;aLFl&6jpE$VOyFrg>HO4|Kvkjd5J9jf{5yGeP ziwjOp^B19_Uw_#>t)z8{c0D33PvkA9<;~Fg>hLm;g6$}Zk;s~MlaB2sUe0+M?>!qD zlM*uO4?p?tmkv<29y5o-vgeE`#UXZzF8t_21S8vRHY%;c1+2^w*)9t5U0$t?jhX>y zsqS6gu0i6?$6N+(Bcryf=QGPw>5-qyI=OCN4fLf3``N4BmY>3_I{(P(FKUU8b?X-2sGm4V#Y z$hqy77ERIqj}5Fd0bKw4RXRI+8|K>r6B(PT&UVJ9>`6~5Y*g*GjyjI_Qb}uX*S16O zGS-la;ixH3v&t~5CqJutNDrR$9eu)IolgGDajLokw}d4v9>&O&gg5O+41>Be9=DrY z;z{o+a_lLlB0e4rSwv#QqTNx*zFd?VB`fdz{E#+l5VCbwkSIQaEN4{{)zg+K%lR3! zZ!O&vj?eW9LCC=1=V5hJ&UXK)FA^43;a6H?N{zQvegHxiW(t?7eBK~xV89tVzzk6( zxfuvRCiQEga)#^Nd6BU2B$%6JxfMYJ0$%&kt#!GCFT1*?UO3Dpjl)y_Htu!WbV&2& z8Vs=Wse!0V56Lf4AB-A${PGs!`XxO$1(ia>h8BDWJn?|5u}5+PJGQhl=v{!P{1&f* zL1KQsmoBVl*R(k(BN4hd@zq2s?WAQQ*yM0NU6_J_=faGFOxSoI?@Hv!l;K_7mo7}8 z$#wA~H4e@;#d-K&WRoukYJAR^6N2aCQ!ApG5HqXlBr$)3i=6=lSzn22x^QQjx$Yc5 zK3BpS_WbpWJU0t5wp`9O-qlH%%>+QnX&d1@JoGOM1K`_ejkv>8Rm0y zX>IyG;5#tDv#iN_qclk_WJ@#?+9yMswNw*rNsViP3Mv0f>(~KE z&>^(i{B|L!aRk@a!e3>UN1y$E+*^b0Ze}H;((p}tF_frg^*X-9EU5o-Rr>$;RQME1 zJuN>Ubb|+LdbRIrTgk_#lBPiY@{vsP==kbnEtv4c&Uq+X_nF4vO5QnBqUA`Zm*wGv zDkr-6DBb>w!L#teY3}RhZiUlz+PLh2Pl7kz#xj;RzxlmWU<%LRo1L7^3tr-LPSF&> zyTGG;SMFYYWVM5#0?Yr&^M;0O+NT-x2o*4;d5mC zlhw0`n$of3I!N;|Y|JssYCd$GvYM6yU`CpYJ=u#gylU1opaxf(+J8FzsYLMn^-#T0p7KCd`+W^ zv4)^ci!r64Pps~tq;e#@QnBW{BgyP7@+Tw7SS2l;-af9SCk{Fy`o;|42!(-Ih7fqu zqAcBvSTY8|lFJ-UXrORGB}8IB4O_Oun7bL56AXi~5BQ9^{-@6W3yq3pfE8okVrzt( z@dFovS4pRF{Hcm6Lh(gXj?2F8^jB|;3oZ4U-dH8=sYF{|S7r6-V{c4-SM`n8SS9nR zgmwf^hUrQ}D8A)WNfRtx>$Z(4MdPg%yt{{`Kt6pUihsI)VdGa=SsDVkx7A@=z-)c2dmE@@2hI3sJeO7NA*};1{+>BKFzity8 zT}HJtExbPld2G*P?951>f0r;RjBY=uAau@&as<4jPGv^KbI#@VF6w$%ucFN}le0Dn zWmum7id2her0-fltSvDQeoCh#DT?_ScNBDKxFvN6zlQ!P$}4C|z0x$_m}7KPY|Yu` zYyb34K^dbU)U070zZLxR1cB>hQ*5$!hOZ9h*& ziod(=rj6i8u{PohMAz0q7siR~<-iTlyxNZu$DD4o1OWbuH}4@)30_>V07H?R@JKwL z{+UofJ?ieLRm~AcAuuDtsalh|T7F4AiN+RN@#cWmS@)y9NgaGM#pp-3FS^#K9NdI> z#Q`H=V=C#Q)xO+MB$}{_VP*d^UaoQ-qmrMAkvkK{%+T)`yJn%|gZ!%*uFUrLP9r`` z9*(R|C^_4?qc-19g#ZfrzEny(r3F#3}U@D9aLQ(kq)0-HzII9OR7kMyf(!^?`D z1O)+g^k`u7>zx{F&UfiI6jQ<92n(3=1%SL^BKSK~)%|`3U{tLovRlfBudv*n?ZVf5 zdk8nEcfv=;zJ6M3{0@36CfnswGcc-cN(uEPVUwYdK2zJzB!;5mB;WT!dqtqFZPS`CxTk=8`K@kVNU#+?_oBVb(h~yM~GY+eP*v6KZMrv z=aSPAtW1tKA8ZyU;YbxrCH{etpnRgGh8mPL;QxHl0dQ1OfCFCO_C7sOxBZe zkauJb22v}O<%u)aCOOG*+VD2D{JO}yagkv6RLGX71V3bGMGVt0+FMLvIwBWjv#u@P zL#~*(keX{z30*}rP$BHi^J1jUl}p+^C`~N37&IZBv8^V6w7J#0miJH#fc~&nbOxUO z1fbGZPcPqe$B}DUgVI$={vnyZ!6TN=+Z#>L6;*_TGWDRE45U8-=z7k! z1=Sye%Oh=S|NhFh<^rBa3|XeD`T&u2c{_qSX^Ic`{!Azs)c8o`I!q-3*m~)8n~qRo zi203u%YS*SuZqKVD~fk8&GGc>O87_)(>jNRU=ua8Iz|yL#082V6_HAf(VY$9a)wlI z(!iW(C$Luy3#IFmg~JQs#wOm~yWDVYK>85`i{ArB$a)Exj^Xt%?_1jkT~N$jnqcJ~ z@Koz!RiO;hJDp*fd^%X0vSf2DXQE3%$OkL|cFgwkdMTNXCfV6BCdJ2mtexhrgs}PC zJu&YHW7KG~iUWq@&>g~d?icC*p1SYJV|nV|4mEiR)?y)zaxzIWL%C1^{y3bsT?W6C z!cGOkXa~stx%?7khmdLek(<->Sx_%i>F(LXF1N4Ov&r8Yei(Eez?^!;d@GpAHP~0o zrGyA(mLiZUAuSi=c@0KxbutS4@UTbiVBK)Z1$eR-!t;s{QVnlUq0 z6S1wI+(7p<%V#U5yYTWJH8lUt!TuvFO-6_|$g7+L{n97v!7QRgnuFz?xK--YS z@>@o6n=dJ&4SZ7&^W3Z@3}%lX%3|FJzR{(Ru@bNl?qY=$3nQP|RAP_f3!E{LyuM4^ zP(8RoUDY7)-_}Umya?wk1=()ARtkDFKwwJ+#tPxN<#yIyY6HdceEl;+-y+ZRSu(FN zsY>X>@a|f%VA$u9Z|kE)jMs_U40~~=cjU3Ef|@c>h3Q_!*FC?n)s!3loaS~D z6%|EBM+ZKGRL-`p&CA2E46UBj)|%I2RoTn6%3sJmsSobX zSx>*6V?*A&zxa2qXJkA;wk971ZXVV(w)y8>RhJoYHT>v3Ixo(Q1iicSEsJ%0IX3qwrTJuNeSyH&fw=$y&vT>VwwdW${m3cjNBmB36;}z`%!}q8IHsPa%zjK!_TD zSz^;v;UI>eZh4BlU~RYw{N*Y5L(3aOYrY-ArtozstfL+u3UsdKdWJ<8W2*9neKrX# zlVG&LNcpx#QY}VegOc*?$!I1*Ih5GTe<)LC0Y2uZMNy zaY(Pd)_amfGr9t`U~LhB;F%;2SjVU#Kyz8d^wypP4YUDZRFHSWO}Qa0(W8*hF2bo2 z`BtOo5AOm4%xdT+n$Me6uxQJ-+#9PTS7sLFA4OD;bYn?VzV6=MFtBWr^;L}#?Npao zD}@PdAUc#psbWz)yum3|gQ0kx-d&)M%Bd{_jQ}>TawgucRrw`?un7oAz8* zt`Hge4HjhZ8 z+56#?q^FbP<7upzz{3|ZT&v5_i=^6aU@`qBT9cwwms}brN~LcmqP)6a5?;VsUO;W8 zSW1~UpUPb(j4XX)Os7f}sWkAj$|xXvubz zlI>dI2(t7MaD5^-kw0&~UGM}CSy~jho-A5|5~T!x>lrOVyO7nOaziVXWReIYiLer_ zLH(;iB^vnKC=rL3HwV|KB*3PC`nhOj4z9qi-I$`qt%g5F!|IcDNrYyil%bzm0C*Kq za)c?JDsiMzNt6=k6vAPw26bqI$^)%bhxx|VN>XvKh83lCiIryJV>hL0j^|llctL1& za_KM8lGrFEd!ysJq*2sQJJi%JW1 zlsC~|R~qie=<1bj&6%b4CTLBYQGcAEc|8y!h9|UZryp;$q{BOBSZ;i#1H+JvfBnLk zl|)AvcD2{%uhqlpAd{uRhtRh}kAbX(0nGqk!*AuHZ;b_Yj9)|Bx##G#2}gqa=3yB= z6y&#GALy99foQB(xQnFq3dI2LxNBqttSj$W7hk^q^?%=bNZad!PlMqcFL zTn1V=nrGLQsMx(fKA=L;nK6e%%)=7uDC%qRO0iE2=#3u~R~;_fcPLuz;FQpxoti;7 z@&hLV3K!v-qMcbtI>k^7bX&@G14w09(2w-Uan*tro+Rz{BIjZwhKr0y^W57b72nC~ z_$%&vYo7aD<43a8eD;b`$;@j&&7{idc*!unjz*7-W$fEjWza z@IRumtMqRZvS=X&ID1D{c`FP7S4Bk6pv0%4ae5j>y6jZBGzKITF1~l0l_3?~Y}E>R zSbiVw)(dCwy2l5yHFIb7$x47(MFz-Wjny0e{J0I@P=FV|iyt$&pv41GJF7@$miwx9 z-+N3aOhw$#v^ejPvP`V4zzhPwUC(K3CUhKnR6yH&>&j$A#@h9s+E<))97l0(11ul| zj-?k2{V%6Hv0yiu#hrpG3Kn090Ee&3C$|1e!b8dkA{Yipue5Oaq$Bsn2XTK`lmHJt zD*aOsKs=idvpoZRi~;N>hO@KW5@|O-N&&amayunNjS*q~iTGmn5X$h(ZtRiA>ll8< zRH&QcqZWSbd)Fj)H#S|)qpVq@dzquZ7s7_yRD+5J_iqoY?>AhM>i-o@o?3`qH7bjc z?ROACVV(*jvf?`tsh+7oZnN~FLQ4B4Er+40fAi=q?gV-y)wXs#Y@*ZKhf8WqDD&P% z5ADR^MU8@IE;}XUy-n#tJmayO0ofbKiYeS{ur_75@^m$xDbCzL2^{k(9tVT=`1Yvz zY=vtyW&9a5py{+LzqXDesXnvgX%NZcavb|G-oF9UYLav2zEeWww+kGgMY~o|xQOVX zTdKlbPUgt`W#kTi#PPw%WT|oqBDD5zP+2+s-1#Nk6%PESa=G228^6caYo~-R*9|Yt zjNDkTgMr{e=9{hx+3!XTH!5tmoK%OlT&1ZA;Mci<_NIpwjBTDnf1Gfpc%YYjuiY;iSM#K2C8Pl~rYPAVIg8Wkqp5UY z7y@OjQEa4H-A}>N{*eF99M)~(-;Ihk(9T_HN(B9wauV?>#DevN@X1MuR>>Iy&`a{ox(U#NvD1B#V!d*shgZM!nX_%qqcc?-R(2t)G1%hW~JjUOwI zY}>VXdLQ^2ETH8qK_r$t`Ns{I&kYwB|KsAC(u+Pt?6hq=HT()j30Q@b;F7~6li{B)O;3mZMuV!#w0 zu0IvR*jxa*noHj;uK)VXci15iTXfN9t9Ut|xhI^&y3;8g!=9d|S=auVbs%3OEo_kq zZN9BHZzbMKXzhAXUOD~jWj{zY3TWKs&F4MSfHTjXB664%ZH;9hi+&`sYs@Smja$)K z$%m)}YqKAg@qS)YY8X7N-&=l1rWtMfZ|NXzh|S)|7s~J%k`|sni3epnkDGGL?y{p` zLUt8pU+!^lXYH^4-cw9d>-oMcDdvr?@As}A56TPoqos4Lf2;`OZKdf%)xLJmto;?d zAyq)^)A9_`_kW1%W$m8pMb_wU5VhXdI_Rc&$V_OPSRi)C)|>;Lj}BL?7ac?HUEKy4 zB}QL3jlp*WItaU>5Uzo88CSiP@ocm8OAXSZ%dq=}9$+ivwf! zidP~+qg+fTl^5!AfACpQQ|a(mH~DbB*&CVxM&aRHu|ZP4U-M(z7)yWe zRBEpwi8Dxa1}HH)j*c>t*(b*rEUUOHS#hF%Z)v$EO5HwPY|Gqe(C-eHpPwiOJB(jx z(3>2jheA^daZ-5DV%9b(j*$cMz8fHyNDfE>a-l0A-`GxIQwlAcyFfu{W+=H(Mj(l= z3N4a@MF}Eri)c%1KjXb91ar<|#Lv_E!$lxrt_tlpRVU>_ITp~F%y=*HQ40OGfK7cM zZeWQ(atAKb-mjvHhC|vCJ4O<)3nOn!Y7f^ZGpy!9T^eQKzy1h`Q;;+%)}mXaBromc z3XMY+)jOf48y-U2zI!EJorpdA9c9$Q5*b3XFk@68ghOo3%s|IyHnC@9wO^T3CVq#- zagSKF~x-LwypnCI@CN249c*ThVAb-}REig#!(0H?AO1?6{r2yHTo!^63#3e_oxFotvuK8qP^ zDcbECaby8!a_keVD$Eo7j{&7l)lX4V@VIwMjdqnI&KrRI270?t0v7}xDNm&aOI#JIl|w1X&P{eGRVlYt zrE{KgEkso*gjOYjXVoUE(!_L^g7z-lLV=|C$jZceM{4-e#NUAG*K`47X(A6GmEoa8 zP?Iz0QVnHt?TaPF$5tjPEAznSN?jJxn7+-+%M~kK%_TWCFUrfSP=Oyxwcs4+gQ!Xg zwkp}FA(qEb4}?`FIQdJ=HBKs2s!~*~O02Di1}an_E(M3ap`n0LS@lt@n~1GqrP1jm z<{s~LP8BLs0Iy|`jG!t-q)SB_LyPx~x&Tj?YCSJ4Ni<0*voaBB(=AD{KqZ@|K)J>$ z1d#G+3dT~sUW=8eXOeO$@isoIN}0AQC4QMd11{#!h&oX>W0WSAx)coO^}_?IJemSI z=%LtRrP8@1rMJKXcNHqOL#Yy{%Q+T6I+7}3Iy?X-so_w{(zQ~=4UmqcEHSm|2}={X zTnbnTel7vd>9#8U!els&pe`UNLwl;wq)na?v4np^f2%`}CpNRsOW-SbiZ%Y0nM_^G`M5`m0FDP+K⪻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 x%~%O@&ufKXxje1)?;(_z&Mt^kT5|lewQh_^h Date: Tue, 17 Sep 2024 20:34:14 -0400 Subject: [PATCH 62/65] Properly update mappings and fix walls --- .../populator/BlockRegistryPopulator.java | 57 +++++++------------ .../registry/populator/Conversion712_685.java | 4 ++ .../registry/populator/Conversion729_712.java | 16 +++++- core/src/main/resources/mappings | 2 +- 4 files changed, 38 insertions(+), 41 deletions(-) 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 9603cba63..bface58da 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 @@ -34,8 +34,16 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; -import it.unimi.dsi.fastutil.objects.*; -import org.cloudburstmc.nbt.*; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.cloudburstmc.nbt.NBTInputStream; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +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; @@ -63,7 +71,15 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.io.DataInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; @@ -112,40 +128,7 @@ public final class BlockRegistryPopulator { .put(ObjectIntPair.of("1_20_80", Bedrock_v671.CODEC.getProtocolVersion()), Conversion685_671::remapBlock) .put(ObjectIntPair.of("1_21_0", Bedrock_v685.CODEC.getProtocolVersion()), Conversion712_685::remapBlock) .put(ObjectIntPair.of("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()), Conversion729_712::remapBlock) - .put(ObjectIntPair.of("1_21_30", Bedrock_v729.CODEC.getProtocolVersion()), tag -> { // TODO: Remove me when mappings is updated - String name = tag.getString("name"); - if ("minecraft:sponge".equals(name)) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - builder.remove("sponge_type"); - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - if ("minecraft:tnt".equals(name)) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - builder.remove("allow_underwater_bit"); - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - if ("minecraft:cobblestone_wall".equals(name)) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - builder.remove("wall_block_type"); - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - if ("minecraft:purpur_block".equals(name)) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - builder.remove("chisel_type"); - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - if ("minecraft:structure_void".equals(name)) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - builder.remove("structure_void_type"); - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - return tag; - }) + .put(ObjectIntPair.of("1_21_30", Bedrock_v729.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/Conversion712_685.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion712_685.java index 557a38f1f..db715e015 100644 --- 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 @@ -32,6 +32,8 @@ public class Conversion712_685 { 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) { + mapping = Conversion729_712.remapItem(item, mapping); + String identifer = mapping.getBedrockIdentifier(); if (!NEW_BLOCKS.contains(identifer)) { @@ -153,6 +155,8 @@ public class Conversion712_685 { } static NbtMap remapBlock(NbtMap tag) { + tag = Conversion729_712.remapBlock(tag); + final String name = tag.getString("name"); if (!NEW_BLOCKS.contains(name)) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java index 3b8d6d4a2..5d4ebdc47 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion729_712.java @@ -12,7 +12,8 @@ public class Conversion729_712 { private static final List NEW_WALL_BLOCKS = List.of("minecraft:cobblestone_wall", "minecraft:mossy_cobblestone_wall", "minecraft:granite_wall", "minecraft:diorite_wall", "minecraft:andesite_wall", "minecraft:sandstone_wall", "minecraft:brick_wall", "minecraft:stone_brick_wall", "minecraft:mossy_stone_brick_wall", "minecraft:nether_brick_wall", "minecraft:end_stone_brick_wall", "minecraft:prismarine_wall", "minecraft:red_sandstone_wall", "minecraft:red_nether_brick_wall"); private static final List NEW_SPONGE_BLOCKS = List.of("minecraft:sponge", "minecraft:wet_sponge"); private static final List NEW_TNT_BLOCKS = List.of("minecraft:tnt", "minecraft:underwater_tnt"); - private static final List NEW_BLOCKS = Stream.of(NEW_PURPUR_BLOCKS, NEW_WALL_BLOCKS, NEW_SPONGE_BLOCKS, NEW_TNT_BLOCKS).flatMap(List::stream).toList(); + private static final List STRUCTURE_VOID = List.of("minecraft:structure_void"); + private static final List NEW_BLOCKS = Stream.of(NEW_PURPUR_BLOCKS, NEW_WALL_BLOCKS, NEW_SPONGE_BLOCKS, NEW_TNT_BLOCKS, STRUCTURE_VOID).flatMap(List::stream).toList(); static GeyserMappingItem remapItem(Item item, GeyserMappingItem mapping) { String identifier = mapping.getBedrockIdentifier(); @@ -88,7 +89,7 @@ public class Conversion729_712 { switch (name) { case "minecraft:cobblestone_wall" -> wallType = "cobblestone"; - case "minecraft:mossy_cobblestone_wall" -> wallType = "mossy"; + case "minecraft:mossy_cobblestone_wall" -> wallType = "mossy_cobblestone"; case "minecraft:granite_wall" -> wallType = "granite"; case "minecraft:diorite_wall" -> wallType = "diorite"; case "minecraft:andesite_wall" -> wallType = "andesite"; @@ -97,7 +98,7 @@ public class Conversion729_712 { case "minecraft:stone_brick_wall" -> wallType = "stone_brick"; case "minecraft:mossy_stone_brick_wall" -> wallType = "mossy_stone_brick"; case "minecraft:nether_brick_wall" -> wallType = "nether_brick"; - case "minecraft:end_stone_brick_wall" -> wallType = "end_stone_brick"; + case "minecraft:end_stone_brick_wall" -> wallType = "end_brick"; case "minecraft:prismarine_wall" -> wallType = "prismarine"; case "minecraft:red_sandstone_wall" -> wallType = "red_sandstone"; case "minecraft:red_nether_brick_wall" -> wallType = "red_nether_brick"; @@ -136,6 +137,15 @@ public class Conversion729_712 { return tag.toBuilder().putString("name", replacement).putCompound("states", states).build(); } + if (STRUCTURE_VOID.contains(name)) { + NbtMap states = tag.getCompound("states") + .toBuilder() + .putString("structure_void_type", "air") + .build(); + + return tag.toBuilder().putCompound("states", states).build(); + } + return tag; } } diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 698fd2b10..3e85fcc87 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 698fd2b108a9e53f1e47b8cfdc122651b70d6059 +Subproject commit 3e85fcc87d7cfa4162cd8823192fcee0830be049 From 1ab740915a61430baefc04df9e22ce56fdc06db0 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:00:33 -0400 Subject: [PATCH 63/65] Fix diorite wall 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 3e85fcc87..0fb6f09c1 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 3e85fcc87d7cfa4162cd8823192fcee0830be049 +Subproject commit 0fb6f09c1507632f1a0c2520a78d3cf450f7bea3 From 2ef6172bfc84c11a4689e32fbcb64de99a687e34 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:51:48 -0400 Subject: [PATCH 64/65] Fix wet sponges and purpur pillars --- 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 0fb6f09c1..830085e37 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 0fb6f09c1507632f1a0c2520a78d3cf450f7bea3 +Subproject commit 830085e37e0053c9d6116bcad37cafcba83aded5 From 7af5bac9057a65c299d8328d9b5711df42b84be1 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:42:24 -0400 Subject: [PATCH 65/65] Update sponge and purpur pillar items --- 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 830085e37..93f207e7e 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 830085e37e0053c9d6116bcad37cafcba83aded5 +Subproject commit 93f207e7e9d73f58a7c8902f7deda9dcb0524c8e