From 50a0e61c943fc91db47f970194a4202a73417d58 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 27 Feb 2025 20:58:08 +0200 Subject: [PATCH 01/86] Indicate 1.21.62 support (#5378) Co-authored-by: chris --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6962f65e..dcd9b8530 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.21.40 - 1.21.61 and Minecraft Java 1.21.4. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.21.40 - 1.21.62 and Minecraft Java 1.21.4. 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. From 758700cb2271ed54a3890bcdaac9c3c3db04f7a9 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 27 Feb 2025 20:11:16 +0100 Subject: [PATCH 02/86] Fix render distance issues (#5381) * Potentially fix render distance issues * AGGRESSIVE fix render distance issues --------- Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com> --- .../geyser/session/GeyserSession.java | 21 ++++++++++++++++++- .../org/geysermc/geyser/util/ChunkUtils.java | 9 +++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 96f553e20..f303ae3ce 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -341,7 +341,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private Vector2i lastChunkPosition = null; - @Setter private int clientRenderDistance = -1; private int serverRenderDistance = -1; @@ -1453,11 +1452,31 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { sendDownstreamGamePacket(new ServerboundChatCommandSignedPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet())); } + public void setClientRenderDistance(int clientRenderDistance) { + boolean oldSquareToCircle = this.clientRenderDistance < this.serverRenderDistance; + this.clientRenderDistance = clientRenderDistance; + boolean newSquareToCircle = this.clientRenderDistance < this.serverRenderDistance; + + if (this.serverRenderDistance != -1 && oldSquareToCircle != newSquareToCircle) { + recalculateBedrockRenderDistance(); + } + } + public void setServerRenderDistance(int renderDistance) { // Ensure render distance is not above 96 as sending a larger value at any point crashes mobile clients and 96 is the max of any bedrock platform renderDistance = Math.min(renderDistance, 96); this.serverRenderDistance = renderDistance; + recalculateBedrockRenderDistance(); + } + + /** + * Ensures that the ChunkRadiusUpdatedPacket uses the correct render distance for whatever the client distance is set as. + * If the server render distance is larger than the client's, then account for this and add some extra padding. + * We don't want to apply this for every render distance, if at all possible, because + */ + private void recalculateBedrockRenderDistance() { + int renderDistance = ChunkUtils.squareToCircle(this.serverRenderDistance); ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket(); chunkRadiusUpdatedPacket.setRadius(renderDistance); upstream.sendPacket(chunkRadiusUpdatedPacket); 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 96471a2ce..cb149426d 100644 --- a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java @@ -99,13 +99,20 @@ public class ChunkUtils { chunkPublisherUpdatePacket.setPosition(position); // Mitigates chunks not loading on 1.17.1 Paper and 1.19.3 Fabric. As of Bedrock 1.19.60. // https://github.com/GeyserMC/Geyser/issues/3490 - chunkPublisherUpdatePacket.setRadius(GenericMath.ceil((session.getServerRenderDistance() + 1) * MathUtils.SQRT_OF_TWO) << 4); + chunkPublisherUpdatePacket.setRadius(squareToCircle(session.getServerRenderDistance()) << 4); session.sendUpstreamPacket(chunkPublisherUpdatePacket); session.setLastChunkPosition(newChunkPos); } } + /** + * Converts a Java render distance number to the equivalent in Bedrock. + */ + public static int squareToCircle(int renderDistance) { + return GenericMath.ceil((renderDistance + 1) * MathUtils.SQRT_OF_TWO); + } + /** * Sends a block update to the Bedrock client. If the platform is not Spigot, this also * adds that block to the cache. From ce1535991c94e1d39804e55cf9c7c6bad5a7158d Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Sun, 2 Mar 2025 18:53:33 +0100 Subject: [PATCH 03/86] fix https://github.com/GeyserMC/Geyser/issues/5387 --- .../main/java/org/geysermc/geyser/session/GeyserSession.java | 3 ++- .../bedrock/entity/player/BedrockInteractTranslator.java | 4 ++++ .../player/input/BedrockPlayerAuthInputTranslator.java | 5 +++-- .../protocol/java/entity/JavaSetPassengersTranslator.java | 4 ++++ 4 files changed, 13 insertions(+), 3 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 f303ae3ce..005e72097 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -126,6 +126,7 @@ import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.GeyserEntityData; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.ItemFrameEntity; import org.geysermc.geyser.entity.type.Tickable; @@ -1370,7 +1371,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * You can't break blocks, attack entities, or use items while driving in a boat */ public boolean isHandsBusy() { - return steeringRight || steeringLeft; + return playerEntity.getVehicle() instanceof BoatEntity && (steeringRight || steeringLeft); } /** diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java index 62487b20d..794375fde 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java @@ -78,6 +78,10 @@ public class BedrockInteractTranslator extends PacketTranslator ServerboundPlayerCommandPacket sneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING); session.sendDownstreamGamePacket(sneakPacket); + // Reset steering to avoid these accidentally triggering session#isHandsBusy + session.setSteeringLeft(false); + session.setSteeringRight(false); + Entity currentVehicle = session.getPlayerEntity().getVehicle(); if (currentVehicle != null) { session.setMountVehicleScheduledFuture(session.scheduleInEventLoop(() -> { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java index 62e574a3a..8e09c6c98 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java @@ -39,7 +39,6 @@ import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket; -import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.Entity; @@ -168,7 +167,9 @@ public final class BedrockPlayerAuthInputTranslator extends PacketTranslator Date: Mon, 3 Mar 2025 20:56:02 +0800 Subject: [PATCH 04/86] Fix button sounds (#5367) * fix buttons * delete unused mapper * remove ignore arm swing * Update mappings --- .../geysermc/geyser/level/block/Blocks.java | 28 +++++----- .../geyser/level/block/type/ButtonBlock.java | 32 +++++++++++ .../registry/loader/SoundRegistryLoader.java | 7 +-- .../geyser/registry/type/SoundMapping.java | 16 ++---- ...BedrockInventoryTransactionTranslator.java | 10 +++- .../player/input/BedrockBlockActions.java | 1 - .../java/level/JavaLevelEventTranslator.java | 2 +- .../ButtonSoundInteractionTranslator.java | 53 +++++++++++++++++++ .../org/geysermc/geyser/util/SoundUtils.java | 28 +++++----- core/src/main/resources/mappings | 2 +- 10 files changed, 131 insertions(+), 48 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/level/block/type/ButtonBlock.java create mode 100644 core/src/main/java/org/geysermc/geyser/translator/sound/block/ButtonSoundInteractionTranslator.java diff --git a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java index 7dc526ee3..527e49b14 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java @@ -630,7 +630,7 @@ public final class Blocks { public static final Block REDSTONE_WALL_TORCH = register(new Block("redstone_wall_torch", builder().pushReaction(PistonBehavior.DESTROY) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(LIT))); - public static final Block STONE_BUTTON = register(new Block("stone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block STONE_BUTTON = register(new ButtonBlock("stone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -997,43 +997,43 @@ public final class Blocks { .intState(AGE_7))); public static final Block POTATOES = register(new Block("potatoes", builder().pushReaction(PistonBehavior.DESTROY) .intState(AGE_7))); - public static final Block OAK_BUTTON = register(new Block("oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block OAK_BUTTON = register(new ButtonBlock("oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block SPRUCE_BUTTON = register(new Block("spruce_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block SPRUCE_BUTTON = register(new ButtonBlock("spruce_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block BIRCH_BUTTON = register(new Block("birch_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block BIRCH_BUTTON = register(new ButtonBlock("birch_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block JUNGLE_BUTTON = register(new Block("jungle_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block JUNGLE_BUTTON = register(new ButtonBlock("jungle_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block ACACIA_BUTTON = register(new Block("acacia_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block ACACIA_BUTTON = register(new ButtonBlock("acacia_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block CHERRY_BUTTON = register(new Block("cherry_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block CHERRY_BUTTON = register(new ButtonBlock("cherry_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block DARK_OAK_BUTTON = register(new Block("dark_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block DARK_OAK_BUTTON = register(new ButtonBlock("dark_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block PALE_OAK_BUTTON = register(new Block("pale_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block PALE_OAK_BUTTON = register(new ButtonBlock("pale_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block MANGROVE_BUTTON = register(new Block("mangrove_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block MANGROVE_BUTTON = register(new ButtonBlock("mangrove_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block BAMBOO_BUTTON = register(new Block("bamboo_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block BAMBOO_BUTTON = register(new ButtonBlock("bamboo_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -2232,11 +2232,11 @@ public final class Blocks { .enumState(HALF) .enumState(STAIRS_SHAPE) .booleanState(WATERLOGGED))); - public static final Block CRIMSON_BUTTON = register(new Block("crimson_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block CRIMSON_BUTTON = register(new ButtonBlock("crimson_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block WARPED_BUTTON = register(new Block("warped_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block WARPED_BUTTON = register(new ButtonBlock("warped_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -2336,7 +2336,7 @@ public final class Blocks { .booleanState(WATERLOGGED))); public static final Block POLISHED_BLACKSTONE_PRESSURE_PLATE = register(new Block("polished_blackstone_pressure_plate", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .booleanState(POWERED))); - public static final Block POLISHED_BLACKSTONE_BUTTON = register(new Block("polished_blackstone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block POLISHED_BLACKSTONE_BUTTON = register(new ButtonBlock("polished_blackstone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); diff --git a/core/src/main/java/org/geysermc/geyser/level/block/type/ButtonBlock.java b/core/src/main/java/org/geysermc/geyser/level/block/type/ButtonBlock.java new file mode 100644 index 000000000..2a2ae4702 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/block/type/ButtonBlock.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 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.type; + +public class ButtonBlock extends Block { + public ButtonBlock(String javaIdentifier, Builder builder) { + super(javaIdentifier, builder); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java index 318cc08d7..7033740c1 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/SoundRegistryLoader.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.registry.loader; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.registry.type.SoundMapping; @@ -39,7 +40,6 @@ import java.util.Map; * Loads sounds from the given input. */ public class SoundRegistryLoader implements RegistryLoader> { - @Override public Map load(String input) { JsonNode soundsTree; @@ -51,7 +51,7 @@ public class SoundRegistryLoader implements RegistryLoader soundMappings = new HashMap<>(); Iterator> soundsIterator = soundsTree.fields(); - while(soundsIterator.hasNext()) { + while (soundsIterator.hasNext()) { Map.Entry next = soundsIterator.next(); JsonNode brMap = next.getValue(); String javaSound = next.getKey(); @@ -61,7 +61,8 @@ public class SoundRegistryLoader implements RegistryLoader { ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM, 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 5b4ff1de7..f2daf92bd 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 @@ -90,7 +90,7 @@ public class JavaLevelEventTranslator extends PacketTranslator Date: Mon, 3 Mar 2025 20:17:14 +0100 Subject: [PATCH 05/86] Fix: beacon speed effect causing visual glitches looking like a heartbeat almost looks like the player is alive, lol fixes https://github.com/GeyserMC/Geyser/issues/5388 --- .../entity/attribute/GeyserAttributeType.java | 4 +-- .../entity/JavaUpdateMobEffectTranslator.java | 33 +++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java index 10e93810e..80db3354e 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 @@ -25,10 +25,10 @@ package org.geysermc.geyser.entity.attribute; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.cloudburstmc.protocol.bedrock.data.AttributeData; import lombok.AllArgsConstructor; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; @Getter @AllArgsConstructor diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java index 8ff5d6d29..5554251a0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java @@ -25,16 +25,17 @@ package org.geysermc.geyser.translator.protocol.java.entity; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.packet.MobEffectPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.EntityEffectCache; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.EntityUtils; -import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundUpdateMobEffectPacket; import java.util.Collections; @@ -49,8 +50,15 @@ public class JavaUpdateMobEffectTranslator extends PacketTranslator session.getPlayerEntity().getAttributes().get(GeyserAttributeType.ABSORPTION); + // Fixes https://github.com/GeyserMC/Geyser/issues/5388 + case SPEED -> session.getPlayerEntity().getAttributes().get(GeyserAttributeType.MOVEMENT_SPEED); + default -> null; + }; + + if (attribute == null) { return; } UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); attributesPacket.setRuntimeEntityId(entity.getGeyserId()); - // Setting to a higher maximum since plugins/datapacks can probably extend the Bedrock soft limit - attributesPacket.setAttributes(Collections.singletonList( - GeyserAttributeType.ABSORPTION.getAttribute(absorptionAttribute.getValue()))); + attributesPacket.setAttributes(Collections.singletonList(attribute)); session.sendUpstreamPacket(attributesPacket); } } From 679bc4147e6a9e7315787a840198fc68f03b6d04 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Tue, 4 Mar 2025 16:40:14 +0000 Subject: [PATCH 06/86] Change advancement form mechanics to match Java behaviour, fix NPE (#5396) * Change advancement form mechanics to match Java behaviour, fix NPE * Remove unused import * Remove debug statement whoops * Fix another NPE * Close the form first when reopening it --- .../command/defaults/AdvancementsCommand.java | 2 +- .../session/cache/AdvancementsCache.java | 40 +++++++++++++++++-- .../JavaSelectAdvancementsTabTranslator.java | 5 +-- 3 files changed, 38 insertions(+), 9 deletions(-) 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 0cba28f33..e82c0b66b 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 @@ -42,6 +42,6 @@ public class AdvancementsCommand extends GeyserCommand { @Override public void execute(CommandContext context) { GeyserSession session = Objects.requireNonNull(context.sender().connection()); - session.getAdvancementsCache().buildAndShowMenuForm(); + session.getAdvancementsCache().buildAndShowForm(); } } 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 ac04bdf04..68b83225e 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 @@ -28,7 +28,6 @@ package org.geysermc.geyser.session.cache; import org.geysermc.mcprotocollib.protocol.data.game.advancement.Advancement; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSeenAdvancementsPacket; import lombok.Getter; -import lombok.Setter; import org.geysermc.cumulus.form.SimpleForm; import org.geysermc.geyser.level.GeyserAdvancement; import org.geysermc.geyser.session.GeyserSession; @@ -41,6 +40,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; public class AdvancementsCache { /** @@ -58,15 +58,39 @@ public class AdvancementsCache { /** * Stores player's chosen advancement's ID and title for use in form creators. */ - @Setter private String currentAdvancementCategoryId = null; + /** + * Stores if the player is currently viewing advancements. + */ + private boolean formOpen = false; + private final GeyserSession session; public AdvancementsCache(GeyserSession session) { this.session = session; } + public void setCurrentAdvancementCategoryId(String categoryId) { + if (!Objects.equals(currentAdvancementCategoryId, categoryId)) { + // Only open and show list form if we're going to a different category + currentAdvancementCategoryId = categoryId; + if (formOpen) { + session.closeForm(); + buildAndShowForm(); + formOpen = true; + } + } + } + + public void buildAndShowForm() { + if (currentAdvancementCategoryId == null) { + buildAndShowMenuForm(); + } else { + buildAndShowListForm(); + } + } + /** * Build and send a form with all advancement categories */ @@ -88,9 +112,11 @@ public class AdvancementsCache { builder.content("advancements.empty"); } - builder.validResultHandler((response) -> { + builder.closedResultHandler(() -> { + formOpen = false; + }).validResultHandler((response) -> { String id = rootAdvancementIds.get(response.clickedButtonId()); - if (!id.equals("")) { + if (!id.isEmpty()) { // Send a packet indicating that we are opening this particular advancement window ServerboundSeenAdvancementsPacket packet = new ServerboundSeenAdvancementsPacket(id); session.sendDownstreamGamePacket(packet); @@ -99,6 +125,7 @@ public class AdvancementsCache { } }); + formOpen = true; session.sendForm(builder); } @@ -133,6 +160,9 @@ public class AdvancementsCache { builder.closedResultHandler(() -> { // Indicate that we have closed the current advancement tab + // Don't set currentAdvancementCategoryId to null here, so that when the advancements form is shown again (buildAndShowForm), + // the tab that was last open is opened again, which matches Java behaviour + formOpen = false; session.sendDownstreamGamePacket(new ServerboundSeenAdvancementsPacket()); }).validResultHandler((response) -> { @@ -142,6 +172,7 @@ public class AdvancementsCache { } else { buildAndShowMenuForm(); // Indicate that we have closed the current advancement tab + currentAdvancementCategoryId = null; session.sendDownstreamGamePacket(new ServerboundSeenAdvancementsPacket()); } }); @@ -206,6 +237,7 @@ public class AdvancementsCache { .validResultHandler((response) -> buildAndShowListForm()) .closedResultHandler(() -> { // Indicate that we have closed the current advancement tab + formOpen = false; session.sendDownstreamGamePacket(new ServerboundSeenAdvancementsPacket()); }) ); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSelectAdvancementsTabTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSelectAdvancementsTabTranslator.java index 04c018472..9da4dec62 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSelectAdvancementsTabTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSelectAdvancementsTabTranslator.java @@ -27,7 +27,6 @@ package org.geysermc.geyser.translator.protocol.java; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundSelectAdvancementsTabPacket; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.cache.AdvancementsCache; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; @@ -39,8 +38,6 @@ public class JavaSelectAdvancementsTabTranslator extends PacketTranslator Date: Wed, 5 Mar 2025 22:02:09 +0100 Subject: [PATCH 07/86] Fix https://github.com/GeyserMC/Geyser/issues/5395, fix https://github.com/GeyserMC/Geyser/issues/5398 --- .../org/geysermc/geyser/impl/camera/GeyserCameraData.java | 7 ++++++- .../geyser/network/netty/handler/RakGeyserRateLimiter.java | 3 +-- .../translator/protocol/java/JavaLoginTranslator.java | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java index beb10db86..759828744 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java +++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java @@ -272,7 +272,12 @@ public class GeyserCameraData implements CameraData { elementSet.add(HUD_ELEMENT_VALUES[element.id()]); } - session.sendUpstreamPacket(packet); + if (session.isSentSpawnPacket()) { + session.sendUpstreamPacket(packet); + } else { + // Ensures hidden GUI elements properly hide when we spawn in the spectator gamemode + session.getUpstream().queuePostStartGamePacket(packet); + } } @Override diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakGeyserRateLimiter.java b/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakGeyserRateLimiter.java index 7ccf8fdfb..2e6f9be79 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakGeyserRateLimiter.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakGeyserRateLimiter.java @@ -44,7 +44,6 @@ public class RakGeyserRateLimiter extends RakServerRateLimiter { @Override protected int getAddressMaxPacketCount(InetAddress address) { - // The default packet limit is already padded, so we reduce it by 20% - return (int) (super.getAddressMaxPacketCount(address) * sessionManager.getAddressMultiplier(address) * 0.8); + return super.getAddressMaxPacketCount(address) * sessionManager.getAddressMultiplier(address); } } 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 5bb839a43..7a33c53d6 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 @@ -82,6 +82,7 @@ public class JavaLoginTranslator extends PacketTranslator Date: Wed, 12 Mar 2025 00:58:31 +0100 Subject: [PATCH 08/86] Fix https://github.com/GeyserMC/Geyser/issues/5405 --- .../org/geysermc/geyser/translator/item/ItemTranslator.java | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index f2213bd07..0c8c63f00 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 @@ -174,7 +174,7 @@ public final class ItemTranslator { // Translate item-specific components javaItem.translateComponentsToBedrock(session, components, nbtBuilder); - Rarity rarity = Rarity.fromId(components.get(DataComponentTypes.RARITY)); + Rarity rarity = Rarity.fromId(components.getOrDefault(DataComponentTypes.RARITY, 0)); String customName = getCustomName(session, customComponents, bedrockItem, rarity.getColor(), false, false); if (customName != null) { PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7a8d2824..a91ba0fbd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ protocol-common = "3.0.0.Beta6-20250212.131009-3" protocol-codec = "3.0.0.Beta6-20250212.131009-3" raknet = "1.0.0.CR3-20250218.160705-18" minecraftauth = "4.1.1" -mcprotocollib = "1.21.4-20250218.175633-22" +mcprotocollib = "1.21.4-20250311.232133-24" adventure = "4.14.0" adventure-platform = "4.3.0" junit = "5.9.2" From 32160c5c6426a07ffc7f0b9e80a76631e8b3a8ff Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 16 Mar 2025 01:28:46 +0100 Subject: [PATCH 09/86] Fix chunks not loading when riding a vehicle, fix world border corrections not applying (#5410) --- .../player/input/BedrockMovePlayer.java | 130 ++++++++++-------- 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java index ac5a62802..23132a28b 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java @@ -76,7 +76,7 @@ final class BedrockMovePlayer { boolean hasVehicle = entity.getVehicle() != null; // shouldSendPositionReminder also increments a tick counter, so make sure it's always called unless the player is on a vehicle. - boolean positionChanged = !hasVehicle && (session.getInputCache().shouldSendPositionReminder() || actualPositionChanged); + boolean positionChangedAndShouldUpdate = !hasVehicle && (session.getInputCache().shouldSendPositionReminder() || actualPositionChanged); boolean rotationChanged = hasVehicle || (entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw); if (session.getLookBackScheduledFuture() != null) { @@ -87,15 +87,23 @@ final class BedrockMovePlayer { } // Client is telling us it wants to move down, but something is blocking it from doing so. - boolean isOnGround = packet.getInputData().contains(PlayerAuthInputData.VERTICAL_COLLISION) && packet.getDelta().getY() < 0; + boolean isOnGround; + if (hasVehicle) { + // VERTICAL_COLLISION is not accurate while in a vehicle (as of 1.21.62) + isOnGround = Math.abs(packet.getDelta().getY()) < 0.1; + } else { + isOnGround = packet.getInputData().contains(PlayerAuthInputData.VERTICAL_COLLISION) && packet.getDelta().getY() < 0; + } + // This takes into account no movement sent from the client, but the player is trying to move anyway. // (Press into a wall in a corner - you're trying to move but nothing actually happens) + // This isn't sent when a player is riding a vehicle (as of 1.21.62) boolean horizontalCollision = packet.getInputData().contains(PlayerAuthInputData.HORIZONTAL_COLLISION); // If only the pitch and yaw changed // This isn't needed, but it makes the packets closer to vanilla // It also means you can't "lag back" while only looking, in theory - if (!positionChanged && rotationChanged) { + if (!positionChangedAndShouldUpdate && rotationChanged) { ServerboundMovePlayerRotPacket playerRotationPacket = new ServerboundMovePlayerRotPacket(isOnGround, horizontalCollision, yaw, pitch); entity.setYaw(yaw); @@ -103,71 +111,79 @@ final class BedrockMovePlayer { entity.setHeadYaw(headYaw); session.sendDownstreamGamePacket(playerRotationPacket); - } else if (positionChanged) { + + // Player position MUST be updated on our end, otherwise e.g. chunk loading breaks + if (hasVehicle) { + entity.setPositionManual(packet.getPosition()); + session.getSkullCache().updateVisibleSkulls(); + } + } else if (positionChangedAndShouldUpdate) { if (isValidMove(session, entity.getPosition(), packet.getPosition())) { - CollisionResult result = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), isOnGround, packet.getInputData().contains(PlayerAuthInputData.HANDLE_TELEPORT)); - if (result != null) { // A null return value cancels the packet - Vector3d position = result.correctedMovement(); - boolean isBelowVoid = entity.isVoidPositionDesynched(); + if (!session.getWorldBorder().isPassingIntoBorderBoundaries(entity.getPosition(), true)) { + CollisionResult result = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), isOnGround, packet.getInputData().contains(PlayerAuthInputData.HANDLE_TELEPORT)); + if (result != null) { // A null return value cancels the packet + Vector3d position = result.correctedMovement(); + boolean isBelowVoid = entity.isVoidPositionDesynched(); - boolean teleportThroughVoidFloor, mustResyncPosition; - // Compare positions here for void floor fix below before the player's position variable is set to the packet position - if (entity.getPosition().getY() >= packet.getPosition().getY() && !isBelowVoid) { - int floorY = position.getFloorY(); - int voidFloorLocation = entity.voidFloorPosition(); - teleportThroughVoidFloor = floorY <= (voidFloorLocation + 1) && floorY >= voidFloorLocation; - } else { - teleportThroughVoidFloor = false; - } + boolean teleportThroughVoidFloor, mustResyncPosition; + // Compare positions here for void floor fix below before the player's position variable is set to the packet position + if (entity.getPosition().getY() >= packet.getPosition().getY() && !isBelowVoid) { + int floorY = position.getFloorY(); + int voidFloorLocation = entity.voidFloorPosition(); + teleportThroughVoidFloor = floorY <= (voidFloorLocation + 1) && floorY >= voidFloorLocation; + } else { + teleportThroughVoidFloor = false; + } - if (teleportThroughVoidFloor || isBelowVoid) { - // https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground. - isOnGround = false; - } + if (teleportThroughVoidFloor || isBelowVoid) { + // https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground. + isOnGround = false; + } - if (isBelowVoid) { - int floorY = position.getFloorY(); - int voidFloorLocation = entity.voidFloorPosition(); - mustResyncPosition = floorY < voidFloorLocation && floorY >= voidFloorLocation - 1; - } else { - mustResyncPosition = false; - } + if (isBelowVoid) { + int floorY = position.getFloorY(); + int voidFloorLocation = entity.voidFloorPosition(); + mustResyncPosition = floorY < voidFloorLocation && floorY >= voidFloorLocation - 1; + } else { + mustResyncPosition = false; + } - double yPosition = position.getY(); - if (entity.isVoidPositionDesynched()) { // not using the cached variable on purpose - yPosition += 4; // We are de-synched since we had to teleport below the void floor. - } + double yPosition = position.getY(); + if (entity.isVoidPositionDesynched()) { // not using the cached variable on purpose + yPosition += 4; // We are de-synched since we had to teleport below the void floor. + } - Packet movePacket; - if (rotationChanged) { - // Send rotation updates as well - movePacket = new ServerboundMovePlayerPosRotPacket( + Packet movePacket; + if (rotationChanged) { + // Send rotation updates as well + movePacket = new ServerboundMovePlayerPosRotPacket( isOnGround, horizontalCollision, position.getX(), yPosition, position.getZ(), yaw, pitch - ); - entity.setYaw(yaw); - entity.setPitch(pitch); - entity.setHeadYaw(headYaw); - } else { - // Rotation did not change; don't send an update with rotation - movePacket = new ServerboundMovePlayerPosPacket(isOnGround, horizontalCollision, position.getX(), yPosition, position.getZ()); + ); + entity.setYaw(yaw); + entity.setPitch(pitch); + entity.setHeadYaw(headYaw); + } else { + // Rotation did not change; don't send an update with rotation + movePacket = new ServerboundMovePlayerPosPacket(isOnGround, horizontalCollision, position.getX(), yPosition, position.getZ()); + } + + entity.setPositionManual(packet.getPosition()); + + // Send final movement changes + session.sendDownstreamGamePacket(movePacket); + + if (teleportThroughVoidFloor) { + entity.teleportVoidFloorFix(false); + } else if (mustResyncPosition) { + entity.teleportVoidFloorFix(true); + } + + session.getInputCache().markPositionPacketSent(); + session.getSkullCache().updateVisibleSkulls(); } - - entity.setPositionManual(packet.getPosition()); - - // Send final movement changes - session.sendDownstreamGamePacket(movePacket); - - if (teleportThroughVoidFloor) { - entity.teleportVoidFloorFix(false); - } else if (mustResyncPosition) { - entity.teleportVoidFloorFix(true); - } - - session.getInputCache().markPositionPacketSent(); - session.getSkullCache().updateVisibleSkulls(); } } else { // Not a valid move From 512c68a88392e8f2354d790a1d30f6b00f4f5d34 Mon Sep 17 00:00:00 2001 From: Tim203 Date: Tue, 18 Mar 2025 22:02:29 +0100 Subject: [PATCH 10/86] The team should still be used when there is a score display name (#5415) --- .../display/score/SidebarDisplayScore.java | 8 +- .../network/ScoreboardIssueTests.java | 113 ++++++++++++++++-- 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/display/score/SidebarDisplayScore.java b/core/src/main/java/org/geysermc/geyser/scoreboard/display/score/SidebarDisplayScore.java index 42c0dbbf7..c6f4e9f43 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/display/score/SidebarDisplayScore.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/display/score/SidebarDisplayScore.java @@ -61,13 +61,15 @@ public final class SidebarDisplayScore extends DisplayScore { markUpdated(); String finalName = reference.name(); - String displayName = reference.displayName(); + String displayName = reference.displayName(); if (displayName != null) { finalName = displayName; - } else if (team != null) { + } + + if (team != null) { this.lastTeamUpdate = team.lastUpdate(); - finalName = team.displayName(reference.name()); + finalName = team.displayName(finalName); } NumberFormat numberFormat = reference.numberFormat(); diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java index 2239c9819..086d972dc 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java @@ -25,14 +25,29 @@ package org.geysermc.geyser.scoreboard.network; +import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket; +import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacketMatch; +import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacketType; +import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket; +import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import net.kyori.adventure.text.Component; +import org.cloudburstmc.protocol.bedrock.data.ScoreInfo; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket; import org.geysermc.geyser.entity.type.living.monster.EnderDragonPartEntity; import org.geysermc.geyser.session.cache.EntityCache; import org.geysermc.geyser.translator.protocol.java.entity.JavaRemoveEntitiesTranslator; @@ -40,7 +55,10 @@ import org.geysermc.geyser.translator.protocol.java.entity.JavaSetEntityDataTran import org.geysermc.geyser.translator.protocol.java.entity.player.JavaPlayerInfoUpdateTranslator; import org.geysermc.geyser.translator.protocol.java.entity.spawn.JavaAddEntityTranslator; import org.geysermc.geyser.translator.protocol.java.entity.spawn.JavaAddExperienceOrbTranslator; +import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator; +import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator; import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator; +import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator; import org.geysermc.mcprotocollib.auth.GameProfile; import org.geysermc.mcprotocollib.protocol.data.game.PlayerListEntry; import org.geysermc.mcprotocollib.protocol.data.game.PlayerListEntryAction; @@ -53,6 +71,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility; +import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction; +import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType; +import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundPlayerInfoUpdatePacket; @@ -60,18 +81,12 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.Clie import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityDataPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddExperienceOrbPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket; import org.junit.jupiter.api.Test; -import java.util.EnumSet; -import java.util.Optional; -import java.util.UUID; - -import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.*; -import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * Tests for issues reported on GitHub. */ @@ -95,7 +110,7 @@ public class ScoreboardIssueTests { String displayName = context.mockOrSpy(EntityCache.class).getEntityByJavaId(2).getDisplayName(); assertEquals("entity.minecraft.experience_orb", displayName); - context.translate(removeEntitiesTranslator, new ClientboundRemoveEntitiesPacket(new int[] { 2 })); + context.translate(removeEntitiesTranslator, new ClientboundRemoveEntitiesPacket(new int[]{2})); }); // we know that spawning and removing the entity should be fine @@ -153,7 +168,7 @@ public class ScoreboardIssueTests { playerInfoUpdateTranslator, new ClientboundPlayerInfoUpdatePacket( EnumSet.of(PlayerListEntryAction.ADD_PLAYER, PlayerListEntryAction.UPDATE_LISTED), - new PlayerListEntry[] { + new PlayerListEntry[]{ new PlayerListEntry(npcUuid, new GameProfile(npcUuid, "1297"), false, 0, GameMode.SURVIVAL, null, false, 0, null, 0, null, null) })); @@ -183,7 +198,7 @@ public class ScoreboardIssueTests { ); context.translate( setPlayerTeamTranslator, - new ClientboundSetPlayerTeamPacket("npc_team_1297", TeamAction.ADD_PLAYER, new String[]{ "1297" })); + new ClientboundSetPlayerTeamPacket("npc_team_1297", TeamAction.ADD_PLAYER, new String[]{"1297"})); context.translate(addEntityTranslator, new ClientboundAddEntityPacket(1297, npcUuid, EntityType.PLAYER, 1, 2, 3, 4, 5, 6)); // then it updates the displayed skin parts, which isn't relevant for us @@ -245,7 +260,7 @@ public class ScoreboardIssueTests { ); context.translate( setPlayerTeamTranslator, - new ClientboundSetPlayerTeamPacket("npc_team_1298", TeamAction.ADD_PLAYER, new String[]{ hologramUuid.toString() })); + new ClientboundSetPlayerTeamPacket("npc_team_1298", TeamAction.ADD_PLAYER, new String[]{hologramUuid.toString()})); assertNextPacket(context, () -> { var packet = new SetEntityDataPacket(); @@ -255,4 +270,76 @@ public class ScoreboardIssueTests { }); }); } + + /** + * Test for #5353. + * It follows a code snippet provided in the PR description. + */ + @Test + void prefixNotShowing() { + mockContextScoreboard(context -> { + var setObjectiveTranslator = new JavaSetObjectiveTranslator(); + var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator(); + var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator(); + var setScoreTranslator = new JavaSetScoreTranslator(); + + context.translate( + setObjectiveTranslator, + new ClientboundSetObjectivePacket( + "sb-0", + ObjectiveAction.ADD, + Component.text("Test Scoreboard"), + ScoreType.INTEGER, + null + ) + ); + assertNoNextPacket(context); + + context.translate( + setDisplayObjectiveTranslator, + new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "sb-0") + ); + assertNextPacket(context, () -> { + var packet = new SetDisplayObjectivePacket(); + packet.setObjectiveId("0"); + packet.setDisplayName("Test Scoreboard"); + packet.setCriteria("dummy"); + packet.setDisplaySlot("sidebar"); + packet.setSortOrder(1); + return packet; + }); + + context.translate( + setPlayerTeamTranslator, + new ClientboundSetPlayerTeamPacket( + "sbt-1", + Component.text("displaynametest"), + Component.text("§aScore: 10"), + Component.empty(), + false, + false, + NameTagVisibility.NEVER, + CollisionRule.NEVER, + TeamColor.DARK_GREEN, + new String[]{"§0"}) + ); + assertNoNextPacket(context); + + context.translate( + setScoreTranslator, + new ClientboundSetScorePacket( + "§0", + "sb-0", + 10 + ).withDisplay(Component.empty()) + ); + assertNextPacket(context, () -> { + var packet = new SetScorePacket(); + packet.setAction(SetScorePacket.Action.SET); + packet.setInfos(List.of(new ScoreInfo(1, "0", 10, "§2§aScore: 10§r§2§r§2"))); + return packet; + }); + assertNoNextPacket(context); + }); + } } From 69329f2cad5002906f5ccda41b730e9c031836e7 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 25 Mar 2025 15:49:18 +0100 Subject: [PATCH 11/86] Feature: Resource Pack API additions - ResourcePackOptions and a GeyserDefineResourcePacksEvent (#4978) --- .../api/event/bedrock/SessionJoinEvent.java | 1 + .../SessionLoadResourcePacksEvent.java | 73 ++++- .../GeyserDefineResourcePacksEvent.java | 104 +++++++ .../GeyserLoadResourcePacksEvent.java | 5 +- .../event/lifecycle/GeyserPreReloadEvent.java | 2 +- .../geysermc/geyser/api/pack/PackCodec.java | 42 ++- .../geyser/api/pack/PathPackCodec.java | 4 +- .../geyser/api/pack/ResourcePack.java | 72 +++++ .../geyser/api/pack/ResourcePackManifest.java | 162 +++++++++- .../geyser/api/pack/UrlPackCodec.java | 51 ++++ .../pack/exception/ResourcePackException.java | 97 ++++++ .../api/pack/option/PriorityOption.java | 59 ++++ .../api/pack/option/ResourcePackOption.java | 76 +++++ .../geyser/api/pack/option/SubpackOption.java | 71 +++++ .../api/pack/option/UrlFallbackOption.java | 56 ++++ .../bungeecord/GeyserBungeeLogger.java | 7 + .../geyser/platform/mod/GeyserModLogger.java | 7 + .../platform/spigot/GeyserSpigotLogger.java | 7 + .../standalone/GeyserStandaloneLogger.java | 5 + .../velocity/GeyserVelocityLogger.java | 9 +- .../viaproxy/GeyserViaProxyLogger.java | 7 + .../java/org/geysermc/geyser/GeyserImpl.java | 5 +- .../org/geysermc/geyser/GeyserLogger.java | 9 + .../GeyserDefineResourcePacksEventImpl.java | 123 ++++++++ .../SessionLoadResourcePacksEventImpl.java | 177 ++++++++++- .../geyser/network/UpstreamPacketHandler.java | 94 ++++-- .../geyser/pack/GeyserResourcePack.java | 52 +++- .../pack/GeyserResourcePackManifest.java | 32 +- .../geyser/pack/ResourcePackHolder.java | 45 +++ .../pack/option/GeyserPriorityOption.java | 55 ++++ .../pack/option/GeyserSubpackOption.java | 66 ++++ .../pack/option/GeyserUrlFallbackOption.java | 53 ++++ .../geyser/pack/option/OptionHolder.java | 135 +++++++++ .../geyser/pack/path/GeyserPathPackCodec.java | 9 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 96 ++++++ .../geysermc/geyser/registry/Registries.java | 6 +- .../loader/ProviderRegistryLoader.java | 22 +- .../registry/loader/ResourcePackLoader.java | 285 ++++++++++++++++-- .../geyser/scoreboard/Scoreboard.java | 34 +-- .../geyser/session/GeyserSessionAdapter.java | 4 +- .../geysermc/geyser/skin/SkinProvider.java | 2 +- .../org/geysermc/geyser/util/WebUtils.java | 119 +++++++- .../loader/ResourcePackLoaderTest.java | 4 +- .../network/util/EmptyGeyserLogger.java | 5 + gradle.properties | 2 +- 45 files changed, 2213 insertions(+), 138 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java create mode 100644 core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/option/GeyserPriorityOption.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/option/GeyserSubpackOption.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/option/GeyserUrlFallbackOption.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/option/OptionHolder.java create mode 100644 core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java index ab2088c00..0228214a7 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionJoinEvent.java @@ -31,6 +31,7 @@ import org.geysermc.geyser.api.event.connection.ConnectionEvent; /** * Called when Geyser session connected to a Java remote server and is in a play-ready state. + * @since 2.1.1 */ public final class SessionJoinEvent extends ConnectionEvent { public SessionJoinEvent(@NonNull GeyserConnection connection) { diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java index c2f1cd427..edce76f6a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoadResourcePacksEvent.java @@ -26,15 +26,20 @@ package org.geysermc.geyser.api.event.bedrock; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.connection.GeyserConnection; import org.geysermc.geyser.api.event.connection.ConnectionEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import java.util.Collection; import java.util.List; import java.util.UUID; /** - * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending resource packs. + * Called when Geyser initializes a session for a new Bedrock client and is in the process of sending {@link ResourcePack}'s. + * @since 2.1.1 */ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { public SessionLoadResourcePacksEvent(@NonNull GeyserConnection connection) { @@ -42,26 +47,70 @@ public abstract class SessionLoadResourcePacksEvent extends ConnectionEvent { } /** - * Gets an unmodifiable list of {@link ResourcePack}s that will be sent to the client. + * Gets the {@link ResourcePack}'s that will be sent to this {@link GeyserConnection}. + * To remove packs, use {@link #unregister(UUID)}, as the list returned + * by this method is unmodifiable. * - * @return an unmodifiable list of resource packs that will be sent to the client. + * @return an unmodifiable list of {@link ResourcePack}'s + * @since 2.1.1 */ public abstract @NonNull List resourcePacks(); /** - * Registers a {@link ResourcePack} to be sent to the client. - * - * @param resourcePack a resource pack that will be sent to the client. - * @return true if the resource pack was added successfully, - * or false if already present + * @deprecated Use {{@link #register(ResourcePack, ResourcePackOption[])}} instead */ - public abstract boolean register(@NonNull ResourcePack resourcePack); + @Deprecated + public abstract boolean register(@NonNull ResourcePack pack); /** - * Unregisters a resource pack from being sent to the client. + * Registers a {@link ResourcePack} to be sent to the client, optionally alongside + * specific {@link ResourcePackOption}'s specifying how it will be applied by the client. * - * @param uuid the UUID of the resource pack - * @return true whether the resource pack was removed from the list of resource packs. + * @param pack the {@link ResourcePack} that will be sent to the client + * @param options {@link ResourcePackOption}'s that specify how the client loads the pack + * @throws ResourcePackException if an issue occurred during pack registration + * @since 2.6.2 + */ + public abstract void register(@NonNull ResourcePack pack, @Nullable ResourcePackOption... options); + + /** + * Sets {@link ResourcePackOption}'s for a {@link ResourcePack}. + * This method can also be used to override options for resource packs already registered in the + * {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent}. + * + * @param uuid the uuid of the resource pack to register the options for + * @param options the {@link ResourcePackOption}'s to register for the resource pack + * @throws ResourcePackException if an issue occurred during {@link ResourcePackOption} registration + * @since 2.6.2 + */ + public abstract void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options); + + /** + * Returns a collection of {@link ResourcePackOption}'s for a registered {@link ResourcePack}. + * The collection returned here is not modifiable. + * + * @param uuid the {@link ResourcePack} for which the options are set + * @return a collection of {@link ResourcePackOption}'s + * @throws ResourcePackException if the pack was not registered + * @since 2.6.2 + */ + public abstract Collection> options(@NonNull UUID uuid); + + /** + * Returns the current {@link ResourcePackOption}, or null, for a given {@link ResourcePackOption.Type}. + * + * @param uuid the {@link ResourcePack} for which to query this option type + * @param type the {@link ResourcePackOption.Type} of the option to query + * @throws ResourcePackException if the queried option is invalid or not present on the resource pack + * @since 2.6.2 + */ + public abstract @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type); + + /** + * Unregisters a {@link ResourcePack} from the list of packs sent to this {@link GeyserConnection}. + * + * @param uuid the UUID of the {@link ResourcePack} to be removed + * @since 2.1.1 */ public abstract boolean unregister(@NonNull UUID uuid); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java new file mode 100644 index 000000000..4128f1c47 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineResourcePacksEvent.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2025 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.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Called when {@link ResourcePack}'s are loaded within Geyser. + * @since 2.6.2 + */ +public abstract class GeyserDefineResourcePacksEvent implements Event { + + /** + * Gets the {@link ResourcePack}'s that will be sent to connecting Bedrock clients. + * To remove packs, use {@link #unregister(UUID)}, as the list returned + * by this method is unmodifiable. + * + * @return an unmodifiable list of {@link ResourcePack}'s + * @since 2.6.2 + */ + public abstract @NonNull List resourcePacks(); + + /** + * Registers a {@link ResourcePack} to be sent to the client, optionally alongside + * {@link ResourcePackOption}'s specifying how it will be applied on clients. + * + * @param pack a resource pack that will be sent to the client + * @param options {@link ResourcePackOption}'s that specify how clients load the pack + * @throws ResourcePackException if an issue occurred during pack registration + * @since 2.6.2 + */ + public abstract void register(@NonNull ResourcePack pack, @Nullable ResourcePackOption... options); + + /** + * Sets {@link ResourcePackOption}'s for a {@link ResourcePack}. + * + * @param uuid the uuid of the resource pack to register the options for + * @param options the {@link ResourcePackOption}'s to register for the resource pack + * @throws ResourcePackException if an issue occurred during {@link ResourcePackOption} registration + * @since 2.6.2 + */ + public abstract void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options); + + /** + * Returns a collection of {@link ResourcePackOption}'s for a registered {@link ResourcePack}. + * The collection returned here is not modifiable. + * + * @param uuid the uuid of the {@link ResourcePack} for which the options are set + * @return a collection of {@link ResourcePackOption}'s + * @throws ResourcePackException if the pack was not registered + * @since 2.6.2 + */ + public abstract Collection> options(@NonNull UUID uuid); + + /** + * Returns the current option, or null, for a given {@link ResourcePackOption.Type}. + * + * @param uuid the {@link ResourcePack} for which to query this option type + * @param type the {@link ResourcePackOption.Type} of the option to query + * @throws ResourcePackException if the queried option is invalid or not present on the resource pack + * @since 2.6.2 + */ + public abstract @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type); + + /** + * Unregisters a {@link ResourcePack} from the list of packs sent to connecting Bedrock clients. + * + * @param uuid the UUID of the {@link ResourcePack} to be removed + * @since 2.6.2 + */ + public abstract void unregister(@NonNull UUID uuid); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java index e9b283ecb..047f9df57 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserLoadResourcePacksEvent.java @@ -32,9 +32,8 @@ import java.nio.file.Path; import java.util.List; /** - * Called when resource packs are loaded within Geyser. - * - * @param resourcePacks a mutable list of the currently listed resource packs + * @deprecated Use the {@link GeyserDefineResourcePacksEvent} instead. */ +@Deprecated public record GeyserLoadResourcePacksEvent(@NonNull List resourcePacks) implements Event { } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java index 16d5058da..9147ad4b8 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java @@ -33,7 +33,7 @@ import org.geysermc.geyser.api.extension.ExtensionManager; /** * Called when Geyser is about to reload. Primarily aimed at extensions, so they can decide on their own what to reload. - * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserLoadResourcePacksEvent}. + * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserDefineResourcePacksEvent}. * * @param extensionManager the extension manager * @param eventBus the event bus diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index 884129fa3..c83fce4c4 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -35,6 +35,7 @@ import java.nio.file.Path; /** * Represents a pack codec that can be used * to provide resource packs to clients. + * @since 2.1.1 */ public abstract class PackCodec { @@ -42,6 +43,7 @@ public abstract class PackCodec { * Gets the sha256 hash of the resource pack. * * @return the hash of the resource pack + * @since 2.1.1 */ public abstract byte @NonNull [] sha256(); @@ -49,34 +51,66 @@ public abstract class PackCodec { * Gets the resource pack size. * * @return the resource pack file size + * @since 2.1.1 */ public abstract long size(); /** - * Serializes the given resource pack into a byte buffer. + * @deprecated use {@link #serialize()} instead. + */ + @Deprecated + @NonNull + public SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + return serialize(); + }; + + /** + * Serializes the given codec into a byte buffer. * - * @param resourcePack the resource pack to serialize * @return the serialized resource pack + * @since 2.6.2 */ @NonNull - public abstract SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException; + public abstract SeekableByteChannel serialize() throws IOException; /** * Creates a new resource pack from this codec. * * @return the new resource pack + * @since 2.1.1 */ @NonNull protected abstract ResourcePack create(); + /** + * Creates a new resource pack builder from this codec. + * + * @return the new resource pack builder + * @since 2.6.2 + */ + protected abstract ResourcePack.@NonNull Builder createBuilder(); + /** * Creates a new pack provider from the given path. * * @param path the path to create the pack provider from * @return the new pack provider + * @since 2.1.1 */ @NonNull - public static PackCodec path(@NonNull Path path) { + public static PathPackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } + + /** + * Creates a new pack provider from the given url. + * + * @param url the url to create the pack provider from + * @return the new pack provider + * @since 2.6.2 + */ + @NonNull + public static UrlPackCodec url(@NonNull String url) { + return GeyserApi.api().provider(UrlPackCodec.class, url); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java index a3770451a..d6d668fb2 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java @@ -32,6 +32,7 @@ import java.nio.file.Path; /** * Represents a pack codec that creates a resource * pack from a path on the filesystem. + * @since 2.1.1 */ public abstract class PathPackCodec extends PackCodec { @@ -39,7 +40,8 @@ public abstract class PathPackCodec extends PackCodec { * Gets the path of the resource pack. * * @return the path of the resource pack + * @since 2.1.1 */ @NonNull public abstract Path path(); -} \ No newline at end of file +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java index de1beaf65..19b579f26 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePack.java @@ -26,12 +26,17 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.UUID; /** * Represents a resource pack sent to Bedrock clients *

* This representation of a resource pack only contains what * Geyser requires to send it to the client. + * @since 2.1.1 */ public interface ResourcePack { @@ -39,6 +44,7 @@ public interface ResourcePack { * The {@link PackCodec codec} for this pack. * * @return the codec for this pack + * @since 2.1.1 */ @NonNull PackCodec codec(); @@ -47,6 +53,7 @@ public interface ResourcePack { * Gets the resource pack manifest. * * @return the resource pack manifest + * @since 2.1.1 */ @NonNull ResourcePackManifest manifest(); @@ -55,18 +62,83 @@ public interface ResourcePack { * Gets the content key of the resource pack. Lack of a content key is represented by an empty String. * * @return the content key of the resource pack + * @since 2.1.1 */ @NonNull String contentKey(); + /** + * Shortcut for getting the UUID from the {@link ResourcePackManifest}. + * + * @return the resource pack uuid + * @since 2.6.2 + */ + @NonNull + default UUID uuid() { + return manifest().header().uuid(); + } + /** * Creates a resource pack with the given {@link PackCodec}. * * @param codec the pack codec * @return the resource pack + * @since 2.1.1 */ @NonNull static ResourcePack create(@NonNull PackCodec codec) { return codec.create(); } + + /** + * Returns a {@link Builder} for a resource pack. + * It can be used to set a content key. + * + * @param codec the {@link PackCodec} to base the builder on + * @return a {@link Builder} to build a resource pack + * @since 2.6.2 + */ + static Builder builder(@NonNull PackCodec codec) { + return GeyserApi.api().provider(Builder.class, codec); + } + + /** + * A builder for a resource pack. It allows providing a content key manually. + * @since 2.6.2 + */ + interface Builder { + + /** + * @return the {@link ResourcePackManifest} of this resource pack + * @since 2.6.2 + */ + ResourcePackManifest manifest(); + + /** + * @return the {@link PackCodec} of this resource pack + * @since 2.6.2 + */ + PackCodec codec(); + + /** + * @return the current content key, or an empty string if not set + * @since 2.6.2 + */ + String contentKey(); + + /** + * Sets a content key for this resource pack. + * + * @param contentKey the content key + * @return this builder + * @since 2.6.2 + */ + @This Builder contentKey(@NonNull String contentKey); + + /** + * @return the resource pack + * @since 2.6.2 + */ + ResourcePack build(); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java index c9ccdd6c5..737682e22 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/ResourcePackManifest.java @@ -26,55 +26,99 @@ package org.geysermc.geyser.api.pack; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.option.SubpackOption; import java.util.Collection; import java.util.UUID; /** - * Represents a resource pack manifest. + * Represents a Bedrock edition resource pack manifest (manifest.json). + * All resource packs are required to have such a file as it identifies the resource pack. + * See + * Microsoft's docs for more info. + * @since 2.1.1 */ public interface ResourcePackManifest { /** * Gets the format version of the resource pack. + *

+ * "1" is used for skin packs, + * "2" is used for resource and behavior packs, and world templates. * * @return the format version + * @since 2.1.1 */ int formatVersion(); /** - * Gets the header of the resource pack. + * Gets the {@link Header} of the resource pack. * - * @return the header + * @return the {@link Header} + * @since 2.1.1 */ @NonNull Header header(); /** - * Gets the modules of the resource pack. + * Gets the {@link Module}'s of the resource pack. * - * @return the modules + * @return a collection of modules + * @since 2.1.1 */ @NonNull Collection modules(); /** - * Gets the dependencies of the resource pack. + * Gets the {@link Dependency}'s of the resource pack. * - * @return the dependencies + * @return a collection of dependencies + * @since 2.6.2 */ @NonNull Collection dependencies(); + /** + * Gets the {@link Subpack}'s of the resource pack. + * See Microsoft's docs on subpacks + * for more information. + * + * @return a collection of subpacks + * @since 2.6.2 + */ + @NonNull + Collection subpacks(); + + /** + * Gets the {@link Setting}'s of the resource pack. + * These are shown to Bedrock client's in the resource pack settings menu (see here) + * to inform users about what the resource pack and sub-packs include. + * + * @return a collection of settings + * @since 2.6.2 + */ + @NonNull + Collection settings(); + /** * Represents the header of a resource pack. + * It contains the main information about the resource pack, such as + * the name, description, or uuid. + * See + * Microsoft's docs for further details on headers. + * @since 2.1.1 */ interface Header { /** - * Gets the UUID of the resource pack. + * Gets the UUID of the resource pack. It is a unique identifier that differentiates this resource pack from any other resource pack. + * Bedrock clients will cache resource packs, and download resource packs when the uuid is new (or the version changes). * * @return the UUID + * @since 2.1.1 */ @NonNull UUID uuid(); @@ -83,6 +127,7 @@ public interface ResourcePackManifest { * Gets the version of the resource pack. * * @return the version + * @since 2.1.1 */ @NonNull Version version(); @@ -91,6 +136,7 @@ public interface ResourcePackManifest { * Gets the name of the resource pack. * * @return the name + * @since 2.1.1 */ @NonNull String name(); @@ -99,6 +145,7 @@ public interface ResourcePackManifest { * Gets the description of the resource pack. * * @return the description + * @since 2.1.1 */ @NonNull String description(); @@ -107,6 +154,7 @@ public interface ResourcePackManifest { * Gets the minimum supported Minecraft version of the resource pack. * * @return the minimum supported Minecraft version + * @since 2.1.1 */ @NonNull Version minimumSupportedMinecraftVersion(); @@ -114,21 +162,29 @@ public interface ResourcePackManifest { /** * Represents a module of a resource pack. + * It contains information about the content type that is + * offered by this resource pack. + * See + * Microsoft's docs for further details on modules. + * @since 2.1.1 */ interface Module { /** * Gets the UUID of the module. + * This should usually be different from the UUID in the {@link Header}. * * @return the UUID + * @since 2.1.1 */ @NonNull UUID uuid(); /** - * Gets the version of the module. + * Gets the {@link Version} of the module. * - * @return the version + * @return the {@link Version} + * @since 2.1.1 */ @NonNull Version version(); @@ -137,6 +193,7 @@ public interface ResourcePackManifest { * Gets the type of the module. * * @return the type + * @since 2.1.1 */ @NonNull String type(); @@ -145,6 +202,7 @@ public interface ResourcePackManifest { * Gets the description of the module. * * @return the description + * @since 2.1.1 */ @NonNull String description(); @@ -152,28 +210,102 @@ public interface ResourcePackManifest { /** * Represents a dependency of a resource pack. + * These are references to other resource packs that must be + * present in order for this resource pack to apply. + * See + * Microsoft's docs for further details on dependencies. + * @since 2.1.1 */ interface Dependency { /** - * Gets the UUID of the dependency. + * Gets the UUID of the resource pack dependency. * * @return the uuid + * @since 2.1.1 */ @NonNull UUID uuid(); /** - * Gets the version of the dependency. + * Gets the {@link Version} of the dependency. * - * @return the version + * @return the {@link Version} + * @since 2.1.1 */ @NonNull Version version(); } + /** + * Represents a subpack of a resource pack. These are often used for "variants" of the resource pack, + * such as lesser details, or additional features either to be determined by player's taste or adapted to the player device's performance. + * See Micoroft's docs for more information. + */ + interface Subpack { + + /** + * Gets the folder name where this sub-pack is placed in. + * + * @return the folder name + * @since 2.6.2 + */ + @NonNull + String folderName(); + + /** + * Gets the name of this subpack. Required for each subpack to be valid. + * To make a Bedrock client load any subpack, register the resource pack + * in the {@link SessionLoadResourcePacksEvent} or {@link GeyserDefineResourcePacksEvent} and specify a + * {@link SubpackOption} with the name of the subpack to load. + * + * @return the subpack name + * @since 2.6.2 + */ + @NonNull + String name(); + + /** + * Gets the memory tier of this Subpack, representing how much RAM a device must have to run it. + * Each memory tier requires 0.25 GB of RAM. For example, a memory tier of 0 is no requirement, + * and a memory tier of 4 requires 1GB of RAM. + * + * @return the memory tier + * @since 2.6.2 + */ + @Nullable + Float memoryTier(); + } + + /** + * Represents a setting that is shown client-side that describe what a pack does. + * Multiple setting entries are shown in separate paragraphs. + * @since 2.6.2 + */ + interface Setting { + + /** + * The type of the setting. Usually just "label". + * + * @return the type + * @since 2.6.2 + */ + @NonNull + String type(); + + /** + * The text shown for the setting. + * + * @return the text content + * @since 2.6.2 + */ + @NonNull + String text(); + } + /** * Represents a version of a resource pack. + * @since 2.1.1 */ interface Version { @@ -181,6 +313,7 @@ public interface ResourcePackManifest { * Gets the major version. * * @return the major version + * @since 2.1.1 */ int major(); @@ -188,6 +321,7 @@ public interface ResourcePackManifest { * Gets the minor version. * * @return the minor version + * @since 2.1.1 */ int minor(); @@ -195,6 +329,7 @@ public interface ResourcePackManifest { * Gets the patch version. * * @return the patch version + * @since 2.1.1 */ int patch(); @@ -202,6 +337,7 @@ public interface ResourcePackManifest { * Gets the version formatted as a String. * * @return the version string + * @since 2.1.1 */ @NonNull String toString(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.java new file mode 100644 index 000000000..e729e3fbb --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/UrlPackCodec.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.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a pack codec that creates a resource + * pack from a URL. + *

+ * Due to Bedrock limitations, the URL must: + *

    + *
  • be a direct download link to a .zip or .mcpack resource pack
  • + *
  • use the application type `application/zip` and set a correct content length
  • + *
+ * @since 2.6.2 + */ +public abstract class UrlPackCodec extends PackCodec { + + /** + * Gets the URL to the resource pack location. + * + * @return the URL of the resource pack + * @since 2.6.2 + */ + @NonNull + public abstract String url(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java b/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java new file mode 100644 index 000000000..66b3924d5 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/exception/ResourcePackException.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 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.pack.exception; + +import java.io.Serial; + +/** + * Used to indicate an exception that occurred while handling resource pack registration, + * or during resource pack option validation. + * @since 2.6.2 + */ +public class ResourcePackException extends IllegalArgumentException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The {@link Cause} of this exception. + */ + private final Cause cause; + + /** + * @param cause the cause of this exception + * @since 2.6.2 + */ + public ResourcePackException(Cause cause) { + super(cause.message()); + this.cause = cause; + } + + /** + * @param cause the cause of this exception + * @param message an additional, more in-depth message about the issue. + * @since 2.6.2 + */ + public ResourcePackException(Cause cause, String message) { + super(message); + this.cause = cause; + } + + /** + * @return the cause of this exception + * @since 2.6.2 + */ + public Cause cause() { + return cause; + } + + /** + * Represents different causes with explanatory messages stating which issue occurred. + * @since 2.6.2 + */ + public enum Cause { + DUPLICATE("A resource pack with this UUID was already registered!"), + INVALID_PACK("This resource pack is not a valid Bedrock edition resource pack!"), + INVALID_PACK_OPTION("Attempted to register an invalid resource pack option!"), + PACK_NOT_FOUND("No resource pack was found!"), + UNKNOWN_IMPLEMENTATION("Use the resource pack codecs to create resource packs."); + + private final String message; + + /** + * @return the message of this cause + * @since 2.6.2 + */ + public String message() { + return message; + } + + Cause(String message) { + this.message = message; + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java new file mode 100644 index 000000000..f1f91e9a9 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/PriorityOption.java @@ -0,0 +1,59 @@ +/* + * 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.pack.option; + +import org.geysermc.geyser.api.GeyserApi; + +/** + * Allows specifying a pack priority that decides the order on how packs are sent to the client. + * If two resource packs modify the same texture - for example if one removes the pumpkin overlay and + * the other is just making it translucent, one of the packs will override the other. + * Specifically, the pack with the higher priority will override the pack changes of the lower priority. + * @since 2.6.2 + */ +public interface PriorityOption extends ResourcePackOption { + + PriorityOption HIGHEST = PriorityOption.priority(100); + PriorityOption HIGH = PriorityOption.priority(50); + PriorityOption NORMAL = PriorityOption.priority(0); + PriorityOption LOW = PriorityOption.priority(-50); + PriorityOption LOWEST = PriorityOption.priority(-100); + + /** + * Constructs a priority option based on a value between 0 and 10. + * The higher the number, the higher will this pack appear in the resource pack stack. + * + * @param priority an integer that is above 0, but smaller than 10 + * @return the priority option + * @since 2.6.2 + */ + static PriorityOption priority(int priority) { + if (priority < -100 || priority > 100) { + throw new IllegalArgumentException("Priority must be between 0 and 10 inclusive!"); + } + return GeyserApi.api().provider(PriorityOption.class, priority); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java new file mode 100644 index 000000000..a86ad88db --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/ResourcePackOption.java @@ -0,0 +1,76 @@ +/* + * 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.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; + +/** + * Represents a resource pack option that can be used to specify how a resource + * pack is sent to Bedrock clients. + *

+ * Not all options can be applied to all resource packs. For example, you cannot specify + * a specific subpack to be loaded on resource packs that do not have subpacks. + * To see which limitations apply to specific resource pack options, check the javadocs + * or see the {@link #validate(ResourcePack)} method. + * @since 2.6.2 + */ +public interface ResourcePackOption { + + /** + * @return the option type + * @since 2.6.2 + */ + @NonNull Type type(); + + /** + * @return the value of the option + * @since 2.6.2 + */ + @NonNull T value(); + + /** + * Used to validate a specific options for a pack. + * Some options are not applicable to some packs. + * + * @param pack the resource pack to validate the option for + * @throws ResourcePackException with the {@link ResourcePackException.Cause#INVALID_PACK_OPTION} cause + * @since 2.6.2 + */ + void validate(@NonNull ResourcePack pack); + + /** + * Represents the different types of resource pack options. + * @since 2.6.2 + */ + enum Type { + SUBPACK, + PRIORITY, + FALLBACK + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java new file mode 100644 index 000000000..a0462afa7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/SubpackOption.java @@ -0,0 +1,71 @@ +/* + * 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.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.ResourcePackManifest; + +/** + * Can be used to specify which subpack from a resource pack a player should load. + * Available subpacks can be seen in a resource pack manifest {@link ResourcePackManifest#subpacks()}. + * @since 2.6.2 + */ +public interface SubpackOption extends ResourcePackOption { + + /** + * Creates a subpack option based on a {@link ResourcePackManifest.Subpack}. + * + * @param subpack the chosen subpack + * @return a subpack option specifying that subpack + * @since 2.6.2 + */ + static SubpackOption subpack(ResourcePackManifest.@NonNull Subpack subpack) { + return named(subpack.name()); + } + + /** + * Creates a subpack option based on a subpack name. + * + * @param subpackName the name of the subpack + * @return a subpack option specifying a subpack with that name + * @since 2.6.2 + */ + static SubpackOption named(@NonNull String subpackName) { + return GeyserApi.api().provider(SubpackOption.class, subpackName); + } + + /** + * Creates a subpack option with no subpack specified. + * + * @return a subpack option specifying no subpack + * @since 2.6.2 + */ + static SubpackOption empty() { + return GeyserApi.api().provider(SubpackOption.class, ""); + } + +} diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java b/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java new file mode 100644 index 000000000..9431c8cdd --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/pack/option/UrlFallbackOption.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.pack.option; + +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.pack.PathPackCodec; +import org.geysermc.geyser.api.pack.UrlPackCodec; + +/** + * Can be used for resource packs created with the {@link UrlPackCodec}. + * When a Bedrock client is unable to download a resource pack from a URL, Geyser will, by default, + * serve the resource pack over raknet (as packs are served with the {@link PathPackCodec}). + * This option can be used to disable that behavior, and disconnect the player instead. + * By default, the {@link UrlFallbackOption#TRUE} option is set. + * @since 2.6.2 + */ +public interface UrlFallbackOption extends ResourcePackOption { + + UrlFallbackOption TRUE = fallback(true); + UrlFallbackOption FALSE = fallback(false); + + /** + * Whether to fall back to serving packs over the raknet connection + * + * @param fallback whether to fall back + * @return a UrlFallbackOption with the specified behavior + * @since 2.6.2 + */ + static UrlFallbackOption fallback(boolean fallback) { + return GeyserApi.api().provider(UrlFallbackOption.class, fallback); + } + +} diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java index e8cf7ee39..e9b18acca 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java @@ -75,4 +75,11 @@ public class GeyserBungeeLogger implements GeyserLogger { info(message); } } + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + info(String.format(message, arguments)); + } + } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java index 9260288d7..9903d0d2e 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java @@ -84,6 +84,13 @@ public class GeyserModLogger implements GeyserLogger { } } + @Override + public void debug(String message, Object... arguments) { + if (debug) { + logger.info(message, arguments); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java index 5c6101eae..231255fec 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java @@ -75,4 +75,11 @@ public class GeyserSpigotLogger implements GeyserLogger { info(message); } } + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + info(String.format(message, arguments)); + } + } } 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 9c744f015..510ac0e79 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 @@ -115,6 +115,11 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey log.debug(ChatColor.GRAY + message); } + @Override + public void debug(String message, Object... arguments) { + log.debug(ChatColor.GRAY + message, arguments); + } + @Override public void setDebug(boolean debug) { Configurator.setLevel(log.getName(), debug ? Level.DEBUG : Level.INFO); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java index 4d10e4daf..5155d8958 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java @@ -73,4 +73,11 @@ public class GeyserVelocityLogger implements GeyserLogger { info(message); } } -} \ No newline at end of file + + @Override + public void debug(String message, Object... arguments) { + if (debug) { + logger.info(message, arguments); + } + } +} diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java index 10f414b51..fdcfe2279 100644 --- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java +++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java @@ -75,6 +75,13 @@ public class GeyserViaProxyLogger implements GeyserLogger, GeyserCommandSource { } } + @Override + public void debug(String message, Object... arguments) { + if (this.debug) { + this.debug(String.format(message, arguments)); + } + } + @Override public void setDebug(boolean debug) { this.debug = debug; diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index e81d528aa..c7b8ff031 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -80,6 +80,7 @@ import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.netty.GeyserServer; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.session.GeyserSession; @@ -677,9 +678,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { runIfNonNull(newsHandler, NewsHandler::shutdown); runIfNonNull(erosionUnixListener, UnixSocketClientListener::close); - if (Registries.RESOURCE_PACKS.loaded()) { - Registries.RESOURCE_PACKS.get().clear(); - } + ResourcePackLoader.clear(); this.setEnabled(false); } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java index f408de29c..92b50751a 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java @@ -103,6 +103,15 @@ public interface GeyserLogger extends GeyserCommandSource { debug(String.valueOf(object)); } + /** + * Logs and formats a message to console if debug mode is enabled, + * with the provided arguments. + * + * @param message the message to log + * @param arguments the arguments to replace in the message + */ + void debug(String message, Object... arguments); + /** * Sets if the logger should print debug messages * diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java new file mode 100644 index 000000000..2e62a4482 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineResourcePacksEventImpl.java @@ -0,0 +1,123 @@ +/* + * 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.event.type; + +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +@Getter +public class GeyserDefineResourcePacksEventImpl extends GeyserDefineResourcePacksEvent { + private final Map packs; + + public GeyserDefineResourcePacksEventImpl(Map packMap) { + this.packs = packMap; + } + + @Override + public @NonNull List resourcePacks() { + return packs.values().stream().map(ResourcePackHolder::resourcePack).toList(); + } + + @Override + public void register(@NonNull ResourcePack resourcePack, @Nullable ResourcePackOption... options) { + Objects.requireNonNull(resourcePack, "resource pack must not be null!"); + if (!(resourcePack instanceof GeyserResourcePack pack)) { + throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION); + } + + UUID uuid = resourcePack.uuid(); + if (packs.containsKey(uuid)) { + throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE); + } + + ResourcePackHolder holder = ResourcePackHolder.of(pack); + attemptRegisterOptions(holder, options); + packs.put(uuid, holder); + } + + @Override + public void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(options); + + ResourcePackHolder holder = packs.get(uuid); + if (holder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + attemptRegisterOptions(holder, options); + } + + @Override + public Collection> options(@NonNull UUID uuid) { + Objects.requireNonNull(uuid); + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + return packHolder.optionHolder().immutableValues(); + } + + @Override + public @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(type); + + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + return packHolder.optionHolder().get(type); + } + + @Override + public void unregister(@NonNull UUID uuid) { + packs.remove(uuid); + } + + private void attemptRegisterOptions(@NonNull ResourcePackHolder holder, @Nullable ResourcePackOption... options) { + if (options == null) { + return; + } + + holder.optionHolder().validateAndAdd(holder.pack(), options); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java index 5bc0dd0bd..a926e5400 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/SessionLoadResourcePacksEventImpl.java @@ -25,45 +25,200 @@ package org.geysermc.geyser.event.type; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.bedrock.SessionLoadResourcePacksEvent; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; +import org.geysermc.geyser.pack.option.OptionHolder; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; public class SessionLoadResourcePacksEventImpl extends SessionLoadResourcePacksEvent { - private final Map packs; + /** + * The packs for this Session. A {@link ResourcePackHolder} may contain resource pack options registered + * during the {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent}. + */ + @Getter + private final Map packs; - public SessionLoadResourcePacksEventImpl(GeyserSession session, Map packMap) { + /** + * The additional, per-session options for the resource packs of this session. + * These options are prioritized over the "default" options registered + * in the {@link org.geysermc.geyser.api.event.lifecycle.GeyserDefineResourcePacksEvent} + */ + private final Map sessionPackOptionOverrides; + + public SessionLoadResourcePacksEventImpl(GeyserSession session) { super(session); - this.packs = packMap; - } - - public @NonNull Map getPacks() { - return packs; + this.packs = new Object2ObjectLinkedOpenHashMap<>(Registries.RESOURCE_PACKS.get()); + this.sessionPackOptionOverrides = new Object2ObjectOpenHashMap<>(); } @Override public @NonNull List resourcePacks() { - return List.copyOf(packs.values()); + return packs.values().stream().map(ResourcePackHolder::resourcePack).toList(); } @Override public boolean register(@NonNull ResourcePack resourcePack) { - UUID packID = resourcePack.manifest().header().uuid(); - if (packs.containsValue(resourcePack) || packs.containsKey(packID)) { + try { + register(resourcePack, PriorityOption.NORMAL); + } catch (ResourcePackException e) { + GeyserImpl.getInstance().getLogger().error("An exception occurred while registering resource pack: " + e.getMessage(), e); return false; } - packs.put(resourcePack.manifest().header().uuid(), resourcePack); return true; } + @Override + public void register(@NonNull ResourcePack resourcePack, @Nullable ResourcePackOption... options) { + Objects.requireNonNull(resourcePack); + if (!(resourcePack instanceof GeyserResourcePack pack)) { + throw new ResourcePackException(ResourcePackException.Cause.UNKNOWN_IMPLEMENTATION); + } + + UUID uuid = resourcePack.uuid(); + if (packs.containsKey(uuid)) { + throw new ResourcePackException(ResourcePackException.Cause.DUPLICATE); + } + + attemptRegisterOptions(pack, options); + packs.put(uuid, ResourcePackHolder.of(pack)); + } + + @Override + public void registerOptions(@NonNull UUID uuid, @NonNull ResourcePackOption... options) { + Objects.requireNonNull(uuid, "uuid cannot be null"); + Objects.requireNonNull(options, "options cannot be null"); + ResourcePackHolder holder = packs.get(uuid); + if (holder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + attemptRegisterOptions(holder.pack(), options); + } + + @Override + public Collection> options(@NonNull UUID uuid) { + Objects.requireNonNull(uuid); + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + OptionHolder optionHolder = sessionPackOptionOverrides.get(uuid); + if (optionHolder == null) { + // No need to create a new session option holder + return packHolder.optionHolder().immutableValues(); + } + + return optionHolder.immutableValues(packHolder.optionHolder()); + } + + @Override + public @Nullable ResourcePackOption option(@NonNull UUID uuid, ResourcePackOption.@NonNull Type type) { + Objects.requireNonNull(uuid); + Objects.requireNonNull(type); + + ResourcePackHolder packHolder = packs.get(uuid); + if (packHolder == null) { + throw new ResourcePackException(ResourcePackException.Cause.PACK_NOT_FOUND); + } + + @Nullable OptionHolder additionalOptions = sessionPackOptionOverrides.get(uuid); + OptionHolder defaultHolder = packHolder.optionHolder(); + Objects.requireNonNull(defaultHolder); // should never be null + + return OptionHolder.optionByType(type, additionalOptions, defaultHolder); + } + @Override public boolean unregister(@NonNull UUID uuid) { + sessionPackOptionOverrides.remove(uuid); return packs.remove(uuid) != null; } + + private void attemptRegisterOptions(@NonNull GeyserResourcePack pack, @Nullable ResourcePackOption... options) { + if (options == null) { + return; + } + + OptionHolder holder = this.sessionPackOptionOverrides.computeIfAbsent(pack.uuid(), $ -> new OptionHolder()); + holder.validateAndAdd(pack, options); + } + + // Methods used internally for e.g. ordered packs, or resource pack entries + + public List orderedPacks() { + return packs.values().stream() + // Map each ResourcePack to a pair of (GeyserResourcePack, Priority) + .map(holder -> new AbstractMap.SimpleEntry<>(holder.pack(), priority(holder.pack()))) + // Sort by priority in descending order + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + // Map the sorted entries to ResourcePackStackPacket.Entry + .map(entry -> { + ResourcePackManifest.Header header = entry.getKey().manifest().header(); + return new ResourcePackStackPacket.Entry( + header.uuid().toString(), + header.version().toString(), + subpackName(entry.getKey()) + ); + }) + .toList(); + } + + public List infoPacketEntries() { + List entries = new ArrayList<>(); + + for (ResourcePackHolder holder : packs.values()) { + GeyserResourcePack pack = holder.pack(); + ResourcePackManifest.Header header = pack.manifest().header(); + entries.add(new ResourcePacksInfoPacket.Entry( + header.uuid(), header.version().toString(), pack.codec().size(), pack.contentKey(), + subpackName(pack), header.uuid().toString(), false, false, false, subpackName(pack)) + ); + } + + return entries; + } + + // Helper methods to get the options for a ResourcePack + + public T value(UUID uuid, ResourcePackOption.Type type, T defaultValue) { + OptionHolder holder = sessionPackOptionOverrides.get(uuid); + OptionHolder defaultHolder = packs.get(uuid).optionHolder(); + Objects.requireNonNull(defaultHolder); // should never be null + + return OptionHolder.valueOrFallback(type, holder, defaultHolder, defaultValue); + } + + private double priority(GeyserResourcePack pack) { + return value(pack.uuid(), ResourcePackOption.Type.PRIORITY, PriorityOption.NORMAL.value()); + } + + private String subpackName(GeyserResourcePack pack) { + return value(pack.uuid(), ResourcePackOption.Type.SUBPACK, ""); + } } 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 c67ea6545..f2bd7a5c5 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -58,10 +58,14 @@ import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.pack.ResourcePackHolder; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.PendingMicrosoftAuthentication; import org.geysermc.geyser.text.GeyserLocale; @@ -74,7 +78,6 @@ import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.util.ArrayDeque; import java.util.Deque; -import java.util.HashMap; import java.util.OptionalInt; import java.util.UUID; @@ -199,17 +202,12 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { geyser.getSessionManager().addPendingSession(session); - this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session, new HashMap<>(Registries.RESOURCE_PACKS.get())); + this.resourcePackLoadEvent = new SessionLoadResourcePacksEventImpl(session); this.geyser.eventBus().fire(this.resourcePackLoadEvent); ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket(); - for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { - PackCodec codec = pack.codec(); - ResourcePackManifest.Header header = pack.manifest().header(); - resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry( - header.uuid(), header.version().toString(), codec.size(), pack.contentKey(), - "", header.uuid().toString(), false, false, false, "")); - } + resourcePacksInfo.getResourcePackInfos().addAll(this.resourcePackLoadEvent.infoPacketEntries()); + resourcePacksInfo.setForcedToAccept(GeyserImpl.getInstance().getConfig().isForceResourcePacks()); resourcePacksInfo.setWorldTemplateId(UUID.randomUUID()); resourcePacksInfo.setWorldTemplateVersion("*"); @@ -222,7 +220,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public PacketSignal handle(ResourcePackClientResponsePacket packet) { switch (packet.getStatus()) { - case COMPLETED: + case COMPLETED -> { if (geyser.getConfig().getRemote().authType() != AuthType.ONLINE) { session.authenticate(session.getAuthData().name()); } else if (!couldLoginUserByName(session.getAuthData().name())) { @@ -230,30 +228,21 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { session.connect(); } geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.connect", session.getAuthData().name())); - break; - - case SEND_PACKS: + } + case SEND_PACKS -> { packsToSend.addAll(packet.getPackIds()); sendPackDataInfo(packsToSend.pop()); - break; - - case HAVE_ALL_PACKS: + } + case HAVE_ALL_PACKS -> { ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); stackPacket.setExperimentsPreviouslyToggled(false); stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not stackPacket.setGameVersion(session.getClientData().getGameVersion()); - - for (ResourcePack pack : this.resourcePackLoadEvent.resourcePacks()) { - ResourcePackManifest.Header header = pack.manifest().header(); - stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.uuid().toString(), header.version().toString(), "")); - } + stackPacket.getResourcePacks().addAll(this.resourcePackLoadEvent.orderedPacks()); session.sendUpstreamPacket(stackPacket); - break; - - default: - session.disconnect("disconnectionScreen.resourcePack"); - break; + } + default -> session.disconnect("disconnectionScreen.resourcePack"); } return PacketSignal.HANDLED; @@ -302,10 +291,29 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public PacketSignal handle(ResourcePackChunkRequestPacket packet) { + ResourcePackHolder holder = this.resourcePackLoadEvent.getPacks().get(packet.getPackId()); + + if (holder == null) { + GeyserImpl.getInstance().getLogger().debug("Client {0} tried to request pack id {1} not sent to it!", + session.bedrockUsername(), packet.getPackId()); + session.disconnect("disconnectionScreen.resourcePack"); + return PacketSignal.HANDLED; + } + + ResourcePack pack = holder.pack(); ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket(); - ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packet.getPackId()); PackCodec codec = pack.codec(); + // If a remote pack ends up here, that usually implies that a client was not able to download the pack + if (codec instanceof UrlPackCodec urlPackCodec) { + ResourcePackLoader.testRemotePack(session, urlPackCodec, packet.getPackId(), packet.getPackVersion()); + + if (!resourcePackLoadEvent.value(pack.uuid(), ResourcePackOption.Type.FALLBACK, true)) { + session.disconnect("Unable to provide downloaded resource pack. Contact an administrator!"); + return PacketSignal.HANDLED; + } + } + data.setChunkIndex(packet.getChunkIndex()); data.setProgress((long) packet.getChunkIndex() * GeyserResourcePack.CHUNK_SIZE); data.setPackVersion(packet.getPackVersion()); @@ -315,10 +323,11 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { long remainingSize = codec.size() - offset; byte[] packData = new byte[(int) MathUtils.constrain(remainingSize, 0, GeyserResourcePack.CHUNK_SIZE)]; - try (SeekableByteChannel channel = codec.serialize(pack)) { + try (SeekableByteChannel channel = codec.serialize()) { channel.position(offset); channel.read(ByteBuffer.wrap(packData, 0, packData.length)); } catch (IOException e) { + session.disconnect("disconnectionScreen.resourcePack"); e.printStackTrace(); } @@ -337,8 +346,33 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { private void sendPackDataInfo(String id) { ResourcePackDataInfoPacket data = new ResourcePackDataInfoPacket(); String[] packID = id.split("_"); - UUID uuid = UUID.fromString(packID[0]); - ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(uuid); + + if (packID.length < 2) { + GeyserImpl.getInstance().getLogger().debug("Client {0} tried to request invalid pack id {1}!", + session.bedrockUsername(), packID); + session.disconnect("disconnectionScreen.resourcePack"); + return; + } + + UUID packId; + try { + packId = UUID.fromString(packID[0]); + } catch (IllegalArgumentException e) { + GeyserImpl.getInstance().getLogger().debug("Client {0} tried to request pack with an invalid id {1})", + session.bedrockUsername(), id); + session.disconnect("disconnectionScreen.resourcePack"); + return; + } + + ResourcePackHolder holder = this.resourcePackLoadEvent.getPacks().get(packId); + if (holder == null) { + GeyserImpl.getInstance().getLogger().debug("Client {0} tried to request pack id {1} not sent to it!", + session.bedrockUsername(), id); + session.disconnect("disconnectionScreen.resourcePack"); + return; + } + + ResourcePack pack = holder.pack(); PackCodec codec = pack.codec(); ResourcePackManifest.Header header = pack.manifest().header(); diff --git a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java index 82408b6e7..306bb9aa8 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePack.java @@ -25,14 +25,64 @@ package org.geysermc.geyser.pack; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.pack.PackCodec; import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.api.pack.ResourcePackManifest; -public record GeyserResourcePack(PackCodec codec, ResourcePackManifest manifest, String contentKey) implements ResourcePack { +import java.util.Objects; + +public record GeyserResourcePack( + @NonNull PackCodec codec, + @NonNull ResourcePackManifest manifest, + @NonNull String contentKey +) implements ResourcePack { /** * The size of each chunk to use when sending the resource packs to clients in bytes */ public static final int CHUNK_SIZE = 102400; + + public static class Builder implements ResourcePack.Builder { + + public Builder(PackCodec codec, ResourcePackManifest manifest) { + this.codec = codec; + this.manifest = manifest; + } + + public Builder(PackCodec codec, ResourcePackManifest manifest, String contentKey) { + this.codec = codec; + this.manifest = manifest; + this.contentKey = contentKey; + } + + private final PackCodec codec; + private final ResourcePackManifest manifest; + private String contentKey = ""; + + @Override + public ResourcePackManifest manifest() { + return manifest; + } + + @Override + public PackCodec codec() { + return codec; + } + + @Override + public String contentKey() { + return contentKey; + } + + public Builder contentKey(@NonNull String contentKey) { + Objects.requireNonNull(contentKey); + this.contentKey = contentKey; + return this; + } + + public GeyserResourcePack build() { + return new GeyserResourcePack(codec, manifest, contentKey); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java index 25a0f0ee0..6b309e910 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java +++ b/core/src/main/java/org/geysermc/geyser/pack/GeyserResourcePackManifest.java @@ -35,9 +35,30 @@ import org.geysermc.geyser.api.pack.ResourcePackManifest; import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.UUID; -public record GeyserResourcePackManifest(@JsonProperty("format_version") int formatVersion, Header header, Collection modules, Collection dependencies) implements ResourcePackManifest { +public record GeyserResourcePackManifest( + @JsonProperty("format_version") int formatVersion, + Header header, + Collection modules, + Collection dependencies, + Collection subpacks, + Collection settings +) implements ResourcePackManifest { + public GeyserResourcePackManifest(int formatVersion, + Header header, + Collection modules, + Collection dependencies, + Collection subpacks, + Collection settings) { + this.formatVersion = formatVersion; + this.header = header; + this.modules = ensureNonNull(modules); + this.dependencies = ensureNonNull(dependencies); + this.subpacks = ensureNonNull(subpacks); + this.settings = ensureNonNull(settings); + } public record Header(UUID uuid, Version version, String name, String description, @JsonProperty("min_engine_version") Version minimumSupportedMinecraftVersion) implements ResourcePackManifest.Header { } @@ -45,6 +66,15 @@ public record GeyserResourcePackManifest(@JsonProperty("format_version") int for public record Dependency(UUID uuid, Version version) implements ResourcePackManifest.Dependency { } + public record Subpack(@JsonProperty("folder_name") String folderName, String name, @JsonProperty("memory_tier") Float memoryTier) implements ResourcePackManifest.Subpack { } + + public record Setting(String type, String text) implements ResourcePackManifest.Setting { } + + static Collection ensureNonNull(Collection collection) { + if (collection == null) return Collections.emptyList(); + return Collections.unmodifiableCollection(collection); + } + @JsonDeserialize(using = Version.VersionDeserializer.class) public record Version(int major, int minor, int patch) implements ResourcePackManifest.Version { diff --git a/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java b/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java new file mode 100644 index 000000000..f475127e7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/ResourcePackHolder.java @@ -0,0 +1,45 @@ +/* + * 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.pack; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.pack.option.OptionHolder; + +public record ResourcePackHolder( + @NonNull GeyserResourcePack pack, + @NonNull OptionHolder optionHolder +) { + + public static ResourcePackHolder of(GeyserResourcePack pack) { + return new ResourcePackHolder(pack, new OptionHolder(PriorityOption.NORMAL)); + } + + public ResourcePack resourcePack() { + return this.pack; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/option/GeyserPriorityOption.java b/core/src/main/java/org/geysermc/geyser/pack/option/GeyserPriorityOption.java new file mode 100644 index 000000000..a62386b24 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/option/GeyserPriorityOption.java @@ -0,0 +1,55 @@ +/* + * 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.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.PriorityOption; + +import java.util.Objects; + +public record GeyserPriorityOption(int priority) implements PriorityOption { + + @Override + public @NonNull Type type() { + return Type.PRIORITY; + } + + @Override + public @NonNull Integer value() { + return priority; + } + + @Override + public void validate(@NonNull ResourcePack pack) { + Objects.requireNonNull(pack); + if (priority < -100 || priority > 100) { + throw new ResourcePackException(ResourcePackException.Cause.INVALID_PACK_OPTION, + "Priority must be between -100 and 100 inclusive!"); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/option/GeyserSubpackOption.java b/core/src/main/java/org/geysermc/geyser/pack/option/GeyserSubpackOption.java new file mode 100644 index 000000000..9f26820dd --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/option/GeyserSubpackOption.java @@ -0,0 +1,66 @@ +/* + * 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.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.SubpackOption; + +import java.util.Objects; + +/** + * Can be used to specify which subpack from a resource pack a player should load. + * Available subpacks can be seen in a resource pack manifest {@link ResourcePackManifest#subpacks()} + */ +public record GeyserSubpackOption(String subpackName) implements SubpackOption { + + @Override + public @NonNull Type type() { + return Type.SUBPACK; + } + + @Override + public @NonNull String value() { + return subpackName; + } + + @Override + public void validate(@NonNull ResourcePack pack) { + Objects.requireNonNull(pack); + + // Allow empty subpack names - they're the same as "none" + if (subpackName.isEmpty()) { + return; + } + + if (pack.manifest().subpacks().stream().noneMatch(subpack -> subpack.name().equals(subpackName))) { + throw new ResourcePackException(ResourcePackException.Cause.INVALID_PACK_OPTION, + "No subpack with the name %s found!".formatted(subpackName)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/option/GeyserUrlFallbackOption.java b/core/src/main/java/org/geysermc/geyser/pack/option/GeyserUrlFallbackOption.java new file mode 100644 index 000000000..7a3d3d3da --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/option/GeyserUrlFallbackOption.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.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.api.pack.exception.ResourcePackException; +import org.geysermc.geyser.api.pack.option.UrlFallbackOption; + +public record GeyserUrlFallbackOption(Boolean enabled) implements UrlFallbackOption { + + @Override + public @NonNull Type type() { + return Type.FALLBACK; + } + + @Override + public @NonNull Boolean value() { + return enabled; + } + + @Override + public void validate(@NonNull ResourcePack pack) { + if (!(pack.codec() instanceof UrlPackCodec)) { + throw new ResourcePackException(ResourcePackException.Cause.INVALID_PACK_OPTION, + "The UrlFallbackOption cannot be set on resource packs not created using the url pack codec!"); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/option/OptionHolder.java b/core/src/main/java/org/geysermc/geyser/pack/option/OptionHolder.java new file mode 100644 index 000000000..f6ec741df --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/option/OptionHolder.java @@ -0,0 +1,135 @@ +/* + * 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.pack.option; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.api.pack.option.ResourcePackOption; +import org.geysermc.geyser.pack.GeyserResourcePack; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class OptionHolder extends HashMap> { + + public OptionHolder() { + super(); + } + + // Used when adding resource packs initially to ensure that a priority option is always set + // It is however NOT used for session-options, as then the "normal" prio might override + // the resource pack option + public OptionHolder(PriorityOption option) { + super(); + put(option.type(), option); + } + + public void validateAndAdd(ResourcePack pack, ResourcePackOption... options) { + for (ResourcePackOption option : options) { + // Validate before adding + option.validate(pack); + + // Ensure that we do not have duplicate types. + if (super.containsKey(option.type())) { + super.replace(option.type(), option); + } else { + super.put(option.type(), option); + } + } + } + + @SuppressWarnings("unchecked") + public static T valueOrFallback(ResourcePackOption.@NonNull Type type, + @Nullable OptionHolder sessionPackOptions, + @NonNull OptionHolder resourcePackOptions, + @NonNull T defaultValue) { + ResourcePackOption option; + + // First: the session's options, if they exist + if (sessionPackOptions != null) { + option = sessionPackOptions.get(type); + if (option != null) { + return (T) option.value(); + } + } + + // Second: check the resource pack options + option = resourcePackOptions.get(type); + if (option != null) { + return (T) option.value(); + } + + // Finally: return default + return defaultValue; + } + + public static @Nullable ResourcePackOption optionByType(ResourcePackOption.@NonNull Type type, + @Nullable OptionHolder sessionPackOptions, + @NonNull OptionHolder resourcePackOptions) { + + // First: the session-specific options, if these exist + if (sessionPackOptions != null) { + ResourcePackOption option = sessionPackOptions.get(type); + if (option != null) { + return option; + } + } + + // Second: check the default holder for the option, if it exists; + // Or return null if the option isn't set. + return resourcePackOptions.get(type); + } + + public void remove(ResourcePackOption option) { + super.remove(option.type()); + } + + /** + * @return the options of this holder in an immutable collection + */ + public Collection> immutableValues() { + return Collections.unmodifiableCollection(values()); + } + + /** + * @return the options of this option holder, with fallbacks to options of a {@link GeyserResourcePack} + * if they're not already overridden here + */ + public Collection> immutableValues(OptionHolder defaultValues) { + // Create a map to hold the combined values + Map> combinedOptions = new HashMap<>(this); + + // Add options from the pack if not already overridden by this OptionHolder + defaultValues.forEach(combinedOptions::putIfAbsent); + + // Return an immutable collection of the combined options + return Collections.unmodifiableCollection(combinedOptions.values()); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java index 84067600f..ea6f319f9 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java +++ b/core/src/main/java/org/geysermc/geyser/pack/path/GeyserPathPackCodec.java @@ -79,15 +79,20 @@ public class GeyserPathPackCodec extends PathPackCodec { } @Override - public @NonNull SeekableByteChannel serialize(@NonNull ResourcePack resourcePack) throws IOException { + public @NonNull SeekableByteChannel serialize() throws IOException { return FileChannel.open(this.path); } @Override - protected @NonNull ResourcePack create() { + protected ResourcePack.@NonNull Builder createBuilder() { return ResourcePackLoader.readPack(this.path); } + @Override + protected @NonNull ResourcePack create() { + return createBuilder().build(); + } + private void checkLastModified() { try { FileTime lastModified = Files.getLastModifiedTime(this.path); diff --git a/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java new file mode 100644 index 000000000..ee7f48c90 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java @@ -0,0 +1,96 @@ +/* + * 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.pack.url; + +import lombok.Getter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.pack.PathPackCodec; +import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.pack.GeyserResourcePack; +import org.geysermc.geyser.registry.loader.ResourcePackLoader; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.util.Objects; + +public class GeyserUrlPackCodec extends UrlPackCodec { + private final @NonNull String url; + @Getter + private PathPackCodec fallback; + + public GeyserUrlPackCodec(@NonNull String url) throws IllegalArgumentException { + Objects.requireNonNull(url); + this.url = url; + } + + @Override + public byte @NonNull [] sha256() { + Objects.requireNonNull(fallback, "must call #create() before attempting to get the sha256!"); + return fallback.sha256(); + } + + @Override + public long size() { + Objects.requireNonNull(fallback, "must call #create() before attempting to get the size!"); + return fallback.size(); + } + + @Override + public @NonNull SeekableByteChannel serialize() throws IOException { + Objects.requireNonNull(fallback, "must call #create() before attempting to serialize!!"); + return fallback.serialize(); + } + + @Override + @NonNull + public GeyserResourcePack create() { + return createBuilder().build(); + } + + @Override + protected GeyserResourcePack.@NonNull Builder createBuilder() { + if (this.fallback == null) { + try { + ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> { + if (throwable != null) { + throw new IllegalArgumentException(throwable); + } else if (pack != null) { + this.fallback = pack; + } + }).join(); // Needed to ensure that we don't attempt to read a pack before downloading/checking it + } catch (Exception e) { + throw new IllegalArgumentException("Failed to download pack from the url %s (%s)!".formatted(url, e.getMessage())); + } + } + + return ResourcePackLoader.readPack(this); + } + + @Override + public @NonNull String url() { + return this.url; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index fc41275ae..af0c9dbc0 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -34,10 +34,10 @@ import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.pack.ResourcePackHolder; import org.geysermc.geyser.registry.loader.BiomeIdentifierRegistryLoader; import org.geysermc.geyser.registry.loader.BlockEntityRegistryLoader; import org.geysermc.geyser.registry.loader.ParticleTypesRegistryLoader; @@ -166,9 +166,9 @@ public final class Registries { //public static final SimpleMappedDeferredRegistry> RECIPES = SimpleMappedDeferredRegistry.create("mappings/recipes.nbt", RecipeRegistryLoader::new); /** - * A mapped registry holding {@link ResourcePack}'s with the pack uuid as keys. + * A mapped registry holding {@link ResourcePackHolder}'s with the pack uuid as keys. */ - public static final SimpleMappedDeferredRegistry RESOURCE_PACKS = SimpleMappedDeferredRegistry.create(GeyserImpl.getInstance().packDirectory(), RegistryLoaders.RESOURCE_PACKS); + public static final SimpleMappedDeferredRegistry RESOURCE_PACKS = SimpleMappedDeferredRegistry.create(GeyserImpl.getInstance().packDirectory(), RegistryLoaders.RESOURCE_PACKS); /** * A versioned registry holding most Bedrock tags, with the Java item list (sorted) being the key, and the tag name as the value. 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 94de0c298..b1f62ca99 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 @@ -40,10 +40,14 @@ import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomItemOptions; 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.api.pack.UrlPackCodec; +import org.geysermc.geyser.api.pack.option.PriorityOption; +import org.geysermc.geyser.api.pack.option.SubpackOption; +import org.geysermc.geyser.api.pack.option.UrlFallbackOption; import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.extension.command.GeyserExtensionCommand; +import org.geysermc.geyser.impl.camera.GeyserCameraFade; +import org.geysermc.geyser.impl.camera.GeyserCameraPosition; import org.geysermc.geyser.item.GeyserCustomItemData; import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; @@ -53,7 +57,11 @@ import org.geysermc.geyser.level.block.GeyserGeometryComponent; import org.geysermc.geyser.level.block.GeyserJavaBlockState; import org.geysermc.geyser.level.block.GeyserMaterialInstance; import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData; +import org.geysermc.geyser.pack.option.GeyserPriorityOption; +import org.geysermc.geyser.pack.option.GeyserSubpackOption; +import org.geysermc.geyser.pack.option.GeyserUrlFallbackOption; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; +import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; import org.geysermc.geyser.registry.provider.ProviderSupplier; import java.nio.file.Path; @@ -66,9 +74,10 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov @Override public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { - // misc + // commands providers.put(Command.Builder.class, args -> new GeyserExtensionCommand.Builder<>((Extension) args[0])); + // custom blocks providers.put(CustomBlockComponents.Builder.class, args -> new GeyserCustomBlockComponents.Builder()); providers.put(CustomBlockData.Builder.class, args -> new GeyserCustomBlockData.Builder()); providers.put(JavaBlockState.Builder.class, args -> new GeyserJavaBlockState.Builder()); @@ -76,8 +85,15 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov providers.put(MaterialInstance.Builder.class, args -> new GeyserMaterialInstance.Builder()); providers.put(GeometryComponent.Builder.class, args -> new GeyserGeometryComponent.Builder()); + // misc providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0])); + + // packs providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0])); + providers.put(UrlPackCodec.class, args -> new GeyserUrlPackCodec((String) args[0])); + providers.put(PriorityOption.class, args -> new GeyserPriorityOption((int) args[0])); + providers.put(SubpackOption.class, args -> new GeyserSubpackOption((String) args[0])); + providers.put(UrlFallbackOption.class, args -> new GeyserUrlFallbackOption((Boolean) args[0])); // items providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.Builder()); diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java index adb64b8af..e53903154 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java @@ -25,16 +25,30 @@ package org.geysermc.geyser.registry.loader; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent; +import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.ResourcePack; +import org.geysermc.geyser.api.pack.ResourcePackManifest; +import org.geysermc.geyser.api.pack.UrlPackCodec; +import org.geysermc.geyser.event.type.GeyserDefineResourcePacksEventImpl; import org.geysermc.geyser.pack.GeyserResourcePack; import org.geysermc.geyser.pack.GeyserResourcePackManifest; +import org.geysermc.geyser.pack.ResourcePackHolder; import org.geysermc.geyser.pack.SkullResourcePackManager; import org.geysermc.geyser.pack.path.GeyserPathPackCodec; +import org.geysermc.geyser.pack.url.GeyserUrlPackCodec; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; +import org.geysermc.geyser.util.WebUtils; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; @@ -42,10 +56,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -53,9 +70,17 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** - * Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserLoadResourcePacksEvent}. + * Loads {@link ResourcePack}s within a {@link Path} directory, firing the {@link GeyserDefineResourcePacksEventImpl}. */ -public class ResourcePackLoader implements RegistryLoader> { +public class ResourcePackLoader implements RegistryLoader> { + + /** + * Used to keep track of remote resource packs that the client rejected. + * If a client rejects such a pack, it falls back to the old method, and Geyser serves a cached variant. + */ + private static final Cache CACHED_FAILED_PACKS = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); static final PathMatcher PACK_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**.{zip,mcpack}"); @@ -65,8 +90,8 @@ public class ResourcePackLoader implements RegistryLoader load(Path directory) { - Map packMap = new HashMap<>(); + public Map load(Path directory) { + Map packMap = new Object2ObjectOpenHashMap<>(); if (!Files.exists(directory)) { try { @@ -77,7 +102,7 @@ public class ResourcePackLoader implements RegistryLoader resourcePacks; - try (Stream stream = Files.walk(directory)) { + try (Stream stream = Files.list(directory)) { resourcePacks = stream.filter(PACK_MATCHER::matches) .collect(Collectors.toCollection(ArrayList::new)); // toList() does not guarantee mutability } catch (Exception e) { @@ -95,33 +120,76 @@ public class ResourcePackLoader implements RegistryLoader manifestReference = new AtomicReference<>(); try (ZipFile zip = new ZipFile(path.toFile()); @@ -129,7 +197,7 @@ public class ResourcePackLoader implements RegistryLoader { String name = x.getName(); if (SHOW_RESOURCE_PACK_LENGTH_WARNING && name.length() >= 80) { - GeyserImpl.getInstance().getLogger().warning("The resource pack " + path.getFileName() + GeyserImpl.getInstance().getLogger().warning("The resource pack " + packLocation + " has a file in it that meets or exceeds 80 characters in its path (" + name + ", " + name.length() + " characters long). This will cause problems on some Bedrock platforms." + " Please rename it to be shorter, or reduce the amount of folders needed to get to the file."); @@ -148,17 +216,190 @@ public class ResourcePackLoader implements RegistryLoader loadRemotePacks() { + GeyserImpl instance = GeyserImpl.getInstance(); + // Unable to make this a static variable, as the test would fail + final Path cachedDirectory = instance.getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); + + if (!Files.exists(cachedDirectory)) { + try { + Files.createDirectories(cachedDirectory); + } catch (IOException e) { + instance.getLogger().error("Could not create remote pack cache directory", e); + return new Object2ObjectOpenHashMap<>(); + } + } + + //List remotePackUrls = instance.getConfig().getResourcePackUrls(); + List remotePackUrls = List.of(); + Map packMap = new Object2ObjectOpenHashMap<>(); + + for (String url : remotePackUrls) { + try { + GeyserUrlPackCodec codec = new GeyserUrlPackCodec(url); + GeyserResourcePack pack = codec.create(); + packMap.put(pack.uuid(), ResourcePackHolder.of(pack)); + } catch (Throwable e) { + instance.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url)); + instance.getLogger().error(e.getMessage()); + if (instance.getLogger().isDebug()) { + e.printStackTrace(); + } + } + } + + return packMap; + } + + /** + * Used when a Bedrock client requests a Bedrock resource pack from the server when it should be downloading it + * from a remote provider. Since this would be called each time a Bedrock client requests a piece of the Bedrock pack, + * this uses a cache to ensure we aren't re-checking a dozen times. + * + * @param codec the codec of the resource pack that wasn't successfully downloaded by a Bedrock client. + */ + public static void testRemotePack(GeyserSession session, UrlPackCodec codec, UUID packId, String packVersion) { + if (CACHED_FAILED_PACKS.getIfPresent(codec.url()) == null) { + String url = codec.url(); + CACHED_FAILED_PACKS.put(url, codec); + GeyserImpl.getInstance().getLogger().warning( + "Bedrock client (%s, playing on %s) was not able to download the resource pack at %s. Checking for changes now:" + .formatted(session.bedrockUsername(), session.getClientData().getDeviceOs().name(), codec.url()) + ); + + downloadPack(codec.url(), true).whenComplete((pathPackCodec, e) -> { + if (e != null) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); + if (GeyserImpl.getInstance().getLogger().isDebug()) { + e.printStackTrace(); + if (pathPackCodec != null) { + deleteFile(pathPackCodec.path()); + } + return; + } + } + + if (pathPackCodec == null) { + return; // Already warned about + } + + GeyserResourcePack newPack = readPack(pathPackCodec.path()).build(); + if (newPack.uuid().equals(packId)) { + if (packVersion.equals(newPack.manifest().header().version().toString())) { + GeyserImpl.getInstance().getLogger().info("No version or pack change detected: Was the resource pack server down?"); + } else { + GeyserImpl.getInstance().getLogger().info("Detected a new resource pack version (%s, old version %s) for pack at %s!" + .formatted(packVersion, newPack.manifest().header().version().toString(), url)); + } + } else { + GeyserImpl.getInstance().getLogger().info("Detected a new resource pack at the url %s!".formatted(url)); + } + + // This should be safe to do as we're not directly using registries to read packs. + // Instead, they're cached per-session in the SessionLoadResourcePacks event + Registries.RESOURCE_PACKS.get().remove(packId); + Registries.RESOURCE_PACKS.get().put(newPack.uuid(), ResourcePackHolder.of(newPack)); + + if (codec instanceof GeyserUrlPackCodec geyserUrlPackCodec + && geyserUrlPackCodec.getFallback() != null) { + Path path = geyserUrlPackCodec.getFallback().path(); + try { + GeyserImpl.getInstance().getScheduledThread().schedule(() -> { + CACHED_FAILED_PACKS.invalidate(codec.url()); + deleteFile(path); + }, 5, TimeUnit.MINUTES); + } catch (RejectedExecutionException exception) { + // No scheduling here, probably because we're shutting down? + deleteFile(path); + } + } + }); + } + } + + private static void deleteFile(Path path) { + if (path.toFile().exists()) { + try { + Files.delete(path); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Unable to delete old pack! " + e.getMessage()); + e.printStackTrace(); + } + } + } + + public static CompletableFuture<@Nullable PathPackCodec> downloadPack(String url, boolean testing) throws IllegalArgumentException { + return CompletableFuture.supplyAsync(() -> { + Path path = WebUtils.downloadRemotePack(url, testing); + + // Already warned about these above + if (path == null) { + return null; + } + + // Check if the pack is a .zip or .mcpack file + if (!PACK_MATCHER.matches(path)) { + throw new IllegalArgumentException("Invalid pack format from url %s! Not a .zip or .mcpack file.".formatted(url)); + } + + try { + try (ZipFile zip = new ZipFile(path.toFile())) { + if (zip.stream().noneMatch(x -> x.getName().contains("manifest.json"))) { + throw new IllegalArgumentException("The pack at the url " + url + " does not contain a manifest file!"); + } + + // Check if a "manifest.json" or "pack_manifest.json" file is located directly in the zip... does not work otherwise. + // (something like MyZip.zip/manifest.json) will not, but will if it's a subfolder (MyPack.zip/MyPack/manifest.json) + if (zip.getEntry("manifest.json") != null || zip.getEntry("pack_manifest.json") != null) { + if (GeyserImpl.getInstance().getLogger().isDebug()) { + GeyserImpl.getInstance().getLogger().info("The remote resource pack from " + url + " contains a manifest.json file at the root of the zip file. " + + "This may not work for remote packs, and could cause Bedrock clients to fall back to request the pack from the server. " + + "Please put the pack file in a subfolder, and provide that zip in the URL."); + } + } + } + } catch (IOException e) { + throw new IllegalArgumentException(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", url), e); + } + + return new GeyserPathPackCodec(path); + }); + } + + public static void clear() { + if (Registries.RESOURCE_PACKS.loaded()) { + Registries.RESOURCE_PACKS.get().clear(); + } + CACHED_FAILED_PACKS.invalidateAll(); + } + + public static void cleanupRemotePacks() { + File cacheFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").toFile(); + if (!cacheFolder.exists()) { + return; + } + + int count = 0; + final long expireTime = (((long) 1000 * 60 * 60)); // one hour + for (File cachedPack : Objects.requireNonNull(cacheFolder.listFiles())) { + if (cachedPack.lastModified() < System.currentTimeMillis() - expireTime) { + //noinspection ResultOfMethodCallIgnored + cachedPack.delete(); + count++; + } + } + + if (count > 0) { + GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached resource pack files as they are no longer in use!", count)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java index 3d3bfb48d..10d8aa525 100644 --- a/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java +++ b/core/src/main/java/org/geysermc/geyser/scoreboard/Scoreboard.java @@ -25,23 +25,7 @@ package org.geysermc.geyser.scoreboard; -import static org.geysermc.geyser.scoreboard.UpdateType.REMOVE; - import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; -import java.util.stream.Collectors; import lombok.Getter; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; @@ -57,12 +41,28 @@ import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot; import org.geysermc.geyser.scoreboard.display.slot.PlayerlistDisplaySlot; import org.geysermc.geyser.scoreboard.display.slot.SidebarDisplaySlot; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor; import org.jetbrains.annotations.Contract; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.geysermc.geyser.scoreboard.UpdateType.REMOVE; + /** * Here follows some information about how scoreboards work in Java Edition, that is related to the workings of this * class: diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java index 812456644..363635ba6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java @@ -68,7 +68,7 @@ public class GeyserSessionAdapter extends SessionAdapter { @Override public void packetSending(PacketSendingEvent event) { - if (event.getPacket() instanceof ClientIntentionPacket) { + if (event.getPacket() instanceof ClientIntentionPacket intentionPacket) { BedrockClientData clientData = geyserSession.getClientData(); String addressSuffix; @@ -109,8 +109,6 @@ public class GeyserSessionAdapter extends SessionAdapter { addressSuffix = ""; } - ClientIntentionPacket intentionPacket = event.getPacket(); - String address; if (geyser.getConfig().getRemote().isForwardHost()) { address = clientData.getServerAddress().split(":")[0]; diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java index aec1fa4de..f3ad0be2f 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java @@ -167,7 +167,7 @@ public class SkinProvider { if (count > 0) { GeyserImpl.getInstance().getLogger().debug(String.format("Removed %d cached image files as they have expired", count)); } - }, 10, 1440, TimeUnit.MINUTES); + }, 10, 1, TimeUnit.DAYS); } } diff --git a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java index 1b7f2d9d9..4a228d2cb 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -28,22 +28,26 @@ package org.geysermc.geyser.util; import com.fasterxml.jackson.databind.JsonNode; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.GeyserLogger; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; import java.io.*; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; +import java.net.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.stream.Stream; public class WebUtils { + private static final Path REMOTE_PACK_CACHE = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); + /** * Makes a web request to the given URL and returns the body as a string * @@ -96,6 +100,115 @@ public class WebUtils { } } + /** + * Checks a remote pack URL to see if it is valid + * If it is, it will download the pack file and return a path to it + * + * @param url The URL to check + * @param force If true, the pack will be downloaded even if it is cached to a separate location. + * @return Path to the downloaded pack file, or null if it was unable to be loaded + */ + @SuppressWarnings("ResultOfMethodCallIgnored") + public static @Nullable Path downloadRemotePack(String url, boolean force) { + GeyserLogger logger = GeyserImpl.getInstance().getLogger(); + try { + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION); + con.setInstanceFollowRedirects(true); + + int responseCode = con.getResponseCode(); + if (responseCode >= 400) { + throw new IllegalStateException(String.format("Invalid response code from remote pack at URL: %s (code: %d)", url, responseCode)); + } + + int size = con.getContentLength(); + String type = con.getContentType(); + + if (size <= 0) { + throw new IllegalArgumentException(String.format("Invalid content length received from remote pack at URL: %s (size: %d)", url, size)); + } + + if (type == null || !type.equals("application/zip")) { + throw new IllegalArgumentException(String.format("Url %s tries to provide a resource pack using the %s content type, which is not supported by Bedrock edition! " + + "Bedrock Edition only supports the application/zip content type.", url, type)); + } + + Path packMetadata = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".metadata"); + Path downloadLocation; + + // If we downloaded this pack before, reuse it if the ETag matches. + if (Files.exists(packMetadata) && !force) { + try { + List metadata = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); + int cachedSize = Integer.parseInt(metadata.get(0)); + String cachedEtag = metadata.get(1); + long cachedLastModified = Long.parseLong(metadata.get(2)); + downloadLocation = REMOTE_PACK_CACHE.resolve(metadata.get(3)); + + if (cachedSize == size && + cachedEtag.equals(con.getHeaderField("ETag")) && + cachedLastModified == con.getLastModified() && + downloadLocation.toFile().exists()) { + logger.debug("Using cached pack (%s) for %s.".formatted(downloadLocation.getFileName(), url)); + downloadLocation.toFile().setLastModified(System.currentTimeMillis()); + packMetadata.toFile().setLastModified(System.currentTimeMillis()); + return downloadLocation; + } else { + logger.debug("Deleting cached pack/metadata (%s) as it appears to have changed!".formatted(url)); + Files.deleteIfExists(packMetadata); + Files.deleteIfExists(downloadLocation); + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata! " + e); + packMetadata.toFile().deleteOnExit(); + } + } + + downloadLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + "_" + System.currentTimeMillis() + ".zip"); + Files.copy(con.getInputStream(), downloadLocation, StandardCopyOption.REPLACE_EXISTING); + + // This needs to match as the client fails to download the pack otherwise + long downloadSize = Files.size(downloadLocation); + if (downloadSize != size) { + Files.delete(downloadLocation); + throw new IllegalStateException("Size mismatch with resource pack at url: %s. Downloaded pack has %s bytes, expected %s bytes!" + .formatted(url, downloadSize, size)); + } + + try { + Files.write( + packMetadata, + Arrays.asList( + String.valueOf(size), + con.getHeaderField("ETag"), + String.valueOf(con.getLastModified()), + downloadLocation.getFileName().toString() + )); + packMetadata.toFile().setLastModified(System.currentTimeMillis()); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + Files.delete(packMetadata); + Files.delete(downloadLocation); + return null; + } + + downloadLocation.toFile().setLastModified(System.currentTimeMillis()); + return downloadLocation; + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Unable to download resource pack from malformed URL %s! ".formatted(url)); + } catch (SocketTimeoutException | ConnectException e) { + logger.error("Unable to download pack from url %s due to network error! ( %s )".formatted(url, e.getMessage())); + logger.debug(e); + } catch (IOException e) { + throw new IllegalStateException("Unable to download and save remote resource pack from: %s ( %s )!".formatted(url, e.getMessage())); + } + return null; + } + + /** * Post a string to the given URL * diff --git a/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java b/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java index ce2fd2a6f..510680848 100644 --- a/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java +++ b/core/src/test/java/org/geysermc/geyser/registry/loader/ResourcePackLoaderTest.java @@ -62,7 +62,7 @@ public class ResourcePackLoaderTest { public void testPack() throws Exception { // this mcpack only contains a folder, which the manifest is in Path path = getResource("empty_pack.mcpack"); - ResourcePack pack = ResourcePackLoader.readPack(path); + ResourcePack pack = ResourcePackLoader.readPack(path).build(); assertEquals("", pack.contentKey()); // should probably add some more tests here related to the manifest } @@ -71,7 +71,7 @@ public class ResourcePackLoaderTest { public void testEncryptedPack() throws Exception { // this zip only contains a contents.json and manifest.json at the root Path path = getResource("encrypted_pack.zip"); - ResourcePack pack = ResourcePackLoader.readPack(path); + ResourcePack pack = ResourcePackLoader.readPack(path).build(); assertEquals("JAGcSXcXwcODc1YS70GzeWAUKEO172UA", pack.contentKey()); } diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java index f147e766d..e033b7288 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/util/EmptyGeyserLogger.java @@ -63,6 +63,11 @@ public class EmptyGeyserLogger implements GeyserLogger { } + @Override + public void debug(String message, Object... arguments) { + + } + @Override public void setDebug(boolean debug) { diff --git a/gradle.properties b/gradle.properties index 5557e53ca..343f7a51e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,5 +8,5 @@ org.gradle.vfs.watch=false group=org.geysermc id=geyser -version=2.6.1-SNAPSHOT +version=2.6.2-SNAPSHOT description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers. From 24f774e76751e538e69e08aa57c2d1a82528a7a8 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 25 Mar 2025 16:34:00 +0100 Subject: [PATCH 12/86] 1.21.70 support (#5414) * Initial work on 1.21.70 * Update mappings, fixup jitpack * Use LevelSoundEventPacket instead of LevelSoundEvent2Packet, ensure only temperate cow/pig/chicken/thrown egg entities spawn, update item components * Update cloudburst/protocol dependency, target master mappings branch --- README.md | 2 +- .../kotlin/geyser.base-conventions.gradle.kts | 5 +- .../geyser/entity/EntityDefinitions.java | 31 +- .../properties/GeyserEntityProperties.java | 2 +- .../properties/VanillaEntityProperties.java | 74 + .../geyser/entity/type/BoatEntity.java | 10 +- .../geysermc/geyser/entity/type/Entity.java | 5 + .../entity/type/ThrowableItemEntity.java | 11 + .../type/living/animal/ChickenEntity.java | 8 + .../entity/type/living/animal/CowEntity.java | 8 + .../type/living/animal/MooshroomEntity.java | 6 + .../entity/type/living/animal/PigEntity.java | 8 + .../living/animal/tameable/WolfEntity.java | 7 + .../type/living/monster/EndermanEntity.java | 4 +- .../geysermc/geyser/network/GameProtocol.java | 8 +- .../geyser/network/InvalidPacketHandler.java | 2 +- .../geyser/network/LoggingPacketHandler.java | 10 + .../populator/BlockRegistryPopulator.java | 40 +- .../registry/populator/Conversion766_748.java | 4 + .../registry/populator/Conversion776_766.java | 61 + .../populator/ItemRegistryPopulator.java | 2 + .../geyser/session/GeyserSession.java | 4 +- .../entity/JavaEntityEventTranslator.java | 6 +- .../resources/bedrock/biome_definitions.dat | Bin 39004 -> 41609 bytes .../bedrock/block_palette.1_21_70.nbt | Bin 0 -> 193872 bytes .../bedrock/creative_items.1_21_70.json | 8960 +++++++++++++ .../resources/bedrock/item_components.nbt | Bin 14202 -> 14228 bytes .../resources/bedrock/item_tags.1_21_60.json | 1651 ++- .../resources/bedrock/item_tags.1_21_70.json | 829 ++ .../bedrock/runtime_item_states.1_21_70.json | 10784 ++++++++++++++++ core/src/main/resources/mappings | 2 +- gradle/libs.versions.toml | 23 +- 32 files changed, 21655 insertions(+), 912 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java create mode 100644 core/src/main/resources/bedrock/block_palette.1_21_70.nbt create mode 100644 core/src/main/resources/bedrock/creative_items.1_21_70.json create mode 100644 core/src/main/resources/bedrock/item_tags.1_21_70.json create mode 100644 core/src/main/resources/bedrock/runtime_item_states.1_21_70.json diff --git a/README.md b/README.md index dcd9b8530..db462db26 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.21.40 - 1.21.62 and Minecraft Java 1.21.4. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.21.40 - 1.21.70 and Minecraft Java 1.21.4. 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/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts index 093f0a8c0..09440ac6a 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -62,11 +62,12 @@ repositories { name = "viaversion" } + // For Adventure snapshots + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + // 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/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index c8488238d..757c126b9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -28,7 +28,7 @@ package org.geysermc.geyser.entity; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.factory.EntityFactory; -import org.geysermc.geyser.entity.properties.GeyserEntityProperties; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.AbstractArrowEntity; import org.geysermc.geyser.entity.type.AbstractWindChargeEntity; import org.geysermc.geyser.entity.type.AreaEffectCloudEntity; @@ -462,6 +462,7 @@ public final class EntityDefinitions { EGG = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.EGG) .heightAndWidth(0.25f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) .build(); ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.ENDER_PEARL) @@ -685,15 +686,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown) .addTranslator(MetadataTypes.OPTIONAL_POSITION, CreakingEntity::setHomePos) - .properties(new GeyserEntityProperties.Builder() - .addEnum(CreakingEntity.CREAKING_STATE, - "neutral", - "hostile_observed", - "hostile_unobserved", - "twitching", - "crumbling") - .addInt(CreakingEntity.CREAKING_SWAYING_TICKS, 0, 6) - .build()) + .properties(VanillaEntityProperties.CREAKING) .build(); CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase) .type(EntityType.CREEPER) @@ -946,15 +939,7 @@ public final class EntityDefinitions { ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase) .type(EntityType.ARMADILLO) .height(0.65f).width(0.7f) - .properties(new GeyserEntityProperties.Builder() - .addEnum( - "minecraft:armadillo_state", - "unrolled", - "rolled_up", - "rolled_up_peeking", - "rolled_up_relaxing", - "rolled_up_unrolling") - .build()) + .properties(VanillaEntityProperties.ARMADILLO) .addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState) .build(); AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase) @@ -967,19 +952,19 @@ public final class EntityDefinitions { BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase) .type(EntityType.BEE) .heightAndWidth(0.6f) - .properties(new GeyserEntityProperties.Builder() - .addBoolean("minecraft:has_nectar") - .build()) + .properties(VanillaEntityProperties.BEE) .addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags) .addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime) .build(); CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase) .type(EntityType.CHICKEN) .height(0.7f).width(0.4f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) .build(); COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) .type(EntityType.COW) .height(1.4f).width(0.9f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) .build(); FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase) .type(EntityType.FOX) @@ -1030,6 +1015,7 @@ public final class EntityDefinitions { PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase) .type(EntityType.PIG) .heightAndWidth(0.9f) + .properties(VanillaEntityProperties.CLIMATE_VARIANT) .addTranslator(MetadataTypes.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataTypes.INT, PigEntity::setBoost) .build(); @@ -1176,6 +1162,7 @@ public final class EntityDefinitions { WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase) .type(EntityType.WOLF) .height(0.85f).width(0.6f) + .properties(VanillaEntityProperties.WOLF_SOUND_VARIANT) // "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head .addTranslator(MetadataTypes.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor) diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java index 1729b0583..eaa7b7448 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java @@ -162,4 +162,4 @@ public class GeyserEntityProperties { return new GeyserEntityProperties(properties, propertyIndices); } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java new file mode 100644 index 000000000..305dbf22e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 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.properties; + +import org.geysermc.geyser.entity.type.living.monster.CreakingEntity; + +public class VanillaEntityProperties { + + public static final String CLIMATE_VARIANT_ID = "minecraft:climate_variant"; + + public static final GeyserEntityProperties ARMADILLO = new GeyserEntityProperties.Builder() + .addEnum("minecraft:armadillo_state", + "unrolled", + "rolled_up", + "rolled_up_peeking", + "rolled_up_relaxing", + "rolled_up_unrolling") + .build(); + + public static final GeyserEntityProperties BEE = new GeyserEntityProperties.Builder() + .addBoolean("minecraft:has_nectar") + .build(); + + public static final GeyserEntityProperties CLIMATE_VARIANT = new GeyserEntityProperties.Builder() + .addEnum(CLIMATE_VARIANT_ID, + "temperate", + "warm", + "cold") + .build(); + + public static final GeyserEntityProperties CREAKING = new GeyserEntityProperties.Builder() + .addEnum(CreakingEntity.CREAKING_STATE, + "neutral", + "hostile_observed", + "hostile_unobserved", + "twitching", + "crumbling") + .addInt(CreakingEntity.CREAKING_SWAYING_TICKS, 0, 6) + .build(); + + public static final GeyserEntityProperties WOLF_SOUND_VARIANT = new GeyserEntityProperties.Builder() + .addEnum("minecraft:sound_variant", + "default", + "big", + "cute", + "grumpy", + "mad", + "puglin", + "sad") + .build(); +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java index 7d789fb2a..f93845d9a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.entity.type; import lombok.Getter; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.geysermc.geyser.entity.EntityDefinition; @@ -85,7 +86,14 @@ public class BoatEntity extends Entity implements Leashable, Tickable { // Required to be able to move on land 1.16.200+ or apply gravity not in the water 1.16.100+ dirtyMetadata.put(EntityDataTypes.IS_BUOYANT, true); - dirtyMetadata.put(EntityDataTypes.BUOYANCY_DATA, BUOYANCY_DATA); + dirtyMetadata.put(EntityDataTypes.BUOYANCY_DATA, BUOYANCY_DATA);; + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // Without this flag you cant stand on boats + setFlag(EntityFlag.COLLIDABLE, true); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index c986a8067..eac070327 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -200,6 +200,7 @@ public class Entity implements GeyserEntity { addAdditionalSpawnData(addEntityPacket); valid = true; + session.sendUpstreamPacket(addEntityPacket); flagsDirty = false; @@ -372,6 +373,10 @@ public class Entity implements GeyserEntity { flagsDirty = false; } dirtyMetadata.apply(entityDataPacket.getMetadata()); + if (propertyManager != null && propertyManager.hasProperties()) { + propertyManager.applyIntProperties(entityDataPacket.getProperties().getIntProperties()); + propertyManager.applyFloatProperties(entityDataPacket.getProperties().getFloatProperties()); + } session.sendUpstreamPacket(entityDataPacket); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java index fbbe2de50..e49ce864a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java @@ -28,10 +28,13 @@ package org.geysermc.geyser.entity.type; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.UUID; @@ -53,6 +56,14 @@ public class ThrowableItemEntity extends ThrowableEntity { age = 0; } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + if (definition.entityType() == EntityType.EGG) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + } + @Override protected void initializeMetadata() { super.initializeMetadata(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java index 0c8e437c8..231c408d6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java @@ -27,7 +27,9 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; @@ -41,6 +43,12 @@ public class ChickenEntity extends AnimalEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + @Override @Nullable protected Tag getFoodTag() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java index 66210068b..6c83b9dd1 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java @@ -30,7 +30,9 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; @@ -48,6 +50,12 @@ public class CowEntity extends AnimalEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index 2c9040b53..dce1adf79 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; @@ -52,6 +53,11 @@ public class MooshroomEntity extends CowEntity { dirtyMetadata.put(EntityDataTypes.VARIANT, isBrown ? 1 : 0); } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + // There are no variants for mooshroom cows, so far + } + @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { 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 b8ba2c94f..d4227cfd9 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 @@ -31,7 +31,9 @@ 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.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; @@ -58,6 +60,12 @@ public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + @Override @Nullable protected Tag getFoodTag() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index c8b6a6f58..0fb742f71 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; @@ -67,6 +68,12 @@ public class WolfEntity extends TameableEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + @Override + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add("minecraft:sound_variant", "default"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + } + @Override public void setTameableFlags(ByteEntityMetadata entityMetadata) { super.setTameableFlags(entityMetadata); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java index 586ba5cd9..94ff657d2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java @@ -30,7 +30,7 @@ import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; @@ -57,7 +57,7 @@ public class EndermanEntity extends MonsterEntity { //TODO see if Bedrock controls this differently // Java Edition this controls which ambient sound is used if (entityMetadata.getPrimitiveValue()) { - LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + LevelSoundEventPacket packet = new LevelSoundEventPacket(); packet.setSound(SoundEvent.STARE); packet.setPosition(this.position); packet.setExtraData(-1); 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 8625dfac5..a9c86ba07 100644 --- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java +++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java @@ -30,6 +30,7 @@ import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.cloudburstmc.protocol.bedrock.codec.v748.Bedrock_v748; import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766; import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776; +import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786; import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec; @@ -48,8 +49,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_v776.CODEC.toBuilder() - .minecraftVersion("1.21.60") + public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v786.CODEC.toBuilder() + .minecraftVersion("1.21.70") .build()); /** @@ -70,6 +71,9 @@ public final class GameProtocol { SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v766.CODEC.toBuilder() .minecraftVersion("1.21.50 - 1.21.51") .build())); + SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v776.CODEC.toBuilder() + .minecraftVersion("1.21.60 - 1.21.62") + .build())); SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); } diff --git a/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java index 974d6fdce..34b97f36f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/InvalidPacketHandler.java @@ -51,7 +51,7 @@ public class InvalidPacketHandler extends ChannelInboundHandlerAdapter { if (!(rootCause instanceof IllegalArgumentException)) { // Kick users that cause exceptions - logger.warning("Exception caught in session of" + session.bedrockUsername() + ": " + rootCause.getMessage()); + logger.warning("Exception caught in session of " + session.bedrockUsername() + ": " + rootCause.getMessage()); session.disconnect("An internal error occurred!"); return; } diff --git a/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java index 2d8db9517..39c3f5aa9 100644 --- a/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java @@ -906,4 +906,14 @@ public class LoggingPacketHandler implements BedrockPacketHandler { public PacketSignal handle(ServerboundDiagnosticsPacket packet) { return defaultHandler(packet); } + + @Override + public PacketSignal handle(UpdateClientOptionsPacket packet) { + return defaultHandler(packet); + } + + @Override + public PacketSignal handle(PlayerUpdateEntityOverridesPacket packet) { + return defaultHandler(packet); + } } 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 59cdd52c4..81f1fec46 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 @@ -45,6 +45,7 @@ import org.cloudburstmc.nbt.NbtUtils; import org.cloudburstmc.protocol.bedrock.codec.v748.Bedrock_v748; import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766; import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776; +import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786; import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.geysermc.geyser.GeyserImpl; @@ -117,41 +118,10 @@ public final class BlockRegistryPopulator { private static void registerBedrockBlocks() { var blockMappers = ImmutableMap., Remapper>builder() .put(ObjectIntPair.of("1_21_40", Bedrock_v748.CODEC.getProtocolVersion()), Conversion766_748::remapBlock) - .put(ObjectIntPair.of("1_21_50", Bedrock_v766.CODEC.getProtocolVersion()), tag -> tag) // TODO: Finish me - .put(ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()), tag -> { - final String name = tag.getString("name"); - if (name.equals("minecraft:creaking_heart") && tag.getCompound("states").containsKey("active")) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - builder.remove("active"); - builder.putString("creaking_heart_state", "awake"); - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - if ((name.endsWith("_door") || name.endsWith("fence_gate")) && tag.getCompound("states").containsKey("direction")) { - NbtMapBuilder builder = tag.getCompound("states").toBuilder(); - Integer directionCardinality = (Integer) builder.remove("direction"); - switch (directionCardinality) { - case 0: - builder.putString("minecraft:cardinal_direction", "south"); - break; - case 1: - builder.putString("minecraft:cardinal_direction", "west"); - break; - case 2: - builder.putString( "minecraft:cardinal_direction" , "north"); - break; - case 3: - builder.putString("minecraft:cardinal_direction", "east"); - break; - default: - throw new AssertionError("Invalid direction: " + directionCardinality); - } - NbtMap states = builder.build(); - return tag.toBuilder().putCompound("states", states).build(); - } - return tag; - }) - .build(); + .put(ObjectIntPair.of("1_21_50", Bedrock_v766.CODEC.getProtocolVersion()), Conversion776_766::remapBlock) + .put(ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()), tag -> tag) + .put(ObjectIntPair.of("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()), tag -> tag) + .build(); // We can keep this strong as nothing should be garbage collected // Safe to intern since Cloudburst NBT is immutable diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java index 4568d0154..6f2bc61e2 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java @@ -85,6 +85,10 @@ public class Conversion766_748 { } static NbtMap remapBlock(NbtMap tag) { + + // First: Downgrade from 1.21.60 -> 1.21.50 + tag = Conversion776_766.remapBlock(tag); + String name = tag.getString("name").replace("minecraft:", ""); if (PALE_WOODEN_BLOCKS.contains(name)) { return withName(tag, name.replace("pale_oak", "birch")); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java new file mode 100644 index 000000000..edc2543ae --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 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.registry.populator; + +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; + +public class Conversion776_766 { + + public static NbtMap remapBlock(NbtMap tag) { + final String name = tag.getString("name"); + + if (name.equals("minecraft:creaking_heart")) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + String value = (String) builder.remove("creaking_heart_state"); + builder.putBoolean("active", value.equals("awake")); + + return tag.toBuilder().putCompound("states", builder.build()).build(); + } + + if (name.endsWith("_door") || name.endsWith("fence_gate")) { + NbtMapBuilder builder = tag.getCompound("states").toBuilder(); + String cardinalDirection = (String) builder.remove("minecraft:cardinal_direction"); + switch (cardinalDirection) { + case "south" -> builder.putInt("direction", 0); + case "west" -> builder.putInt("direction", 1); + case "east" -> builder.putInt("direction", 3); + case "north" -> builder.putInt("direction", 2); + default -> throw new AssertionError("Invalid direction: " + cardinalDirection); + } + NbtMap states = builder.build(); + return tag.toBuilder().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 c0c006549..0b8d7f982 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 @@ -48,6 +48,7 @@ import org.cloudburstmc.nbt.NbtUtils; import org.cloudburstmc.protocol.bedrock.codec.v748.Bedrock_v748; import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766; import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776; +import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786; import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition; @@ -151,6 +152,7 @@ public class ItemRegistryPopulator { paletteVersions.add(new PaletteVersion("1_21_40", Bedrock_v748.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping)); paletteVersions.add(new PaletteVersion("1_21_50", Bedrock_v766.CODEC.getProtocolVersion())); paletteVersions.add(new PaletteVersion("1_21_60", Bedrock_v776.CODEC.getProtocolVersion())); + paletteVersions.add(new PaletteVersion("1_21_70", Bedrock_v786.CODEC.getProtocolVersion())); GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap(); 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 005e72097..c1ca49af4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -91,7 +91,7 @@ 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.LevelSoundEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; import org.cloudburstmc.protocol.bedrock.packet.SetCommandsEnabledPacket; import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket; @@ -1933,7 +1933,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } public void playSoundEvent(SoundEvent sound, Vector3f position) { - LevelSoundEvent2Packet packet = new LevelSoundEvent2Packet(); + LevelSoundEventPacket packet = new LevelSoundEventPacket(); packet.setPosition(position); packet.setSound(sound); packet.setIdentifier(":"); 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 2e2734410..9599be3fc 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 @@ -33,7 +33,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; @@ -143,7 +143,7 @@ public class JavaEntityEventTranslator extends PacketTranslatoru+c(WTDGz( zCai9L#AF}-<1jG;r}#kP1M&DuWDg_w72TqV4;r%vqlwdH4`#`H&~tBZDOqTRw|nmS zo&W#*KYqXe`DwZ6t?Nb0stJyjvoLL{ue2*kloAzgWI~J!m>ZMA3f!7)h3P&YxosLZ z!8|I1Sn0k3z<#i^ql^V!WkdAn6CgQku&tud)WdY;6U1Z9gnq&19+a zpCYCTKHmLo#%hgbbt@atToZu$u#E)wtTRl-JC0|xixSB(LsxLe=`+l((kOnZb<&kL zW3gDX2v^G7Fg?*lxVjF8vBDMC-W)=Y#Bba?DG5mCXwb64Ng{^?70V)`p@an~GOCbk zt|i7?Q5?lM36}=E@OEh{nfH9bz+InPThPdSu%HIXA71kY@Wy2qz3oAmRXyOE3zDgZ z&Z5!^a}+CB25)s`Ri)uQPSc!C8a<(ffW&GO4FQ-l9<3~3@n59@f8zJ#JET_+?)yKY zu=&R3!Z3?JNCNwx-o@DAbkO79UKZfSq;Ka<0M}eG=sC=SCA31_t%m||bJ8eM)ApN* z+zT}`HCA&>mZq>+CY{1^N|J?WiOkba@Pc8oW3x$pPiMw9JCUVLry=D3|2p0xuf$>jH1b8g10yJKXR= zixJX}oYMfvp~t`1y4`ZD{()}aK^92=F-9{oc)U4h#Gxg3E1D`aaXQoB&*RtiD(@{H zLGid2A4a3O&e=i5;a(_CPCWUI2_C6t!QB}uAY?wcZ3>c~JChlCZ11gdSUlqbl)Lr;~$cc)nxzz;*aXt7xh3-mb34w;PBkm!Ow zjD{so0TeCFr?GUAp5TDkkO#RScEKTDX6~R(=S6gp>ZF~4v|P6*Lr{fF@oGYvP`MR{8k;hXPDChQQgrIb9HR1|fwZZfWo=H)i0hTJ z#d?)KpJkyZU0$o8Q%Dq1cAyx`Y`t3H?#!BAE8(@;LRfNZ_9eyuYKEN~W8?vg0eCO{ zwNA|CRwv9zEDWdFLf~E690u3fj5;)A=16d}SpUysqC(rBi9upMrLSbt8Ig2(MHnm_Gk~5%!|O7?>U5P?n4^N N>fB)?luqWM{{Zdj%On5* delta 1227 zcmZuwTWl0n7dAzTWSK$obji??8c-P!K!c402tnYzoa zXb~Di+ogqmF{Ck}R!w}NS~yRQ!Al}YQeH6Piw1o*sSg?wf*L$CU8tA7o%8?a|IYXS z-}et+vHdt}n;o!Y=Q}REX%8bGZO2bs6S-&aw_~b<$5HNyTJURz$4C5Th{YIbbB*!z z`k5z4ws^-FmUr}Oat6t$597yTcq1U-eJ@LlNB*#(!14I`(R!R{+<}PfzT~^ZV1C*U zuiwJL=(s>`_yvZoTLclCgEf%z3=4nIkJLn#+zf7KkZNYpza>o5Zk+E7lWXD1VlWzwGB$cN~pyqve0^R6%GC4Ys9zyiG@rEU!G>sw{0c5_GK}+mB*g#mwhao z)wPPGy0oICmo#0@IQ2X|BDp(;%bmAr;R<}^X=I%_oj$>~o!6KKICs6+_?S@;OKML= zQ_}ftUdr?o@;N!LT6F20P}g;p^0?6zBFWu$8$G_XCt{)!b}z#)b>O?(=rx45CgS*| z`^tTW_4S9vT+W1(#3N+az7B__?eX{gIQLu#7rEzg&n}R}{(|WQ9^WKG51bG`K%MYK zGJxxc6vPh*r26vHwO0r4`{f`x{;JKnif3ghC-#>(6QA_IK%w8^B=TIa=SGl>m9R+{wUK4zNz6PtzqlQ;w0}Dm(ZU zj-^(~>61eaTo8Dq2E9CIh(%FVMXU4IPhGXT_Ep#5wPCI{4UakLiIMNE)ap|YBSClX zUv)!O4}P_KkQu#TIpEd^k4Ux2tS3X&5VK?$L)A*<9z)g&5<14dqe8jHH(I+tIzGlw zwPxc1^2%917vXC&`@dFBqa-Q?s%gmj^zNhp_oP-hEro(09 r2KuBPNzGuYIfgSu4|!o~zv&)NOAv{cEK<`)=5`W2PU-G$Noncs?ha`XX^;|-?(Psl;JfdO=l%FK$L#FH zIkPhhTuT}S4*~o@uVlL!VJ=|~C6>S@sT76c&?z;wb#!!yxmYD~Gv@|hP88&!aJ&E}*O_5Bx z0o_sK>5j`30XJ2_4M8Y*#te0U+&t2DcnNO%F*19wC`&?wfH#AyH+C`lqsA0XLFTe8 zS1QYVmP$#YY%{%l(cI13lAdH)$~pF~9P<{3zYdp}&?GbC&$KO;82pXQV-x0nsuN)q zF$h*^a+I13mL;$Xzvj|*=nK*-{8)@u9aP`R()E)U9mQLCpXB=W$^+B1<& zF$vy=48}BI)|-SK!i1%9HI+u?;yE;O6t#Cd8HS}1L{ex)VA8Gc8-WNmY2&hmAS7bJ z%Q7%~7;NYpfe*%f-`N_A9LtRqdldFA0a@Xlc~&Xc-K>sG^$dgj<8P&OgA%TvN)1!D z)_T@cYAga~AYUS>qIRx#txh7$5T361`u=T+mZah7)lB{j%H<>U&9dyuA zgh>U4QI&c)rHSm>bd_UsIF|O_qm~aJt6UK?UC)gY6~^JdDF_O;G=JRhhx=yF-SB3q zDDfi5{`lnnmg`XOugJn+r2%H9tG#cYk3d^Yt~{TI(9ikgWJ}Y;$zW7huUTBLZLMyX z7>xsD&&`62m6uJeZ_t6dhb}jjCQB#~lky}g-|J1`AUw*<@7@eK6GP~I+N?e#mUV-0 zR1G_sIMXVD4&*Pe+&|JPfDU~8W~&t#e9VVjJdhpwXA0kQq|c2QVztpbriQ<*20E|9 zpe>;Xv<~r&zPNNLr~jn(7~+5)haUHE;~Pp~jc4{pOd`Dn$e+m-%HKi(IrU{j5?4q4 z()dkPhpr*orwV- zE7Ytgux6B!gLdbAm zYJ?WupA8$yl~GgPaJf6}F-WKID#LYhy9$OoV@31{rZKvsQxd^kpb&ZQIf(EgYzWe7 zT1_~I6hf>8g(@fKn&cp!=oP#Cf__1abo1PY8b{Ido*5#()(cz;_OUnA-N;5{<^TC9 z%)@@%1=1Y#H(b-oXAkDylfa#mk*+k5JV~d+Q!)QqJ47S&)s?;1jCG-H8ZY!_RxcQbt8p^r0TiM(!fc-%ms!qr&kk;EO+GsEo>5^}8Yji&hjD z>}pJL#qM4XYc^eoWC&-pM9zKVFdthpAM-Cy5i?WH$4Z7$Y;?2VPAzqUTRlX4fNs9# z*HYf>ywrc`jB>8a>i61yUmMe&RN}O!gP`o+cqvf+qnW-dXhz!X8|^G^jYefcz&u8O z@qAO8W+Vop71~GtTn~Qr-9VJxWB*I?(oy1{`JF-+rrE?;d*9$(Y0u{!S{6STGY5)< zZ*?!cM5-pE9VpFIUah)UP>EixMyq^h-ycd-5yCD^sN9l!!6;EX#6H>y$WLfwL$;P2 zgPU5TAujxIdVh6-GqzPEYks`MSjB*((v`wKLBJ`yaLpuG|H%t$VkSsx6-rBo&w+9$ za_5Lw^{4!7qJr5&pJlp^ot0_=_iC~Vse)SZLMx6h&m@KZUz!({tMP)5U)tu-D%GU} z1b@5FrwRR5#WK34QL+Ea`Yb&PbN=H0y(QaFlQQ5bL@ViXLD7<~tM9qytv(Nq_e3W} zpyMJPnix(jU2ucO2OJdcu9=(CyxnQzmYZU$CQpd_rc}*%!Rkc@`ghafcJ$ zyO6+BkSQS?;(F|`3~!TlBbv4{x+en)4cK0|-~>^(Iz)cSKFZ^Yi{xQ7$ma$Kodlt+ zQ)BRlS+hh(h*(j%*EL`rq=Q&0Zj*bDpW)$?MCtt^a$&s~zk3ATbk}rJhIon`ZZA(G z;eVJ4y=RVJZRICM1GLc!?NIDYqpibWxLeQDl7EMmA)P4-tj}90Q4#&2>9sRPq^t?C z8K^R}8uvl}18UekbKLP4*-$i8TE878Wp3D;-&j4nc zIvM!uQ-C18R|{+O=D=U-kF7Z@Tud2{Ht0^F&FFrjo} zu}K0sF{ri-A)bM!21zc=*Hs1aRU*E+%_taPyUx9KG-LC8YRcr=K@mANT4#I1KR^)| zXZ|w%%ZN}_0g^47anKT~q9OuS1D0RFpSSCy2ybgeJ7p&@CB^eNoqq(dk13!UC5M_) zv)7+6rBsu^!@ZFqzh3xd=5>ux(FW3Bukg3`%AR@2Gf;51wky!A6|U|`v;j(cWtVr% z9W?|RsyKe?>-_Y5?lY+z8-G}|xMuFZ0M-FLpX8uZ7rZhAu?DEOZEdNiiFgf=K`PR7 z-Y>qs3~W>kUlWF~zw}4ICrL7!`u^jdsz4f%F}2=AD24%-u4~2DkcLl3FGd9;)8#^= z3FR-r>F=0b_HH^WIj9&C>~c}u(4?h%k3NZN)O9YQXj)5aP}5TXO9$ zH-dg zDa0F~Mpo#`LV>cBQhYZ5TR>e#|J@Lsk~$8Xc}S}}BFYk6PH4pA?%kpEAE@Z#m<8?Y z^yAQDKn;U^US>VR5IaJAce-KYYzYb;Zt=9CYU1&t>R)`BK#Bjemm2tNOI6~2srRiE zq@s@>C;31PB6RygTpT z4=SzmI_fQQO3t=f>+CNaZ_(DO1{}+l^P59M+|AsVKQ!hRh;ow zolw3T-?XGB9{4rPpd#7oz`T$PYwhX}T|matuL*lYQwt3&E3QnYdD}Ko%#wJ?;VPoJ zu`bjJo|#y4E74Yo<^WxcDE2bWo}x=xvz!9G@?89Pvm@#R={oe9hbrATvw@1wzrIr_ z%2fVbTqtfVYmhl?((<*q-2%L1pn0&9q4qG%Yv4**>^->j-&+%n*g#3+m!QBx^;LQPb5O7D%?A( zGae$f#Vnmusn#tNIBd>R4U}#zV$epE3c0@~9EB?M1~cC29tvV5YMo_HsGWt%bjj&+ z#nI_=+_$RDYE63iiAV3%IFWCi3tdxNy(mV+68M77G2o`wBT%pcCJX`! zNJ^P!?XqY_u@R~b?AWQ03!8LisT2+^$B-Acax+)EJ-rb9kY5-pa5{Kb)Qwy4E$iWx zD{c^)KfA}5m*Q7bJp8e1S$njUJf56#P2JVbn6%~es8F7Tqmw@?&dPX4x_fx=Zr>-& zGwxxZaiVC_r|J7v)UfH?k$hvEnu3JQ_!7HL{>%s+3jSOCET`q2o0ZLzDlz50!LZ56 zapG8B8uH-d<7OT>51zfE5G4D%QPfbpjql>`xy5cM;`xc<_i&zE8K#I)q+PC;16cZq zW3^op;5o7eQCKwcHB_fnx@O8^<-&?SS=z$=Fu*yVV$JYR>^u}9bx)tC?zLsB-5o$r zrwe$I*>xVptI&{^$CRO+I9doWXR6`1=JQjZZzdoh(?k*jh=Nt39?F`d^6cIm&m6DQv9A_eYCa@&D zkg_*ePw0^&7tpvbMC9YNKhm@-*1Em;g2hP$B}@> z1ms~4<$4%joH{p8CBqeR*|pXtC385%x;I}Ow)eFh_fDq5etkiW@GpvWd%n2&#uGd2 ztC&4K4vkGU#$q{(-+c#I2r=22PWFV(P2SvxnL%Orjh3<`DVZ4>TxU-s>PeGFYq`tG z1%)kH7nu?q99?FyMkzGOt+ppO&Fr&TUwxfz9<{4{id%)KqbrYR*4H+!jEH{93z&`< z?oa&Ppyg;irW}sfEMf41Jg_HW-_vwxA#vjw&tgs@6Jx$N{ehjg>^+>8>EPFZk(?0} zPula1FhXuZWSo$_?e0UdIoVET`SJ0zK-iV)x6B4T+j_dV3CTU^K;8}WGqHBvAc zkQ1$T-d(Y3Q^hQZ`R=H;T6Nz%WWfyGX&KBWmYpI~aGE<*&tf;T+myl|w#QiT{rATd z(qJ}$e{tb*B>3*Tc#5kLr4A&9M2g`@o4E8Pf`tMy4aN;G8)NHx#i*eQxS7r&=5usK zI?c(kf=3oNq-|S8h)>2VsqW>Pzo-i2_(BDCzvLKT8>>{WZo`GpKBl@p_D6q`;yX}6 zX-$esS5Aw=M=KzbRq7Ixm`Zo`cd^E-EmV~vi zO)US!a`l9lBN7g4w7EnzlC98FG=IfcxRjAYhL1L><2psnSn?xk=vCe2(eH6u{5&NE zW_Ht0l9!)Fd1pK0NI7IAX_Kx=!*|RjFT+IJn~dZj&6DKDX}h^JR$uZ#$1P$f+;@aj z0y>|f;z>Z6IdILSuEnNm(0NAIoOQTAX?#JOieep}*i4T3m^(f1MYw)=TlmpkgSU+6 zK&$>yCKe^aYqz)IFEXmLYj3C~3eqA)$J8|B+gc)8$4Ym^Sd_M_(I9!~u01E$-WW|3 zs<7`g6yu_w9N~U+d}))3phN8AckEvL4hi3g)(L%GL&_V;3SwQXSfDI?i3YEtvAP-% zhiJpE1-&QHw#uFqfos;2h@FqE3Fa(FD^QIs+a++FMd81fE&LnWl%)HYk0zyeYl{F z$SXm$QNiu6k^Zb0jN1cjd32OpP>zdUpxg^Bjk-~ojDpEG=DvYI^hY&Q??8KrSAtg0 zWy{dP@##Gz+kOTwqNUthqxUnuITsPlPA>)4n8$PDp}`?mQ^WcX-U}nQ!yZ6OQce&eh){Hd+cIYw25jXO{}2Qs zwe-%@aswhA@j%4`#MuR}j$#0Z>Z&eKL$qm$+n|L7;GF)0cj!!Y2tXvGc9@qG$j6Xl z=CSyXBoH{mT!RY$FHd@3lLWp|$=eYQ>S;5!cl8i;vQ ze!vA_(N5tqfxt3@@G!D?ao3~%4%S=tpmBu*05R6JuMU{!>iM(zUsC_!rXIur;op4! z0}J06kJ|=~4=qzA0k&G^*(6E>whH;8VbKKS2)ahoJ{T;oq-~cL3WTau@KYZYDrFn= z)c24786Cdi83oezbMqgRAlS>^f#eJJJ|IH!UXWQ;A?7@bilB7HEIz(b3?L_i^Cc|1+B{@=vmP4lus16?#F^OHNxF>P+O*Fk}tJrzJyp z`j`jz&ImwqDf9FW*nH1h_#RZ%fX2uk=0#$_|CCj4FBE{f^JPrI2dE5#9?GFvL-^sK zg-SxC9{|-d+p-5VkhGN}1t=i4EP@OC-D(MLM>zM>$US=Ul&8=@B0Nq10apPqrDeM~ zdCSW-2F9yN7;^-4j6t3a2Q?;l6m&MjpZ7l03MRecdU|wP4N#7b-b4%Dp8!o-O+M3z zAWeCDK{cr18vw??jBoodZa_)N6A9}xyaZEZR=w=6K@SHFV9WOO0q!1Xx+t&dgm@UK z_!`t;;)9$}8ln$>OnD8a;O`tUu-WLb*In>c;dq3No=3y&s;oe; zO>&DJq#$(4_P?9dIGu8jCWX!f@z-&$Tm8?tf-J1rAYeQiF6k6dpQqkB0;+T%oVGSe zcLV_5oP&zfen1dyI^G$GJDz5ZMi8)r<J&y;jzJRRuaAR_FWZ_NwDy zJZ(x{l<^T%m1sU*Hpl?ti+{VWWC6<4;}-3Ixvn{jWQYSw0soBq!{2vgERJv)QT73^ zP$;&-dA(ZMj|}$M0#MFI66 zv0IZT-?;%`@xtf103L{7r9CAT)X0D;7Rn4i2EqH8;Cs%%J0OL<Dk+y$r!mpNhM8bI`Y5E5lak%20S23zvG!mMJU;X0IHdD_|lE4pOFgIW5cOq64d8_ zEi|KUNWg5n$e^=}1JqT8zMMYZ#?qyJtql(*eOeMinJkKWA{t3$_9pzLDovV zxh6D#yrLrYcc5$EOmx)X_A@4-tOUt!C;qSP$HG95f~c+n`S+8jpc_U3F74`#PHM#&FF5!zIMIEC4c=0!#rJ)U<%(&PbOZ zuaVJEO!JX8P>}Gh<10geA(c1Nn0g813RnEgHn89Om*v#|dhSNVFz0&gF*vJvlgSYc zuhae59+?j))VwT?Jt*J;@rAf{#uAnQD4zJx&P{Gkn4BcQ z0DRQXq9xY(An=W#w-AaL@V`M|$`Jz$qEXY`DCoWis=aP(3tZ4FV-MoLu+!K5Z`2{$ zo%5rE<;cpz{xJb=A}&$9W?jjmFw?x|^{Sd$jGhpT)pMxjFeArxgtHf7%Ekr8!t>Em z{_9{xYV(P)iDLpf)UdY4KU2US9=*H#b@AuPThP$kSX;)I1?E=o1k>05YQRVzM_>O{ zl+$sG5JcsrV1(v4{^vrT!FI7#x7yEk@Icn{y<2@X`)-#GJ@)lZVNc`xI+s2t{L!P6BJigJz99Bw)9f!D!=kG!Cy?q5p3f|1zY_LJP+cE#od&;N_^=N2muNm_V*WS7C6m zF#90Y5Wij;Syp3ZPg!`{DoBe{aTJA$E1qH;HMrDz*6mt*~BQg@UdfvUe=5eoOWqv3+30A>t$rqE$DE` zjci5pg>tIdso`0c)R^iE(C+N{cr&1mMEEgw5two9CR)?0ks}r!tnVg&*>6DAWhu}e zSzwVKof)NWOb%3I;G5+(dtjYyB14`p@26lo_4?*dSI=}j<8ZTGO#+KO29d+HJZ8SB z-tg5>rC573fs-rW4${N>(4;Ti#b5Ud>+j1j$idYw=;@>xPT4ceG~;6Klt zNt1b|&**|?a!=^X?giN9{GXJ67z*bX--PYZ#q$>O6NXG0aFD(wgCC#$()hts+t~)K zfvoK?xkMO-Hql zMa=J`9_Yd$U|}BCe|iS4%4jE8t|1w z!gghd551KkqPwl#fdWQssQCULHi4463a2r;3(`}-Bykl3*JX(L!S)WwWY zScHegaQq|lhvK@}0fi+bOSf1zNujx)+Ug{AKcE@+R)$gY_7H~)ns1g8VPR+7U`i6P zT7>5<5sE14+|z}}&Xa~CQXqNGqd9ke@){lhX)`|iM)L%K!&)O{;x{$e z=z#4@5&SSTF36})J8U}uix%;`B!bu0m0{jJb!eIKbpr@=&*b>2t{I3$A3@)w&o_T( z0|14K>;DACs5uLwz}&JG)|6$s0&vXXF2S?GB+VBp2rzK#2V^0%8}t25nV9C#hR2(Q zczk}}rgeZ&+~*Zqph2M&<2ATl{J|2%y0G5d%5t9|SJb|z3++3bg+Y7MDWlcOW-NTt zMIt2g!G}Gs6|Epp6LDxy&;Z`MK0HnZuf1AS3i1tPErvI#&W}4Rk}_XLGo?P6{FxXJ z+uUKa7!cY>nwpmIKK=c&Y(!R%o&{w6ftttzljCi@wHWLepLjZ(z48F9+56%Q#){gW zbYX|V!Q5}bI4UtLv|IZ!P*9S?DGv(NhN8oDdeg2lFMrCJ|N1q4fUvo$5m1My8DhJm zNr(TIZ6zoEZwH|6sp#+2RaHuK2%hzz(!d*Z*iLJ_D;HmU|K!B4Xkr6I3nV~&q4`>x z_~p!lc)`}mpE?0`elm9aQ>~xbF`Ub$zuSedZUt(h*4n5Ek2hw3c#A)sOtSzv2dLxD zOE+?w5OoR_A$q$nCw1r{?+yvT<7_f6R2h3o3N=nyi!sBk}QP|*mF zRCPRHPFash59d3zPF2Ey-J>-;&WUvxG^5(dW8t)G1=O+G*k=U`1Z|cRwJ4;g>4Fmn z+uEbpaQXnN_DNC{=~fJ`?({*D6LeA>wQs3urmV-MBaHVp!YqJ@)6`6e$9_|kv2$R2 zvRt?%9OL6?Q0}b^MMr)x-_y z^pKVBDapgp&)4f1*(iuWe%|Z!mtysdPJj$)4xIe~iV^RdXH0~}tvS_qN6R_G3Bb5z z&-tz0FeRwGjjyh#2Z8eTQ=|ooWNjJ>Hd`8kUkGNPe>N_p?Ie z`*uX%r+|)}BUMmYgK>hid96KoWktOhwY*X6$Sf-`W|1r&9H{v=j?d6&`5dXDH#!gt zVV+>JR_+%warE9ODgjX_Cl7(F`k~2oZz=uP+DCVXHuXt+cfgFG&DRqYj2UE9s zXU#3WlnvcH96Y&i5*Ji9zE+i4LCW-2FdTQ zDpNp#3J5&HAp-s9=Gu`Z2Ri|j9F!NvP#~3foE{$yy4Ame+<7L?rT|KKG0C-&Z7OmY zK4SB<`b#2E8fk@9@V|$?q^caSIkH;f(iqU^Z!{is2r!UQlEM6K|3nRFlZIc@N;SMS zc~i8Jtj(qY@xe&rvn;6#+Epnmc{BpDIs@pVCDHu(O8K*o@(LBM-a67Hq(08V>=#Ua z#M;GkbL-^jQSA85-pP98$*R*ChU?+tJDAlOYV@XZ=MBf;&LEqB#)wE<6xA~fVe9wh7 z{@|9fwN|WOy`;oyGp8N7OwGk@EY?>gWgjmW1jU!`XA&<1v+2B2?B7nL?9aEF3lx`)G{9J+bDj4#}{usp8^ z)BOw2>+^dvS(vfX|0N;Atob?}uAFe&(q8OE+4$km)aM&b;?^XkQzQ_f5y^|2_sAb%LaX~1BHkE?vby$-^fZ>tV=+dkYv2;V~>T4NfTm+ zey*%8p^I3^Z%*!zW4Qp?#-f_9E;)wHI5&kuA36jW1#aJ_hL&@p-wYtt(?*TvMacXW zw@Q+Gv?->;F>AcntxjN9x>J~(BaS_p|$|U!?0qATIZ`+o?&B4QyxIaxRU($T#!eDE`Ce?k>*dZd;k3U!?@g8Nf~&&`U>JxZI(PSR@5qak=*jQHp>GmP8$+YfvG zEvsl1HjH76qzi)H?6~PAzD)W%iocK3%1J@)q znicgs15fLkNY8Db_lmVUZ+tUtj}HwaA7hxLm zV57MdCNe|Ms{e@Qs{L^A`|g~Fh723czE-hPd0nl^!)ujf)eQRHu>i6#hX3{fbyNG= z(RpT=Q17k@;xlAn^ukcsuV8@#Fa!>u$REH&%e4RycWE(HfEE&z8wOthQ(I%<_2UBkBk&< zHcX#}qNC#ZF$(|e!aCPUOiT>F`Yrqi1|0>@jj?UPhG5-0IT3TH8WZLWo}7U7H|^dX z2j#g=Du{R8YYz7YEN7}kc6j4S3xvkGlnRNDPqxL9eUKxM(cC8QBivX=f)aHA)!?+{PuLW0s}V_%w?a>) zbti2Q8rM=PL>3k+=1b0iIQnnixB3!g8>Pu)`p~ZP{hxEyXHxG+?%EO5Rzd!9jl0yzTh$hC5g<9D1P{_#v9Prc+4-h+B9IhCUW<2^8%I7RCI9xy0-UJ z;3^(CwdG5xbSqO4N!TjuapNBAA_5n2VblrQ@*1>Kb0F0q`h|yiOAOs`7zom}mxIac zN}~889)oh;PuG$6FdX^UxSwP))o&I^Z-7^;%iPURIOE7YW?m5+|tRT?S4| zU59G7URJ#W@gz}&>CwSKe`V6=3eoou5o*>OXAxW6l{(-UYduN?eg4P{vQPo@z6v;L zg*wtsxctVMBw5QGxVlvr4+b6KzN>Is3$3z# zkk%~5k?F`cb5!`DQY3@!28Vf*z_m`IY%0PWSMdn8A z>PnD(aGJAD>nbAahb)W@|D7e_rGX}`L^LKPxdN2?<+ndFi_b>BDU#(RfSx9Ton(1>d%2@D5xMCcdj$$WsU}@R8eU(_5hLA3@!> zNq`G-b&q7CQgZmC$;mhG0~G%Y7|^->jdv-UgZ+e_B$@t&i5~&ntrm$p&KP>EpW$T+ z*NYsEUt0@962vlQ^ukP=?lNf$C0=dwSFP$>#%}dTbgXa({}Jd9A)wOQhT?9lGJ;~b zqoj>}<3T+J^{xRNbPUp%&_(61xSixJsB;YjGCUD|-`~DxlZ~RnpJ&Ppy>6X>v^^u* zH=JI)QQMbNBFGL`{ItCB#gsKInLH&e_^a@HJe_PAQtH|CXI+8{)li9};&(Lae1c3= ziKUSpH3)oCh$AU}_T@w0KPkT})Tm8hC~~IPxjy#!8sS@jO`n~WFsl9y%UAhI*fE6P z+g#0=CM0_E+c4^B8|F6%4Ew96wPgPMH}Mxovp*k@LOxybb*gpPAcL-}d;-GNb`k*b z)M-U`7BbG~x@7VWNrpN)WiYJ-j$V>lb1?mxdfbwA*RWb6zmWWyUrE`wf5y`rdb;4z z=1J0&Yo*tg$E9MEHJwk+Oi3$aq>Pk#y}5B1_5o3fn~QFEGTO$l5x+-`!x_7nBmL{q ze1lR(^-`Qnvd#llkWkBAY^$$}%H3QX8~*z;x#5y^_3)?_F}mQO$bLDF7Jq$oLV_fyJhTwfpW*K+-!2BFa$Hg7gAu`cRb;ju`Q5In4a~nQ>3DWhQQ|K5c zXQ5rLXSS@NFvEo9-o#see?rjzRGy#ZQ5sQ2iOz4rxtE5FbF8V+5^@mEusMz0kdT(FHv<6)rMY_#Fix*x7z z^hvJugHXo%MVs!4*v?UfR|`ucFi4r@pE;SXfV=32WhQNcta$UU`9Ns3 z6zG}b13v$cC~5)OhjufRrza<5w^=QMb8D_M8|*dKbcIURpK@*yq=rjB4L)$pGnL4h zrz8f55lV?$C-Z5@gvuhzgf_LxRcm~oVWbzH%$@OF=E)coGPG_oworB*0)13jEHN9N z`6<2gkd@YzPdm-(BU7RFY1i{mP0B*}Ow|LE7w;NH>qE#`zgh-73LOcxoXA+*>`5i0?lVQIT2e*p{5)k$Sz4NUHJ-d+f^t$= z=pe7f@zaDAPZc|%-4|o(b&svBTH7X==w$wHgPdHyp?-MVwPnY~v;@}IB$u%-ez4n2 z@|{SoUEl{RSQQiZAezHbs6yV3JayE{dDEZ45`z(RAyGKPfOvdng^w1+mCnH^^XMKq3xuVYva@1cW!1| z7+39)R%XZ?>@$c-wQZXQmCITWaf^HWuNT~4zlQ6fGjmiRwNKPg)psov??j5Vypi6YZ%>Hacfdsb)$Zotbft1%c7CcLRk<@|)z z@k@h~Lh==sCVFAEIL>*LUiyRCeBoVR|PQN;4sZstsT z|MVofjgS!TZP9NeJEMe63f=G8EK>Obp9o~v_8 z7pY_qp_31$m6Mtd$#nCbst0|r|M=6kiy^zXN8-pS?x*rJtl22SLKB$`Ni^Nw33ChR z8o45_lfx91J%pjqSk|4K`6*EXi8M9fOsI1(1;#_qx+3>|-N+UcX=?5Oy`S;eXMScW z`jBPwuf0QqOqzkG=fkjwXKmwMmQKyH34_=|%;dF4-VJ7b#HlzAm==qsPQh_wX$+L- z^2{{(X(v zE$hQ0c^;Ca$@DkbW_Wg!`iXREwYm*k;g^<3J0I5h^4WB1cgud;+0gasL5q3*2+O-m zxisFoTJv}bqXS2=wGQ&h2d_JCLJg7E{GrQbwvqz$MrV~+Ks0GuzGK{2vcB<)R5*K{ zSN108o71=_^a7;kMN=Y<$ZW);1ErGpXU##N?@zx9izcNzn6$DLvI<{FMX;ZB*|Kp& z@?(t_c~vBj(~>CB${Hxj8A)D-i2k+sHI6t=dzh!RH;Lbticc+wHQFCW8OK^^8YsHN zX)=Imj>SToRPENs=J~Er&L9{UZl;-o0?!^*38iL z&)3f=6&ifx5Zd40=+EOJVwmer=-mI<+m7U+lJR^AqTwa!IDxs-6@sGLEI(h)CX3aOqj`HcQq1p=i>2_*b%Vj3FT@B@nIai8F zoE&BqcZkl8`CCb=t)~il?v8d_N&0r(>;!PBlnM1*qgS?SJ5S1_JDR^dwz7!04~tJH zWc`Fkt{w%=#Ev3&%tmT!pg3jqTayB2%iT;KP%5sNQ<@(zLYGv|gmZ0Q;J<|?08C(LnSf_$`J~~*&6;d-)DR5+^}aU3+L2#QaB0P z$tp|!7X8Iia@~ASnJjn^sV}Z?WM$Pgk?yWh`MVYsql@lOIAtNo$om+xc41s8 zH^Mx;^~i;D@?u9g5R(B<>1T9evu^ZvkGqf4q*CUzXX=XZ)Pb_mhOSwrR?eQtsN3ag z$b0epV~|dvOV6apdWx6ZN|<$hESB7D&43COqT82BWc-lP0#UkIS8KO??hfnS0vrox zrjlHBt_hA2QO~uJ9xC;UgkzCJT%Vkj-KckFBkCoqU4!DMca`;Xq%eqjIk~*YA2L(D zQusH6Y@F8_3Q)w8gtm6aH7_ckaFtP+z4c=}Ku@Rboh0XiBExO`M0pX~)~%g?1w-Su zc94OePggNcKl6-&o+*Dgey>dmyr`y167qq*)-&3F{@h4ndK+;P3x18<+i+5!%{m2> zCYYvC=BU^SdNmCvv`W-S0De*peaY;G2(Ty!nNPw&Pn%Er?nk7?Hi1{xXG0}fR{Uf* zz1?I-{ad#Ro+AE+tagh_<&(ZJJAYt0uI3I7#x+HJ4KK{Z3yncfrT=A2o}Zuip~pgb z!qG4IyCdz=2L9O?ZYiEZ1G9V^Uj%}I=hM||OP3758|`eI9ji#duI9o5;^ibv8gW^{ zUuPWP!MCjHjv2s;U=|Q&*aN-kh8vjb)(`YxuDf15@X!^F^46IRAV{hoIrpyX_Dl8~ z3#9lWf39*93tiEp>LAW;=tyAKqS3qcd6twZJfO?AzoHIwyS=X6Z|z(0$NaI@-gwWr`zR=*)alOUIL+%skC5Ja`-AhF;ua@G z*2j*h?J^T;i=UYX|I|0g_=_WdEY>B>ct}H71g%BM{$Q?4O)T)&YSlvZAT>V>q+K}+}>-#mVjI&<1 zP`jRVgq);6u81FVxQME#rnJowhP(#>gITVP|D4kY8R?Z*@Hyk%tl|!Y*}e)6McMQ0 z4`Gu)49w}djo-8D&Viq&=juwYg-{f%h;A0#DaaZYOF$?MP1SkWcl zkS~0OArT~1voX3HoWQ~z3dIlXA|E3xj}qHv&`^|EGrXDK7J;`P&1dv?@q&qs?%`%6 zr5bV0<|!6iUN~Aj`KQd&bA6&bBTl!bQlT8M=I+0^FC!4UlH#hus2Yy@5HN*wAJt-_ z!7{sFyFPG%HR}+U(Dg&?Du}DT*rHAif&Gx>2z}!S_Cr)+s0u9HylKlkMR;_1nN&*mes^@fILfxi;0TDWh23=k~qlXt7%$m>kVP^pdUytEjuty{)8G}gz zv*^=vvs+PVCHOWVsV-@~5l(VlF;GwwfiZHjxL~H+m*u%^NP0nZc{iTM3P4*kQ&Sr? zSX=W6M+Z8XAU3Qsp@UX~rO1%6^fr1Auq?Zs`2ga%%?WUj_*k!u5;_SNU7lDcvlJV= zmTjP~JPD4JMqlP`0|ZOCn|3UgdEltDxGJGXHvI^AO}8JD0YCvIH*+2gIlaC6<_wb3 znmFhDqr8za7`z$o(HtPIvi5XmaKhCp3Fgh&Ma(D)%3I=&m;l|9ijgs(vMDS#PaKoq z0u!Kj=Ne{#SL6lda=b37+h7*N3qlOo$Y>nBayS?%NjE*AOh|eTbom&}YTfT(k1#du z_4~kt>hih%6qraIvM}t2NDhrCVHOzXOaFjx`1wK z7jqP-#y{9>LEywv+jHlkff12UQ>8(LR^y_`Ah>RkaRoxk@z{Y<6cbV-9SsT%T0d&^+*pvY`BQA}VVa%(|EGQUs6; zDt4Q8tfvw)CJ{3)ufC-y?HS8IDm?4F4$xw92xB3 z|8e!zaZx?r-v}boDh&bx(j5YlN-EObB`qP{EFoReB_-0`(%lUbl1g`X?Q`$-^Zn!b z{Wq_fGjsZ!+1-2JJ2_E_qU35KM422io2SoUR2=}EP`a-my_Pfuh|N(?l7yyFoT<+h z$2?%3LxXh{Y}PO78k@i151O#?eAn0l!w?hjsKf6XwrcruaQo2*tHhEPWWXL zaG+Vv&p)x(VV0SpW1BCagFG@zFBOmqOF zqD=2|Pvjd;6+#Ws2YWa75hev}6@ZApqJe;GVmrXj28sWWHDCyc1j>K{4zndpKo! zK?UnCKl4qDcY$J(BPN^!;Rdke>8_Ii6>~c}P{sr!L2!tN8>;*5*a<>|2r6MgIc|z^ za-b7qtJD^YLA&6^SN4*DyRKa=QI#gnm&Lfwm`9%HoS%byj(e+cvuJa^5&{KEzOKAtfChEn?;yoA zagPG>H_N!5$_&ljeaRP?0EJ<63jQyYs+mqqbE5UYkS<1bGGG{C5*1V0Vf|tHFZ_P3 zdub&0bArNF7r#SLq9_EVL1QlV2zpS!^i*aIuIa`Dnhm*zzQP}@G}!UCG|e<_uU5g*(=Ny|W#r8uC#Li^eGKwb2~T0W9a;_MvMlu|UiPbm>V1yl+M2TwICL&UgL ztfjqj2wFA3VE?s%kTabO#N5J%yl?(n$e%&SFu3;Gyg*W>2C7$KA1CB*v`-6X&ojK& zn-LCpAqyVR)DIx2Bn9P|ILp*SHG81`vJpyM@g0G5DB^N1#DR#zqfDOVE5-OP;!?v4 z+5Sb`>`4vQe-U@(mS*Z!tf4}g+~0Jd_n&MGtp~u{twTU&EUsiM_MIZ~0->Ngl@iL? zw3ko(R7M7se&n=<7St{LmXTnBq!V&Gx95W#o*+;;2Ge@90m6&|oH1o5^WZBmHse|( z{f3}{sZ#hWaBx;U;hwYZ`6|dv>g9d+uk}$d0-`5Nq%kQ#zpjmqh2}fvfPRgbnjLZ| z{y_uu;%~$w|CKlQ^kRd0Rj97$IL346v_etXcHW_kAzvM2R?r)1s|nhxpK5y{9Q5vS z@h(CCgAffR7NkM>xB2qG8>1ZMz1epDB86%xyNN=&IpbUAmfNaXd|~f-;=7%$>~Dfw zes_MCmMGSxR(rRmTqQwVz>#QqBgUxA_nO+cl&v#pn!q`|jWkNbCg|x1OR;bwKV{|h z6DnQHNe^6SF?nX8O~ucgNZ%7tG#e{fE{OMoGBg{v)*Hh3Df944od$T*`M;;ushUe* zyc1V0lqPh`Rv^tEO?u{p5pLPvGva!8QJouAtW6Z-HumlJqA3YQuxc-cls|p(2bdg= zL9A-w=om&Rzkt>COesh5-)3sgT_e+(?4)pw_vF+{h1oeRTy!7L9+~GU$W~rfT<_;z zl)TE+@F<;HytNurie*0QP_*XITb$(l5?wicA1=cnu|J>=MgfPlkKx(n) z;O0YUa9TI;>G!6Lv_6w^(a#JllBOvAlfB;)f#Hkq$~;LGOP_-?Sv8UOyBUjRk^XYU zX)p(Qn@jt5Df0`@e~~!V=-BrNJIUKP&2%UIu24_wPu`VBo24j%C28&1oTWSw?-*BQ zF}~8oym(BCXC*(oMEKfRS+L~rQHq4ETR|iF&+!}uel3#RFopsXJp$I!|R>&S%PG4($&Ctzk@zYr||B%=|mpCL>4Kn7qRt39?A*(hEC;}K)ys)n4N18 zmsAeB=Q|sJWl4oce68Zf-!eD7vAMk6?YFbJUtatlIKLM`)JH3CN9hOK(U!XlTL)XN#t*g|f$+LSgTx6|#Y7znh2}xFL31Md!LfZ& zza(Jxm13eF$;~5nfY$a0Tke!x@gPRWx5cRT+Z~}A3{DLDSxK~*&H5?|&M?~NgLNhL zpV&Ko`1#e1y>1rdly>7yWc)N;r616TfMO3j`zvok$jY0@&(hGq-eL3w_*h(@XB==8 z;O9Ee{R{_UhIV6W}*8bG{x;XNo+TK)vu9J$+Szp^4 zHD6E)X-$#SXV+k?@9y&Z^;j&d;`b&&Mfp#N`!g=jsJI3&5qG}1R?5j$e%pV0^Gl*~ zFm!9(M07Df0Hgc!tpBfX%VUBJE?gYrrT2LjpEPEFn%4X^E!XE^ot#vDIHc-*>u}aX z(X@ppJBRY=`9cM+)$NRL$QgLDbU0Rr}w_Pj>)pPaLGn!$;-#A~mU`Lj|R;(~}wuWqE5 zhs`#=vl@2SI5H@k>O1V_0ORd9X}kNJII}g_t@1Uy)pK?S2UR`Q z^^c(+O*X8CC4!k;c!foNx-BU-Q}A!cZ-5bl8aZX@&ptLFwEoYS1mm2cXkgxZPlWtn=G(J zq`vKwBhku)<3D)HS4b71UEw-0d-a@Eyb_Pg;?ckpsgPu=G3%oNY4}`~ZSF$UVB9bL zBdonO)zk1BPe=E7t+_hxv|S~>2`o7ZOwMqtS3||~Y1XcM+-A4&UP!sBv;*hZWB`1q zTwNiB!dyzhlP4a9V`^3@Z!pBz@^r`iYf!blAD66dPLFZ>JPi7yv_ZW`!6Yz}M^0C;2B`%eD{${M55Q7xs1hw8|jscxtd;>o5 z&xEv{5SDU*^KvHAgsoME@WJB{C#j)$bAR(|(ND9QNpx^6nCabt&1tv1pDezKzU>`G z6cRSa^S$0#A3bs1+vQTtdG**kg2Td~NIfhuqq)Bw_vr6fKCKYI@9IlgKN3H9BYJ5y zFCsvN5CH$=cgMpXjzEYK?-jlcJY3v*xD+mR>g4n==snio%fb40w2}x4_k3{-OtwxB zpEzbL@xU`>BM`pdUd=}C`kFs%#rV6}=6o}b7yIlo8}uk^B`9&si7l}!%v-Kd{bw}M!c{u{ku)SIf*g<6IIk!HQ&d?aItvP{;n--(}u=eVO{Hm`bPs|stW-F zc1c@G(dqSD#+uc^sU`>YCIto3xsqJ&9fAjBRW7^h6ruVjt=Pd^-B%yFX4tGu3vTYT zd#9J90!3uZ?>(xX(9vsd&R19}CQstXm73_gvUm+!GK^kp3xth-*}{F~|BI`yf2%kL zQLrX9i6nXREoZ%O$7%;t%Oo9%#NIl*RofV+G=8wm-^9W4kEmcpJ}PN^i^ZBf7j`l( zxAhl^0mEILq{o(ug%S%$%z+bm_gAL4*E^bY57cT?zucGLOSSs%t&HmZ5<&gB+VkSH zwn`rHoUZEm9UgP6x6bvTBup5cT0hW!VoYyX1a+pS*X5L_(yLay>ZjL9VY|lD6b~fL z(pO17<(wwDNuAxyM0Bk?Z)4A`aa-QtwvauI zRba;C@q9FNf5u+thSG(X`y2Mk;-f73%`+vFG8n@0w?5}J=T-Q`kJJvV4Qa1N*Nn|{ zn!V?iyr#zkK{(lPF7u5i9j{|*4`gqza^Lf#;14G?3Y)~=q|=l$oZEJZ6T!Lqw4d+( z=rgTVGr5^~X^&bKLo=eX=Xf9&Gw9zeo80Ike7==VlQiI(%H?W2-^v3^He#7|9A6FZ zr;51;HYe}==<|!2eSQaQeozua6RCP&w+(DIu8~a^IFffd0>)ic!1&c$$2(w5UCGiy zZmq739>H;!1OAe^z?42G><6D6W1=0}?<`Le)|grIxWakRp8aWQpWprl7~!prCDlBg z(SeJ3%iG-RhFd*!x)6T)qDlw9B3S|5DT4ILi%U{G*9KJl;|cSdlk#GZ*`4lCwYUW#V4OP~V{=_}aDuFDKGln`TiMix;l7mWT zna?uUeM93Dj80{hR7B=AN;Azc#bRku@*x~8?bp1Se?6{Oj%EtU)yYWkYHU3P&L}d@ z2%5^=?>tm`#`P$VYHs`1d$~3RhJWM-<3w!^8~+I3jnZ68P>hT7P<<<^d26Y)Ad0GC z_U<*dgP^|8DtyPADs0?LQI`HkfO@gW^$s&h39(exAujUFZE);#4u}JqB7J`Yyp846 zq`f*V7bH#upn=AZSN{AH!17#2&BYY=4e#!tOK&^7D3R6^4c_@vsfl#oc*z8xz)}c8 zj;^?I*T?!Np)_1>^w@M*4?z7ecS-zxHKdbq7B1Fe}Z>g&u_WT$x>d zT^!}V0@rs#C7iUVfsh65V(wV8AQ(`Ga@4l~;9M-9$r=17!7*L0K*l9Qq&4Rb(H9Uw zbGM#s6#JAZ=m7D`?Zw}p5SZen9rdCp8Oo8)i-Ws^U(Y}hDWIPgl8VJy^ z(9D)|28p8igHZzfQxqE|Vs;d3?Qz5-L7e3VF((@FNdQDK1DB)TY@Dh0BSka%>X(rM z6bJ;cU+9-LiXZ`iDY|$!Ukm{~QrbN2JZ{5H&H@O3=<{|R_fh`_0YhCE>^5ueEy)Ca zdkz?Y8&fDt05e%m~{+ z3)U@RL2={iwD(b7tlcXIVy7mC{@R9ZgaHHfz{um82*Qr!;!k7sv7rtyqF#1X<9tW> z6u<@c_cEFc)(}ea8qvaE>jdS<2z~gkbfdtwvZ9%uSel2O(|^4f@Ij z3PJEs5&ruUi1g*cp`eoUf{PH6crtqllI{QR3`q` z=TZnTg4mzgI_o7*pc-KylQMK$@CKNhR?>!{ftZx)&dmo8)8fbh?8*Vs>{5vLrc&OM zf&Gck)q!Hjidpe##_o0?zGl$&oaC3Y`u24FkgG{?^cT z$WO579Hnj8Npyq@;C@@#_Lo!+nEChECJAGlIgTr8lN1WBTuYxvB%v%=j-_Nu|Kl6? z{8?O}&HStQ5t}{`#kSUYNPOB8b!ld#?u2ib^EmQ**{**`#xnxk57vc>=DQ@ZfRc-M>~b^LoSrGhc6 zSn<8~bW>k)p@&W4-OfG&1aU~*)rfZq`jv_bfTCJ!!g4?dR_?4>c{nGC0eXYW!$FY?7H;e_ z87nL=6!&em+mWZgqcQ|2#ZQ*qEP#(DPi^`gTwga)IK>)c`OHy z-KPX&arm)r_dH64vPmtEQ-9lyDGw-zNtJ|f62&QX%U6pKn{XxWr8K9@CtoRPwd}D> zUn30NCN&{5hw4`5dQ}8>{NIn9*m+I!4HP7Az-LP*s(B{X-rZqbt4a9PHP`e-ba;N6 z(D%h+)GNQO=uRxgdtaTC{fnWA<28Ou;0R3NiVD}+X;A8$gFEd|{imqr$*-PjgqP1) zn7>k86^Jnpe)h^w&*VvrM+Y9Y>3fBVxi(PrOtNS7wfI5%k0RG&o)Gm|>b2<~nyDh` zw~n;5g2BE2J~q5tW6+T@c|r~MBPiNT3kf7e{5UK%?1$H{Y8NT0&yv`7e8E&TNx_C{ zQnoEb5lM<@--V(2x^aaxuyvlz&r>M=&jKl$0D10}^ zbFO7!4ko%@-ovJ#1aqE2ZJobS26|>vG85K`=JT(kjd*h|Yd*c&MrcFU84JNYj{WIo z-=M$8sza+Q-Y7AeV~3UIAZ~bXmv45`U%O7fvW=?R!f~WSVLAQy;prOx0yXZy^mIQ* zT)-|P`uU{Uk9)N0^h3Yv#`}sG6Ga7|SXpX*nYT9?Ox!fZD!_LU)e;1j$~QNJ`nOb4 zsX}YfEH!egLPHhkrR1E|PAsv$14R^?O^RtQQFsNie9Y)7bdGl6L6zi|%N$JhcF$Oc zPYF;{e(rvlU+4_w^u+%nqaDvm9TG}ldM*7p$vI?*k2zy1^>)xKQ=oTGzku;$gH`1RWc0g50IDW4_l4XPRf@{gJE>0Z^uQ&1TALbW?i*c~lW)qPp$Ff~Ng(9+oA4=Gpsh zXsK+5%R@(Hd}w$M2_qTlMAkI}+s!!R*{!A`2c;3djA)qIAy5J~z&9A}%BLf9t^xn> z2;cI$#zhxjsDk4yZ6Q5vhZbxZu!jy zbO2o>z2Sw8%o$*)25W+64KV8hKs~jFx}ShY&quE%VQ{xcpfcpKqAcgvUCO{v&n2#9 zW=9f|<%VOfOSdI)8}J=1${N1=of){l^r!1CDj)z()xFfhacU1FKv*qK1MCyH%(^J> zZuhkDpz9r=VA zZb(XlQnQ-A?;K`@1i1Oh@{C{D5ackz(a`|}L?1?ROYdq)C1m=KY0OXgTg<^_Vb_M7 znJW!Q1E$bp!wnwD0!Vlfh8w29(B+;}yC||8ZuFq#UK7TF1{q-Hd*sX&GPGxip0#T@ z5@LE6Uy8@yq7P~P+ct3K>M2AKpYKmicq6B3c2|OaCd>{7;%~!*kklnE&{sX_I}}b{ zQ=l=^vn*bhpQM1!@PaP_$Bra}#*DL4ZEk`vyY+3h`-qycuc41`FoKx0Rj?pVQD;YINzE37jxG8YJ|3=uN%4R)NOeI+NiX?=l$*( z6(IHKU1(XdVX5A~qZyn;{!NG7S88`>j${engq5GZ3KaA7L6$?Gj>TGrHWa{uEq zglPrjIV6?v+WCcLW-t-p*09kq1!rI-M+hrMW(;RXSu=8{&O((dhJ$Y+iunn9_0ZwBEH z>fwoXen6H%?3u5HcR|wz!BxhX|D}UdbIi<@479o;91hfjP#|dDg+n0MOJJN4j`JHJ2IbNKbNPF^@rZU(cz>Kq<{ zY;kdO?53-E&3sP>N1NrNwWTMT z^oqieRnq6uzJ&hzM?Nz`VZOhVlLc3QvlB@y_>M^!QuYIRYSQ`%($*$WQ|wi+GiC`$!Y+$-q7$6exT5*i|icmeR4{^5{CAu@eJzWg5@v0GNIrxl<#gS zZs<@!T|k*^4hPR;9d%PnzR(*VgB4|*U9fPfL(Jc7j9^}40v)&YjJ$^P$Lp4u2(VoI zNn1! z!nf=eeNgehHOxN0a`+6OcncNB&{cxNDcDzpWs4x?;ObL1FF=NR#ls~~D`nlh_M(DB zsQ*;b3{?3?hdVtqR$a$_pyObRzOS7jxdLf6C%!Gcmjt^fthQ!mg#hxJ`HA7gps*Y? zJ$>Sse3cDY+G+Hf#6gP;a!(J#I|0Y*K;wUXl*qE_3Fr|R+etzr zz+K|-{%Scx-3v$>?_pSDLAwkYB7qwb8Yy}Wbm5YtU$UhI z7ljMnUW7nfb(AVgAthi|pGzsgHNEJ8*FzVu>@2uZz|@lLg6h8ryI}oB%nw{tfXhcX zojP;_6wW=6`KNBiPQqH!J`~#n{v+0aGmU(vmO8SP=)eHx}gh#A6dX;i-)Tga7(3Sth)Cf;tyFvfs**HMs^O$fmh8k5&z6`uwajjgb59)VgXc(?Og&N|^}$r?5uxwE@%(cgO`Dz%~%(#n_W40?9gOTtFfU z$+DrlK?;d5ZAMxd1=an&joa7IMii?&zmqFblEW37i7CS?sQ-{Nmohtfp+RLTM}=qS zpNV#V%lu2>&D)IuW=JUE7xY`uH3qha-V+FQ6uo`bI|YcpjTIN^#+~!$yHB&^^CF-i zElfXjUjRB}oYe9)8>Z{MnuKw9>j~G-1^x4Gi^JT?7Sxo5Z+@0g03E{N{>nJ)fBB2v zf!G4^fL9(xH-U7Q*sf~Hkj0Qc7+L^Vj|cTIu1hOfNV7P-!jM$BIM`5OjCoN#@ zyMa>^r-IGrmoRHU@P9_m{gejYe%P+5LYSVV=!FC2qzW!z8uK4#FZVfK8)!WhcnBQ2 z;j|q5_Th!5XjOwcukJ}d-|;&S;_<`5u9qeJ;fvz#tq@doG)B-uhva|`>N6(Xt)%&{ zS5fb&{iiehn)`v_2?@})^iL0T$QZfd|GL%hUD1D8?41V7`>FQNk-;%oyh|-B2*vR5 zoCIbl8aBS|cCAlSfESH}MNj{kTOKIDmMF18hHS)uzRd^URlwS~ac(_3V9p(fyf0km z7ye)UaAp!-`*-a!^&JWFgrPw)TTe)Z3VK0@j__K`X#k488PPklWDkTwY$N%@Jk)%7 zdea7=fsIp$V z8Wa8f)RT`mlXpti%Po{T9d&fM;V^kW$2<|64DI%_6v3&xzMhP!%{&;!Upb?{uw!;Q zqhG?0LQ!9D{JG^;m(R;wsM;1}^CB^eWpn7QyXS4ewv`a2tURVes zNAG>zGDX97&Fi`09~A{j{C8b3s?2fAE8}^w*Lt;EDjY2@n%-uq5(?Kn(OU`3Q|t3U zmMWp^DVjiu!=?MJYvfvp>l~`&t^dnF{+dX*{qRP>+uXnxQ&wL$Mt#(PDPMgP8$quhN-U3fO-f{6IF|0{Z z+f|S7{u-*w*3bU7;Jh!Q_1-Y5N^vR{dz|nlLO<%+bzUZ8hy7!GB6SM%?MhUGd@dX+ zE@IJ)qZb+R@O^u@)o1GV^9zsWVaHf~;OF$v*_G{9LgVm(n^V-ZW-aFTN)d3OjPvxK zBq?d_C9S0bLl&QibDkb!xlGZ)k2OUPvHjVB^MzjvZ7 z{>J0F_U0q!^}_2Pn!jPqC^u48q`!*g^YrhYBZqXp@G4%)t@uXKxumyUs6-5-QOlQ;(wtx%%39C6>6o8f~j|-MpCl_!Dhk!nWM4^%)Ip z_2TVuI{ACgRK1=OJz)cizS|wzD&nTL+t(EMw0sO!$##FfY@H}G$qL-pPk9fal6Ynu z!Cd{ncMAR{BP&A4Fzj_=#mTe> zO+fv@8~?x*B@z$SLr(Uj?EPQxZwioi4*0?94~ zRAT++tKVNgVt?E>_ zPdqqnui4jnfi0|SGfQ`y^gGd*7u{g>;!)OkP~w`bc77iEl4 zu%mbR1Ho^_zvr2iR)Rc}i6h?%+AU)M7f=l|BCgq<=-AyqGbP+z6 zf454+cQ^>pVIoswtn>q^%FOJ^Yzqk{?B5NFl(XisEt4V?0BjYCI$!+SB9x85NTa_d>fv5L;8wMh(*ga0i-OWx4(a_#p(E~ zjQ3aBAthv-ihF|2BdxMbJ_~i1I9)&9IGNw*2rSnI z?fTAlzjfgaC&stFh~r~v^mduO@xqyrj8cEJaz9pD7MWKr-R*G^@S2FEB~HJACUS=Vdv! zfh1q|;SmUADU1B2l^dMi$Z z-b-a^qRyENK;N3lS?xqnUwI*Zp@d=$ehc;3@}Y@%$jc4f!u*>#SRLxO^K&))pqCZK zvc{Zh*7x!ZG6zn&0zfLW^b^iy62#U=`*{ZJS34G+&JX3!fp{KK4Z$mGc#Yv#-qadE z=8^c|P4|t?>uxq~2T9DZp#-<7H6WgEUXC#3RJ?MQJYeaolc@zW2+j1SKj;AO0ZC(4 zKo@C38!*n?1>74>E=IXX?vvb<72v;cu@dAgxvBZ!O;?%{&)b;4PdScphQ1?oMa~3g zv>k3a0CmAM@}RqQ|1TSQm*#+-9pu0F&&Gm_+xRu*wfo#>e2{2gC=%N5x{S>HB4wr{ zi>ub^^TT`M+*fce-bjwfV<(9oYPMk)bRkOT{-Cu>ZH_3CDCK9Czj*b=5Ci1pZ0!!4 zidUZ{(L-__v1Ha;QG-Yi)DEZ!Q);r1#j^*-P@saaFe{NEM_G>EbrF&NHeK#xLU0Lw z5+^6OIj|4$%H#Y94466Mrum`SL{erZp*WpyQJ_OUTUB}yeN=L)Dw9`^YgunYLttp< zI3w?!4FagRTj*imK&ngqY$XmJJxTO1X5@3+WiSfX_pwC<-~lA1S;!KRBq&cy`vC=_ zva0heaJqAL%nZi#joNWRdn~(3s&G$9?Bp%TPg@Fh`V&0aAmAC$Wark9+}m9iy;KE> z3|01L^EATb4^FZk`uBTcSm090&>|$}9p;Q9cjs)Iz0GVoo=E}&;cKB@s{W_E2z=a7 zekR-HR#XI<;W}LRwiz+NycNccis2}XFYGEyPJ7Idmr%a`oR(zw61^1iRj*20YoFl) zv^u9J*Zm7B8d1V~&_Bfn?AxWmI)JLJdbN2&TKRiEP5I}Wua?q*qaw&z1KWfGJDG5A z_J-SpQY3s)KY2#u)J3O`25g4Gc4DYfCU$qcI%HmjKSmNKD;-_WmkO^KE?F{Q&C~LFou3oV#ASx;`ubSwe^Qko@aDK zx+S->u((8)dc$!=|LBBq5(lYbRf>e|>iQ?bD?No{y~v-0JOwQKe>Ic&zQQDt&GGoc z{r_F%QJ$6CPPO`(hdf4lrf?-DbipMqmUzFqpm?|&!ZA#&snJ}W_l8(uH_jUKydw065* z!ZmWg!vnPU%fdh6=p2%AGU(VnA9iMn6*>k>C>Xxi4&Ii(_otI83S!*)4kHQv-Z|^Q zAMtLD|A*qEhr~Y~@M*X#yM9fVHIRS2`n;}^Re++#=S{>$Uyf6dm8w=Ag2OOZK_cTC%g%T&_UmL-Y|#jh}cWl|+N%BHX2EL2KW->$RgxzR|Nx0>8?( z!W&fSRQ{}I7US`i;6C2^yxo^`s{88h@3_*(EU%mk%`NPNVWCb7O}^5vMLg8ZsdwW39rCukK{gddXB>A)&gZ@1G%!*y`}P}Z-SX*=;JdK@lCCC_+;o+GZc?+7IuS& zL$++z$))m>KL5nwV-(9)4?h|VA4XH4cNSLn&!`kp?k_?+6V9kKtdG0TjNO~-7m_sf z&xSw1je{WMWpy6ZdepjHhe%(pU ztb5!-{i!|H&RFdGmnR*!*t-A&C!z-Ub$V=b6!Ql$~Et?{p2@dp5qM0bYV zc4CdM%GqDf6dnubYz~fvcTWX9dAtSNKD&VVtG}x-!y(}C+TLE`dB*(moIl_fa_kNXf5kD{#Zqd zrb1Cys5eI}ZuyOB|J;#lHLx`%?%3tKxSBG(2C!9CFpISyw#X>^mQc%pumD%GLXkGMBK8Ns?AYVp5k$IT(tcUrbjALLKTX~E_6IuQhjo& zQd^lZCEum!->6E}*;VdmNYTGj4f$~Ownrfwmy)sTY&Oq6Pw`uyRQ`I+y=tUY~`O7Y-B1@3KfbB*t$oEDk10^^x>@E1_Bte2{q`4C`;Gg#wmU)imgYw z(8!dsrDn{2yY=xrPf_Y?+<5s?N(C^AjvMbf`#`NspWQ2Eu5z5lSr9vJlwBow@e5By zil{;nx2w`UOUjmtk^0b~c`8q_AJ}kLWQ?gyzt$@ibyi&($k3HqgnRcM8@V9%*eLr; zfT~)iRQ?;)5QauGT4j1Nqii!Bsf#ypicv+eZ4_20z@nIH|K|-BDurxi8pdp!C^t7{ z`ZS|#F)|xsABJo+4f+_g?e*_c`BJL=zdD|Nkk3}8Vl@7xu!NeYSP&D(A-nX6uOOD# zD0@(t*A=isvQakE)L?{YoMLEEY*G_SsYsk+N>OayZ5B_4RQ^XzsTE}rbj86RB=9`V z7aMjl;Raf@zfo7`3FnyBQY)Ur?@cO*PvlDfJhAJkOf7Y#FrjharbX>bFl^=4j_0B< zx=nx4mMBl86POckm41U3`+Q-X-Bk_AoEKxD!s%qU@Xl0YVp6k7TC?U2gZ)g)IOBba zmI6&D?MO7L(8NRZ$bBU$eD+$z?c$zbVG52mq!^+hny}q2A0jrRRU$2VT5e9ZCYLZW z;#7wtMY4HsgRJx)#Foc&17?=6)92hCvBn(di{=)gCXePnecj5WczxQ0iEZ6lW~=Rl z$1a0?sMCA;hj4BhkKH7yf|SkS+dX}s&rw7B)mvl5v~@|B&qx8`+}P;T5(1qfO+-m# z#5j@ZsLCs$zpQJ zM(qtyMm?|d7HlYZJZA#3ey>Z93H*G}Au({Klj6I7U5457Gf5-I)&K>AG~H6L>MMz` zWyjyE-M?65^9c3HRyi8zbtis@QNZ{KXqsbH0S+ZQ>VMphDpdx{3$YX(caGuol}r&SAsut(JspzFdRDfff(t`19v=N z+InO%W7tl?5%9&4!3x{{Q+uyBujz|JkIo-#^u&2Y2L zPp*%%@)^MtkQ*KUlBJ*Z`?8tg<(_wDRs#0}r;Si^-u|!>w1%@TE|RtEET4#VfwlrUsl$_r*uwgkW~8+x&sO)bS6rmk z_j$b0`MuNlN)?w;K6Svjj`9=_>v_n%S`YtHwxlG~Z3!oP3j{H_hv0~MM?d`FS%tWO zX!!hV+u4rIS_d|vkWdKG;u)2^jn}2dKVb6|#`Phd=;GyC6Z~;il{SMb5q7;-Ls6E~|Rwkdp_(N$FVcUNK zJkCKhWz5=5YIGmTL>r&3gNYj%jGGe*whu;^|a=_r`tT{~?nqhk(&c!kW{2&0P)L`fwLC+KuFlEhh z5EG2Vso$HpFX)N+(a#%qY;)CpDFI9=I{O#T&M+JFmArBPZdMl{Zto;)%{~y7+(05F zVdsAM?r8*3A%`pw{TZ&?4W9_gxE}yh=8{IJ+A|Mg;ipU!bnD%4x@Cp&9D6IFTkpSA zmTz#EQTgUi$;AZH; zpTY!A2*6(u)Y=(hh1t=#eU~M4PD_^bR|`-*YW5Glpx(n<8tW?FG_D zQ5f!6Vq^U%85BBVf1*q+hi(17F@||m5!9fvuU^-&Y^)I-zh-bKK&=~(XReV-Gb`vO{JxQC*|NIi z%A?9fqw}|g(AD23M-|eQ#tX0Bs~#C?NL9C#V-lyhlu49g^YnA31;6)nlxlgA+53u= zd?A&CA?wZRbZxlRJ)XIpd9dX8xJFC@x0!>NkFU7w-d^4Lr*2(e@tQc+ugAnMFG`o9 zuLo{px4m(@Odl(tn$+tcNjS?(ts z!l)mPzBN=0rD@=0^A&iSB^;68r^$vu`Z8j%K+qS3wwKeO+GNdZ{TN~MjqRV&9yXSu z7jj*5qB;5V(3R%4kafRD9TC_WkH6)Th(y(kGuAN7*{NpUhIv2n zw!I^lVfjPVCLh3eE%XDfk>zqa)-xaevki!Eh}y&xJvjuPO*xcc>V%XR%}L%RIIBsC zMp`aH_9u4^8D=}r=hM&`dzGHvPIjbLw|Vi&|J(@kX!QfA%xHI_lImQ6snsy`x=4^p zq2yPNz;5`_L>uJ{dut>%Yp?xL!3Q$A_4NySQju*k@=kxe)%;%D(NW*(Hcviz?Vr`t z(V(`rrGvx6gF`zLKHuLt4H-h(TEahgs+0W6Me3Tw&pJN_wZ%VUK8Ywt_2hduAGc7_ z;q!Dgn0MdnA$MxD#>;NE4c~BPMP@- zdY`xH4(TqD?(PuX#Gve-k zdzkbHT!6cFi!XkCq|T28g@*Wf^@}Xf&IYdxb9Ye2i6A@cA-s>g&4R zEo&(a&eO|GuoPWIR5{G-#es8F3xt^dBhci`bN@J^uan!hmO7_l98v?F{F8=FY!z4~ znx}{!sjYToLJs%b8gMkp5BK;TSpskl(Cx;X;`S_9lnAHti>HabYAhk)Qia>QL8w7% ze%G&=wNgU5I?VO~c3zK(HXF8M9hBF9#Y079 z7{B-R&!wCaamE+GzGmAPM`INbgO$9<5yLoSi3UV|gEjE2VIfv=ibFkac7y+=jSBcmEVxxh6q3uraU{*m+atl}31DAgAmvblo>`$jGW z+r-`%{fmVzIoR`xlOQJgeBQ*zJM+7F>q-CM_^bsaKU#X9!+?kl`hbH>;jsG|=Fd#7T(K|`VzUF`Y zsIqrpvr(3{(+&x$n&n&LFW7h~0*?oTHX?#(sL)ZDVd%o@tgBgMTX^5Mq1EqBrr*yH-mRcif>rmIh27`{!6TPwB03~RbQYyyx-!8;4#qz zG`p8YKCcjy#pc(_I5%{Z37xG^C%OX_i`pJ=bPtIK6F>nsyq%TbbUOUJ$&vz(*9P>oH}lyS%5NaY=+G>`5ii%lczz|a-fU!!0?Rk35k zBGoC+V7{i=RKcq?zOolWxB&{x+J}E>Ms$h|6K=87W<*p-g+?C^3(HlX4o^0bu>{(h z=<;Ut=FrHD{oPrlOIc1U^{o8e?W9ZpXh#{E_`8z{ocmk-AB6AN3?Qr{^2kUrF_*ll7IN&5bL9s z;Hoc)BenS=lYamD!==hlW$af6|P^N)KiP7|IQ)z1_2<*=L5Pf3Fqce@Gq{ z_?A-LG^<1vOE#&WD_eU#HIC0vrOKqJv5)cpog@#jK72f$vBFn@o+K}zE|o6HbiG4l zc54DI%`;35@{wjqs-#NY(>+|m1f{c>@ck;d{lWeBkCcLcR>Zu`k4U4OwDYG?(yuct zF$^ItD#eQr2aJU77K()Ks!Cai?(S1mzJ7@*!b*pSo%)VeN=jJ-?(V8$hA9dRo`q_0 zkYb{mFzhI^fL@D2}WG%xMmul4y=efBc=T+@!)d~keacLTzR_GB1aTGPto&iBs=3m9?7bOGP4+3z!%#E7) zc>3RxTr)P38?P^w;Yi?Bw{Ko9+dX*Fwpjlb-at!M#AC)(A9x#VwH^#NE-< zq4tdyzHbG$?9UIMEjlJ^=gTj5Of#^rr9@zoF09S68ZEa|7h<}Z_xyXAr$+j@YQ#Rh z9erQ&2Z6_B_*mOC`C@^ddNi7xhb&cR=jSQ&iltm^8pX&>$#UA=jN*edh8ztXBF|Dw zh1T--%(K_`K}4Hh<~LO)Uo(-a7AqI=rIfuLn3np`VY-ynM6OO=@?1xm)}|E+Vur7v zJIv+H1-vngyMwv;KGZEmvxJ7Vq_>YCV3z8yv2#~f&e7j8vWJe%!P{G*fa{f~ke(2R z7X3@BwMz+|WzWGK=YZ>$zac%3b}QNU@@u9nHj|r;Z!!t?an+n3aNCT7n`3`DvT8RA zjx@XwrQdC>lz*^9EUHGn8Dr};s;`X+kSW3Q{W3JBBU(~3M{06ZeR)=#x+%W*WPnZV zhXOvUmC8X$|DIpqyEakf$nXtrVqJ9Ck@u%L%U!!D1-;$hDj5;Lv0jYAl7G~0ql$G+ z?!Z#EDrR-$98%X31ZySo6Z9bO?FoA1oMpPeQLMsZWPw+-G)A8BCcSb(KGJk+0gm(dliyW{em5zwj{#)f+ zN@))dBgbwJ2ltMTRu7zacX);p^d5hF6LTasu0+Uf^xL=h94)C><(Z3|o{mHASf>4@ zVO7f=+8k!w#R|fCs7xfCrh?+gz z9PxrcuFk4vG_6hk<|>F6`)+WhPar9V!7&jD zOT4jgE2E#@3f_iv)uY5bkV41;4>_*s+{fz-(Dc)lI6xD9KS=-V?FVmR7ZU`V=SZ`a zal}{fM8|6ID3uO)I3fP~_mb)1X)GLkFj+hnc;Q2daw_ZXNRzzN$LpF%8JOf$?5(Ab zzs}a-Z76^8uVf+#8x7K9r4%U&>_D}YPm-ocUC@!}$m5W(_UtGedP1-Zyo*c4WTYi8A@}2YPzQc~jzAm)vMx z9G9WjZ=Y%FdW%cA@ke5=PEU12x|9b@uEB!BpzP6 zV30UvH$B$-@}g{z-st-#N0#F$Df>;QZQvD9l)ls>T?=eQy&<4l>;;km2&u2dU(z?5 zyLehj$cxpe=SR^8^(|t6bA#P1(bg?+oUoNE3!U~A9?r6vH%?vH%sz|;`{fB8Xm6e< z4Y=QxIIOuT5?)g|FY`LN;>ITeDEI<4Ba^@U?;*1~}2WLu&Wx z!77POUdRf94Dbm{M%!4oB|@Fs_-jhix}vCzoir&W?KgqE=1-@=@QTM-U%-~rY|73p zzUrpLI{~VB?q~geo*^_<_N!&(T-8`C*U-nDW-3!Hl=TNv3dTsjPg2q7c@Bs+%;dJp z!^4XxWE705b#0J1Y``2BcY$zksd_9nWq7lApBKsCIV4|p`ovP{5qKA+!ZHq%sR6msSkJcVuv*%o;hs^R=ww4|_<=yC zf1M6NOEaQP9O-Hwm3LeY8D-9|R%5KF_+IjXegf9iH%s+BSXu7ydj#++iCk2uIs5p$ zSn#^F6Hdm!G~lN6ZHk9X@G}dNHyeDTg%-YvUgtYso*XYjVk!9WR{qd472kxKAYM!= zGURSf;Fbo&@NzMM0l?}lPyM}2dmK6!+Q%j(gfm+6Xxt6cM+ZtX+V`4)@pP;KolthX z4u3d_?~7w(2$nHFeHDXt(Z+6&Asz{tRerEc&|AdH3+T+yOlFm;86)X2dE2TpzV9k$ z(|=XblmyWLJkuW)ea--S=DYFv)`Ol)OhXYdB9X*)eJSIG4Zz`d%F`&X6-@qKImC!@ z%$;X9Ccv=rm1F2L$UI%zyosN<9O4C!QqTXUpZf12%@n&Vel=R#SbLXY zlpp-Eykt+n37V99iI^_0m4Qz36KGaioNKRQ%Qa%>aU5;vq6PBR#YoW6$rT z{IN;k7e~5pMVZ>jC+WlUPQ?z;jgra6UL9ibl zGLK+-F={#<7X?vxxpwgGSzLzkmaS%BG07CcxoBhslx(MlOWeA`nd0^J>fK!>EPHAd znJpp*K4P-B0NK45CY9j8&$_=Cv$A^~gNEf@SfSS)glTELpJX25ibjUd-8gMj@`m`! z#DM0ckwo?+cAR}_+_nch1?$0+5{@o`hoTrEx%ma7SFQQV zKBh>h_KNIcHuM4~pKR!~l3(A&A`QXe+lDBUexE>T&8MuleZ1AN_Kla6`i3afm(Ru2 znEWGU=&6z*H-T=}L4DtiTOy`|8jsaCKSVGu z&9>HB;0z7wdB`$zf&j7P)mw71929H_aVX4Zwl<=fFD9SGKWlp z1!g}<#P|!F6;EG?Atqd1Vtqcqzq#SI$UDjnf?3PyJsxHvIs$1cW(w?(Rtck z@vm9$H{ceStqo@N!rx!!?@5m)5kjnO-56-_WfPO7^7)whxwR8=69xigN;R0Sf<8UI zrn^9&B#Zun^V%qD@|NXEOr=C;dwk;{Ekh73-&ushv3s7n@z#?uk+ZkXWBUbGG}lUs z0}~7!n?=uE^=acwc*tG z7y}xCa`o&0D}*#uTzSgOAS<~|PL%T%%)qUCd&xI{-qUs03#r!u$j)0zUtHG1Cf{s8Lz|c_WoT6>>X-*t;JpzRJ5ByL)M$M-gX4T7od+F?b8u3hjTdF zcf*q!g>4LUYAXhhPgpy3OdST!L|r0mqVT3mHJjdpJAdCA_I zdEe<_a?m@7@f1mKFwVSW;jf7H2XbPW4UGBTM>4wwRD0?v(G7nys6ES(h5fnV9+oQD znEQi27Wz8yY9kNj^~_Z%9qmZGVq?kv-61?@!}>PfBHnl2Hvef+Z6|28}w#!9!)bUJO1avl>pNCnjHg z?MZnC2&YLcemZQqN7^g)GlBJ2Y5v$vX#U{t*RD+7?*lN3ql{ldu^rbNQ5zG!iJQ3= zl^Ji{H4QO

z&BL}-vYg#bdH6Afz9zi96PO|E0RC}+=G4iV+|`X(XH0l<;!Rf%W| zrCnE1SsXfY=-Xu zxz}FuzGV9^%7LPt{Ah}5AM+JpBYJ-o-OC9nJ1uAK&$6XGVKRaB=ITk#Ae2X5%nzZr zhI$Pqnp#yPGajg$&2ct9@M9xUx?)2-{BeV5{1#C=`$JIGvSuU1t)={@LK)3dM;WeG z^?OHQY0T5RRz@?P62PGS zYX)BuO`RC9rR)hU@IuK_Iqum)4U(W;%k`vXFx5lR?8JM`PlX{oG@~Pst8ARdr(e=6kW*eF(i&fAJ~AKAMBD!}xwn23Iypp3-D6Gd zdXsR>rO~V+_u7ehxn)7PQ9Ah_lHWd;eA=P|@-b^6wmh{ojm>fAHB zlr+hoI~llB{4cJaQA|fJDm~lYtakVYqe~eEX}JPhLWY8FB>fX+%qXKA4R|VMofuy@ zYj{j2MVZ=8e)!-$Wxgs3azf5&>s2Ky%cuOdVYL#h_`&3+HLCm<=Tw41lve^q{Iq?% zf?hk*zlE=cqsf&dg=Q?tKUmAf669tgO*!~+(e{z5tFC-havbT0a542UGb^ZmG8YQs z&Vt|_@#fi5bhm<_XX+Xqq(dA;F5 zE*r>CCJgKeyJUYwl?cP{UrIt%13aUI3?2I%I$R7xST0{JX#2X;w5g8$5LO9P&6`v5 zr%2*PAYDprP_BD^GIgB%;nEt}nDQM2Y(p z4qFXX2(h>s%9t%W=&^a|19_&2;%CrsbI`SAef8LkRWWwhyq6HVJ;~nGA;cz3;LNbS zLk|NqC$h)%KurU5O=;go9vxf^gO`?i;&Q(EJa2)x2M!D{Zu#Dq(Yj1%qdzE0%#yqV zZoa6kl%{JLmwmabjeRw)GzCyy;pk&P<+c>z{qpPR+V12CRo$-jexIZEaroI3<)3 z_XrGc0acAlw$~|;1=+>;235?~zEC=+q=xCVh(+C?ULqTr-?lUvNiCke0=pvs2Xedwj4>YvxAeXqQm6)8FzN*6eWopjTJ(UjZ`OJA|dWp;Ol z5Su9NVG+yCzXZg;B-{BMKy)IiyL#8n<5PStw-_&Y#T?=FC_jfID93C8+`P09I3h3^ z_VQ+dfNL}iN{TZ3QO(D49Qnog%#nk$1ZeWS)w>RH%y?pd&^R+ye}f}Rf!R$G+?MoJ z;9iagoJl7*{dBANelfe<8MnTady~bmeV5$EjC+65xO)yp11MWu12_NaW`D_iz{ER= zzxanL65oM7OgOmsSr$DaJQ=oaeqDqNZG{BCA@tiSzF$B}X`BaH!M-}=Ie}kD_k7qP z5qpVSXFlH_L=$)+NSOc3O07MNf{7M4?ezKPM_dDWm>f;?`KGY)ck+V~x0u!EJMGWf zUuKXI?Q!z&zashUOBGqS+W`cQq;@8$92%D4Qh+hA8Xq1}GM>a1lwWYVYgy$;V@#QF zq9I{yB;77btLn4dRL!>qtYy*eq!8jnh>ho3vtQ)lUk5~HH+7?XP;+%H8X>I#f!;60 z4cOrB?_qaQ?5vwThfNJ^rTE?)ip93Z}fHG8kTgQ=ni98i-Y7Ao6Elu<*gnS90Jn>K6X@a zWRVxJdARDsnFO_Yz+W=_wl|D~N#?ba#k?0Kd61d8Su0l5Lb^pkbVT0f*HLm}IQa;x z&x(JeF}YvMy?7z>@BymTZTj6pzHXP;CMV`)H~V%wFhkPW&!z24jb);fa7g&>DKG4xbjXjo3e|`Bzt<`b zlVNSs6oE^PRRvk6XoTEssLS6PynCWDQp{1=4kQz+9SZ5*Nht|G6uQVxC6wjSQN(?J zyYk}&nYBU@8Q�+W?{NWTQ4)dTL4%tnTEu1q$7?6mw*@1Kp5c6e>!CC~OD2=(3(u z3CDSKD_JD00&GlpKMKOHq=S`}Tmn-rZ{G!E*(jvt(TQb#2|oS_gH|4~pz zPxr3;Re~nty9EB1MQ=%-K1x|WX#e*VZIbqhpa*bajPCDu{gOX^xHi`X7$ISK70&NR ztU3J%!=$<*c;tP7e?<3N43+9ijo=Z3%CkeIWKPj}R-nhG!11eXI5i&9;YaEg;*;bVZKG|ZtS{dE_+)p0iFS2rr-1&+FG9=V!Ev{e zl`Fzog5V76ZFJgEvINnEIH#@sqA9C!R>}1D84FFW!YoL7>Wf0_YGR}0;oBU<6TR=r zC86?(fAZcJiZ8pW%}=&I6bH?A(U~R8j(qu9QNo5iR!w2Vdt28f?{)e5HpYaNobjBY z`=nzXV(Pcu)I_dhvg;Gs6B+UP4!1c}XTr7*! zsQo;|-iQVCQzsK71QMw)y{5yWT|Bjl=os~1v;odOU%&-N5y{Tlo3TfR6B2XHv=ujU z!UsoBV$Qqn0j3Vj8U}bd8ralC6hBNuHTcA1!bM*NRfuisZzsVE6?qo>B6uEa?B-M=o}8=(GQFg(cBq=d+zM<0{98f|v~2^-|C zI7^0{k?HD)9+5Q0!n05p?-SMbrPuxhM90Km?Ruby_!*}^vmo(+=oVANu`7t+nI`L+ z80Gmj_w?DyX(b6JD<#{NwC}IL3P3sf{y65IASk^xA6c&EcuHN5!GK5dS0Q0}*arz1 zYnV7n;#xZl`4+X8GXOrRn+W{R${tQ)em^ugM|&>i-o)h|&R<4n{+vwbC)~Xh7^`vg zGpqi7!}Iu*V!WLdJ(}B{7%1Fdn@&f@W~nK~IGe&P#k-(RUSc^$pDvKEV$&ZL7@dh_ zcKv~DpLf01TOjSt_eyboC?vo(lyrh@h4Yy5qs;RHOYcQWp2?Vj#n+d2U3{v7K-QNZ z9%fZ(P<%R5aEydNR%5`E7waLTvOOWQ^}Wks`g1^&WnckB)+o?#k8H}iES(eX+RstB zMx#ExMdbJw)|8XZkCWR#JQ3=B#y{&Wjq4R`S=_pQ;kQ^q2cVI185}CZMgQHYb(3FhImreRqcgtpeHMRLUaajLCejiSI+% zU(k7|`-$~8&j@FGFZc5_)>w)nu?48-)kkYnyXOl4eia=Z79yZpXKUj~ydjYmoGGK# zup{(lY3L(6Ur2~EW`U&M&GC7FwVur#z|kK=kms{L|Q*-xd1KeEF(;M zu3%H8A!e&HjM@D07D3Da=pbUA0MSi9+y)^rTVryvU!h((AFF{b5^Rf+v(Gj*`!L5oGz10Gkh%pCXj4<>Hep(kzEaVvxS|CIbaf) zZ!+2;^adG=pcictKFK88^+w3kjbB3 znelCa?dVOytzD|(PXwNizIN}M2Z>~p{~_lWiaz~{l8>{uq+EUNSO(f0o#`d>KF-u^ zoZkoPt8Ww)bj=y>g@VOvd$@@!rkaAF2Sj~pw}HFPG~K6DouF^}FyzjzDAPyGR)F@CVDEo@dgJq?|)%mfxBiMgJ@U zB_C^@(TGbXbb8!soa7}z&P6`#5uJ#-fRoMdu%5ho_L72+^}Acf)<6(}WwBRUBms?W zqco8%s}chr^u}R0mb#$}Lvr&Q1>>H_S5Lo~%K9IV>I?pIOMDZS1a4#UXGu1P-aO2g zXA2`sCPzW~F}MHzi{tjr_JqMqe*ax4y0_5HGQ1*fKjdzN-dpS*qLQ7rfA=U$U1=q2 zQL2dY0X*Ttm$H>OjjyIftIhc1L-IL?5^!7d7WNZwP-(MD^psKlrT2(HkFDM@g z#Bc6=(M(ha0%nRkaxW80qT5QL*CcE)kW@I!1lJnq7}UGdYDb#P6RP2W3lfzxp|?zv z_?IS<-C><%Ea(Un#}-F1Lu~r#T5;dYdQB7F7HT5g4F>!1k=j(ralE;I%*kb5I6>Za za5_(rRBwjUKHcC({ZjZ*HRsnBSBYFg?lKec7ZRe0{@?;;l|V_wsb5G%SQp`_1@`{C zXZ)c_%DUea>82DcOcYzXpJ$$Jn(&KH*sxQ|d7anQkIh?E}<@$sv_b-yD`-nc&R-5{)C;pdapMU{_;Glx)8yQ7}iY%%7DzE)SYpxWn!d z;01o@9Yo_p;)(O{X(j9kM(rQk8kC5epZUU;yNC-1LeSp@6LJ33S@w%IKxqwBviccm zdMtg0IPEXdh|&w_2ad$FBZB(06x;QnSyF?xHgYqfYB&z#(Oqi@O4=c?AW68zni#sE z{fMcC7SuE3qhP$2wEPY@ko&W-kP0-{+_Y#6cue{eW@h~v9k^gAHO2)*ZxPV)?3oW; zN8e9(J);3d4O@0-phGY4hc2E0pg|JxE%OY7G;g5p+JE`Y+Y4QwGu|}A@GpXi72i9d)&vQ2 z0%)PGNiN@$`f+*WVXPx2iBQr1hnjD3y$ZVNmF1Yi0qS`q>mLUVL)oXUn7(oc`BKXf zy6Tt@J#I%=@5{jej@HqMj|{d6z?kDb845rxirvb=SZI#aC0q|eK@o=)!Dti&-Qwfg zJeu0GsCY1KpYbZ!Kq45ib{@i@3!L~a1R==WAQ3mEqW;Fvyy-Y(>B9gtA#=2s_x)4` z?fG%ER)7%ex^-h#V9Xq$6%rr~Rfm7+;6aa?_rJq)dgLjuBP!o!8nH?jHUTwqcRhCk zF0Cr`9*704O@&`S^P%RM0m3N``1YC$v`5~8JPg<)U#Gfm&}+j44OUP*dVn623yT#z z2SsLk-|axEBXb~j5n$9Gq`C})W?%OV{jJ`rW#f_3SnFzsj*$;t>F`MpcCNKzPl+hOW$@sKXPp0?-Fox?gn& z2N*`=SijxzH^t$GMuXD&&W;SK7x)xWjswCwvQOjw7po5LnIHzVnAQsp=kcNq7-e}s z9n8!6s@ePH*eK8&p%lZked8(X-$s}6gPW}78`wsNCCFt?^uU}SHvU2M)b=Vj?t85TBP(Nc0F>wG;luX!3nU7`z6-kfBawI{&PB)KwekoG# z0uIG*_}w{}{|*gAYv8Xh)%p1B0yy-#zoi>3BHTb{q?!O;)(1tI{gZ|NA2{{B320O2ZkzzEuKT0$bq2QeyEF<3H-1Q zBwF`}#Qg^=c=MXw0|T84O3KpPvO_QeR1@ zUa50K3A<%X&Ih=LC2%@DR?YN0+(2*e6)`~v9q3hrg9qrB${MC1W4i9peq+E04}5J~ z8j6rb0nsKfYm^xPziK(mCwwu%HKW*i9M_UzsZeLt`p%8O^r;^%Lx30dg|@fC-zkEH zL+;t869Q6zS+hOz30j$)?q1lCi*C?ko#wE}ECxAf&Wu-(W|%-%lYR-Q{g>wG51JyR z&>}J(IHWZVslkELpL+5^s5i(h* zr@2$h$^R%zYyiv!h9kHA%cnXp#kY0EKj(8r!RnF|6=&nO!UAz| z`B6=~o`bP?T^5Lq3cz*{#aF$rjs=MlIZeWedIo~Rd)hbu3yLF0Ji;#q9gu30u`TQV z3*Xj=ajdHvfXdCDR~#RsfmTl7&4$^(f=#^<4UYybKB^5aCP1BRcY69ZKlhV%7hS4$ zVmOf=6dDIe2#A28&!2h_{fE3G9Pn5Lz*k$!e}& zB3Z#Kz>Lvwh{+N{1*ZO^?EIB3vG2{ZF^B?}yZa8~wfA>at~X!a2Ur>OJY zjXpTVtQ#H&EP#c$KViZX)+VDZUdcm?W+k1x2VA?`TG2=Xt)tv{l{VfSLp6D#uYu#& zdjh~%M9XQ9d438Uu$;l6X~inKhS5_%_ezUHx5>OHyUzR@ z_(t|ZCnrMf+tIBvX2eIFm)+ceIeqKWZr9uv4|v1aH4h^XAqTqgleXPCBo zsehYvsU@Ec@XZ9!|N3$RkO64uQKN0(u2RaFwB46t)b_8ho^&!TTh+k`66eD9fJwZL-aq@tFnwYNu<5am zXuY~N0_+VyWAhY;%%hWn;bL;`yX z0*CQS$WARt_2KXun};hA&?Vnvup)_?TVlZhlV`Bh?eOb4bReHKXv0I6jsOelDDTfb z`hf2X9q17hZ(odjK`YFMdhveM8bZViLhxRYvs@?OLYod|Z^ZL|UQ}JksQk~1%dq=&-L7Qpd%SFai1uVz?rlDhQkQ5)c{&MrdrZC=41prk;TY6lA;e#30~And*B<< zUi_AiZUM_7seADrAI#)7i-svOHQ0nzuA_ASbK{(nsQW)R)&p)T=5g@BXnqL^BmkxG zt^?hAi~ooLYC&?Osjmks1Y|$H<&XOR+?ZZmGyI<$cx`*QBC%0GviJt-9Wo`LTSQ=i znYJGAT77807hTlWLgy4(|8G+vaQ_gu@`=1q6$9F$h45301UMD~eLGbkbMS>t@rYiq z9zNJxzP326fp5D$aL|KRO0GySz`?4{)*$e2ShBH*L_>Eb&A=1HF$nmfbqe3EIEe9q zDq-{I8*HQYn4!vUah`CnmQJMQ4L?JN;rdQzsA*9u$jpB0>DT!zp*IHoXg>q>*mBDL z?ZRDHSv=qrr1sDtf;eY^^m}FA?Mj2&wcl0!CrniE8%M7FWzYmP!U|-=W+remJUc(| zhX~w(_8YdmPlZ9BMEiX4eFFaNKUIum;7&~J{oI-e{M04scsd-M*@QK!Kb}JaYmXfi z`oCSOX2uE;RMZW)JCp*QXnLe41nFs?f;D0Q_M+f>tZ7WbKf~leflN25_j(APPInP; zw%1s|0#janPg=gnoerezNm&ijh+EjCj&ugMy^+{0ejEg)NK#?u;5A6)Jn6hSJb+hx z6ckRO1~QSmvJoQx?~8;(w9gCBnc3CU$O{$8GsIgAgBRDqNMOIU71nkez6PpBYGu9Z zH-V2-2xxmERlt;k3GAW<^+OZjb-Tcq6W)@m>wjM>o`>K3rShC38K18Hh!+_ck!#wq z>T}v6a7=K_^#b-e%_oIt9jjy>kQ>7)&m=qTV@ASb;D36q5IGa_2ZWjjer0aAP@S%p z#^1FL93N*dq@Au4dg5G#=1%Va9BkNef9Xd<4L3SO@M0|zMa_65g6W^E_?covtD3wK zN<_QQOpm_XWoRJE?a9yRKawHE90)IseY5Wy;*uinrPV{rKTbAa^8$yG2(1PIp?_X| z0yU)0+mFFz+eFIbN`R$9*6AJgf%@m2cxNB{@aliY0&4Dr!Z#H44N=e)>+jR#|L0K0 zkstj2J~}K8@rh9rECtlZ^Wd}wV0b`bM87ZhP)JEl6Tv-qDvu@fgn`ol#^asOHOw*& zJ`hl`lDNRlC;*;2D>=jS9yD}2ZujgLC*#-FXU~FGX!$o|x1JvY_MOI?Hh@ls9;!FQ zP~U02^n!adP4_Fzhem0Vm)x*;1dVd8%rSc%m<~`y>s?=RPXt5&GeUAn>&_XNC077F zWf5_`|9Pd}ey5})40V-Tz`G$P777a66Sn>z(yQYylE2F%&Jd6)+G>zc66F|#&+O7{cmv(KS*sC>Os)jmymxqt$#O))e`$5! z>MX{k*5R@9%;TVmzV??cpFB*UO*PmWRHXVLJbUns;x^EaLiZp9;r)q6x9@rx=Iqnd zU$~B%qf+ndb%gJq zOi*S@^5KTDPnA*cA^5D2lOC_kjj;ceDK_?0!nP8!3r&}oQCk6Kdg>W5xS1&e4SS!VINs+}|ve1IwFG4yb```31 zw227@{Zg5pPLIts5aB0A8LZ9s{KxvLa15%r^3uU9XTKv_M`vW#FHB@d%m?fSt3D(d zDX$+tGYq_fY+3Ek#)uGg-$HuSy)^b7EPR~cW?rR|2N*M3&&6UQNjEo>Wc(wtY~m44 ziiNq380Cw!l6@UAgL=%zk-v>5%^vog&kW~;(3e^)LY&V&7%BzVaJ;w|dZUeRO`Sw` z9kqUjF2*pP<$w1bv4VNQl(B5AnyR79JPMMFGNwLISJ>f&63YFiN%Wt|W;$9_afPKL zBuLqWM(|q#pA{NacUGbC*4sDwb|IXsST5wtcdShql=)Z0*d60_7i8V;!B+{!zTD0o|0*AYh-L*CEEvThH~59|KFR6??#q;76-zRGzw$M?nNY zqwR~bsML4uy>eY%D2P2jn!g-(qHCIU2+;h9Djx|ue}-Y^xoc!e<&aB?De{Mq#MW%f zL=>S*{;41lEFX2?KgP<-N5TQcCV5PWd8n6NnM)hx?;Rwg!=3i8cdA0NZi8$^f9#9a zq?eCGoNM@LuKGEE289kebkN?}Lq?3xIA(u^f|*fH&3ud~ANd3rEZ0(^J?X;K%$Lf# z!23E2H(v~dt)AyX`?4Gn3_IP8(xtDium=d+ZSV~b*bW_SAJ3v=VxrEUq49{-|CtXZ zD(>x^SaH=_ob85E@v~uu3`{q1TNllIuUs#%c}JtnmO%#*UpoiGC<@*QxrF8Nw8vN?Fl=$QkU6V8q~? zs7zAqv+Z)%K> zl6+XdPlz!W8$XV>AyT2#kI@Hvu)jNm4njLUPTN`vdvy$P{HBqi&<_`#l|=j5c*5aG z^YfX+*n1xr@8*`kJ`b=$->s{q0erfJJetOXiT6#VlvEGtb*PV^;37jo!(BL`gcSG) zynOqn0ha657-hU%db)mR0VqzBjzLn|_x9odPKCUfgdH}NrU^S&p`}G9VqeBv3be?7 z*_4;bCaQrfaNerxh!x$tgK58Q%-_XbBE;vu{o^W(YGQ?B^W9LAkVMStvjWcREpcjg4LIP3ZV2WGtA;K4LCW+b=+xicmVQR(04 z9saX)voOm@>`Q(7pS zi5bU}MyA`uZ?j3Nb0ObAV3$b$kN(Y0!lKQ{F0dqbTm@= zEX9%7omN<-4qjH>M(HQ?uhgM}uiKd2xnty5H4fZNk-q;n)0*+r6y#A?S-IXFqSB0o zs|+}M@q~?~>Q-3^hb2gU7si-*-f}9s6}Sb1V)go-!BJ>k5U!Kt^TskJmY`~&liX>8 zOAdT24-5H*EncbUD_X5V_j@a)ES-u8Zb@Ep3IFi#mh4CoN}ZPOPl3(2*)T21wD^Z;j7 zMb!lv9-QMq*=o>UHZByiSoC2;2=q~6@N!fQioT(>r6q!D`9hqL7lpm-Nj3Af&WEhG9483%-S#j6?Au*Hb)<1Lw&DhPBEB-i2@s6mz;Ph9SYjn zY=Bx1+KpIBzJCb@>XlUd0$h2lRG|tQd!~05$k7qK-3cG6?08xU%ACqB_ocz8Qsk3< zgMm{2`sD;#mAQ18EH`8ULg+Cwv} z7NyzicV-SPuA5EOA z&+UwvYyp*|AWcoOO(-bPn%_Qp_d^0$&xzJ-iMVB`NdpWojR?Y&16g@0)ac-RnNl*a z5>@qew_XAw2RpqpStaOa(tlUf*e56C;qu4Zo+^L3T>=314Zq>*1+X=Mk2^Jh%Y$G% z%J1JI)vFOeh0?=O3x50i07UAV4u{Ljc}pY`f2zvjmw$kE(nm4qKaKh@#0AJ-DT ze`SnMZxyTa3JTZxKz8!~n7YcSs=lX9w{&+Yh;(NcZhj)ZHJ`#VU}Pp&<8%Kfvg`TCx^(sNzBOv3)p>Ov6qFUCzbcoJQDa+(@`KP zBu*HcFzk#9^=cX-h4tcRJ`Ws+8b3LGn_<@vQ7{BwrD){Arz$EN6#qFcp1{rFU`kQ| z;Vu&T>hxdi(aKIQlR=U0S{Bu+N_!U*c9*a!~PAkj+{!EkLSYd0O4oW^N1$^Ljel#yR)ld2M61C3{EWeAfa9LXY%`>q@dKT?r%;cgqRSxdU`AcuS$dbRN3yw z!e9(4JeXT@!8jtapzdv)VoUIe=UX7tK??HQd;NS6q*o;( zH3xzaw{w4nziWd{iiF^l7=>UruACZjsA*2^9A|Z`pf#zrU$*1yXuxe2bT1Ie;@a7^ zLSQTi-!(|Le+vzUEbp6Eys4TffMp*KAM%5+%slAgSdBLm zlXlq(>R(MmB?5Dt*Uv^J4w8w%)PE0NfrR5fZ#XM2?8G3Ozpruhz;ZKz%#@rAwAj6% z{@QkB9 z?Wa5TZ=h2$|Q|I}O$k(0j>nDq&yi&@un6b7pO1ZY*P=V`=0*lezK0Lk| zP<)%P=DEOF0zU4$iCN$gGD6rQvz@k?z_YOU#F2?-)C4+WJ+bQuh@H*nE;&LNxG)*-hwWJ; zK?@#^$<|&egBUxV72^2Kpaupr-12!}9>OM;W$K3t9`EXY{KVJ+5cV~%JSPrD6c0F9 z5Wk^o!fhd}KpHuHxxTSj1hw*tST};LOnh~mTgjcy@Kn za2#M^!BA+inWuuvemKz@{mX~LDV_>Y5E|jBN1&1P*a&Rc2PrbARaBtJZ0(_@&iC72 z0ig)C^+d1LOaqY^mmzHakEiDLRRV~B`#Ksg;6#+k>3>*M50do(=?XNJr*BYeVuV2O z+AI&xMf5?Qs;I?S@H)4pqr3O-X3g^lr6vzWPnsQD9Xi>Q!Rv#p5LgU<<}h-nL5d-K z4>dEbKI)^VtwM;zxz}gZ(#Q?vi^H{jCf+{mG{0i`I+?h;e;PZ3Jf{++fguMvDi7 z^{`EQKN?bTN+GtK%`DLUGZatDnFMT-pAOb@l)(NMM(_d#e30I+q&?xn)#IU{9 z#F=YRi|JPj={Od72oWQGqcp(at^FAurKN%nA1u{T8?oSU|N&r~h zGbwZg3j_y2!qd6sMEtM2I(WpTvxR|#L&)O#i8i1`?`wOQiUA(2cYc1m{NTB=tfvBh zM}Q4|!mE>@l{X#)x6%V$8%zvn6uDc%&X^I{dlL@Nwv-_V5l>bA_G%WmvIy8Y`iA`T z-2)gR4PNbq^gwCj*+f|p@bF58%!J;8ph7r2Z_9wxAtPKH7Ttw-(70DbExRoQpzz3L z0sPic9LOPvx92E3H}``c>mSh%_+_JjN8T1Z?rdlSreD6PQve_6fd+Tiz<&wvBwWsT zMu`T}b}#z^Kj8~N?!wcQP2V1JeQJ|meRce|urPj5N$g@;w2v-UJV6|<=2=lAQ1hAh zwMB4GXWo8Mpa34x7RCGA1b2QD9<2!@W|@q3s8|O5*C;P$5p2~fR7`ETzW4?l8L-$u z59r2odyN(7xC)xrrSg2spcSveeXA}PC0&vnHCN$Ot}sxye0(Ku9DO)T#C`93n;mU2 z`+Cf%F;7)tWKc~t@9*zvl7&dT0;hU$DikyC7K8H?#TMR`aa4@ zTrXLyjE2VgrZ%=_xmg7jAuS|q43`7O^okWK;$Y;6!JO~A6Yc9HtGI~8^8TQ3A&1BU zK7+is_UN2}&dE*7U+K||?&VTbJXA`#KQkBw#3ZJHHmW~>Ko%H_VU zK{403>r79c!o!Yt^=7||scU0PO;XvaP#aGf^=baQDKs$m==bhb(yKQ zGX{Xx9J4E2t7n5=7!qqVv4zVuK`Jdn(x+YLmTQYYL^DjTh`zgyAb_Dq7;i+l>v-#J zY%CaTESP;Lr-8bQg|>S&YV4u1qdKyi(f-7Kg(fF>6?OSe!AT~?a}Q^?H`H!lbth3d zrePk@QWub5`)c!RgD@bMaL7QWuKa}6Gdn8tMw6>$ z9m&K346!lB*0wwS){ADdgsGbA%-RU`aW~)L>P6gyo5icprr9S>pSSki&uzU+v>gXu zi*UO3r@tE0gm#Ig)h=2H7+qnFWn3iG8GKY^%0h@+DuS%ym{3r%ASPV<=`U zHlo*bN`)Sw@*>;QOCdx4twtNM=T~xPzLZ!e@;6!H%367G>_m2n;%SuX;T6uJiA?|B zC1vzc7G*C9ciNNqKYIzhCuhgwakONZNgt9T-A`oo0~4}d;0c<@?4X3wvVA$0cc*m_ zm1QQoc8XZmjb&-oXCT#D-L;dsq`Zq!6NzbKZ)v~!~`9OWDMX4g; zEU&*0j)}C|uaLKS3$>#y?voSh556PyO=me>8k(ZV!t!RUDCuovNbDlTl^k`KtbfaQ zhLPb|$@eyOwaV(7mOXZFvjW~ha^fTl=iA_n7=kBNJT$aLH{BzNqr=pCvP6#Y z9hi1toeO+bibY&M<0Ll|wNEVFRjM?kDj!OIX`eR!=Z0lhXFq^p8~AY5DP5mNW4p;5 zQ|zv*XB_y3ZA@VwoJ$)tVfHxp7t|xdxR0F36htz|2v}wz87D{m!3raWSyB_LE94YJ zl38J0I*Txng>?p#ZD@Rq&?KqJixC&1TTdG^_F~zB;nIYoXRLltyDE+jt+0AWer7Y_ zUWTV=A~9EWCxb(*hA%D?4aWh?=J8lE$~ao`y4lh-nshvKA0=Sb8C1vOIqZ~cr*D5E z-|B~-x_&G_jyaQFn|BElufmG@J>Cps42S#Cy3pZ{JGwvw7o|2XRwP3TYqXoY2m!Y} z_PnwvAj4l#RF?GG#KLz64I#AJ_$RFNs_(&89cB8o<%pZ^*_<@e*re~%E?>UW9=o`T z3ChjJ7;+em7glh?rI`~W9d4XKB4)~d>1y>>X#ey#S){R_7tCx~Dof{FvSK=zssZ?P zxOdSn)KpS&ZMqY1P=0?ycukKi!|k^n+V9^ihB5{}?*I*ud;v(}?hXFL03`bWNvUhK z|2T5klY0nmN{Cm<8X(=j3m{K4Z9I1sH}2V_gnxPW>w6=hiG^2I>_J)J4jT|df?g`3 z5`!4QY4SFbT4`m{1nYRv<%$$nrT1cLa3b+UmOSa>G;wY z8s#7Qs9Uf_yM)pm+U?s;`x%3)CtA~s;fz$b6X<&}PCuSf8;H=NTv#JcnHP;mNwB&l4;{Dh{KuiS_ye z@TFXONo}1OpK>JksI)G=OOiLHkAoCZE?wig*TVmr(!&d0MSEEFtI#U4egV1D_dE9UAH%tLXhh~~ifxF({ z(|5UnwIgchrtu{E@P7KIR0_O~?m>Y53;P=;5h1^TX1;M!EmKY1ZnwL+ z53Z?v4*gY4zAQWa`oNmGyQ(RR)99QYSf8~6`&}d3kabNnu&&WoXajg#wE8C_^4FM9Jgsb)iv5Xq;#uP|&o?b-sbW}Q87!%sFKj0>qtF(ApBz|(P@cKm?)Te) zeX6`PFcPhE+uVp~aRo$JgzMeeOC!)Bg+|hF(zSSp9!|Ymyv>R{z4WOF5>yx{3sL>% zqt6L%b#mxQPOQBA#@fW23DaJ^h+Q>OTuz560zOQyZ7BJp0IhZtHtVxYqm`+p#%ZN+#jwtt_s688Y34 z@!X)nIO;uWcz=?>OAa?G@mQ;(%40jW9z6SspK%~%CUUQ4Xp2=UQh^3V<<@eG_peah zUT53tMC_?mxm4to1N;m8>mnWXN?aHFk1bu|jK~j%&h>r8ZjI>O4+$d*+{styV!q58 z#0nNl97M!aiLdvJ(VQQ`Gl~&KelRGSEge0)s^`og+g1x9V<{XSNC_ei|0Gk`!r+oR z(pNWgo@CQB&5<9?E1NoVzsqp(D3nx~5CwZ{Xj`Q4tor(So~7M0_w;`AXMide^$h&@ zODZ`(IOMFG=Zd0mZe?vL%OtDpcYi+-uZ8lwWGU$XmiI!@Uw?bC2P5vosx%?1IlBM& zoweYiLQ89imXX?vmXfwZeNEjm(Zm%BP1B~$Iu*#PN4(Q*Y!b_SwdwphoB}m2R#o`* zP%oLw(vPq!Pnxj>5*EMR#1Ng`ljP9_lFc7n>c-om4YNWx9xrX`#Vw?#n^nU(Y-z%U zD#tp*eb*{TcG8;?k!ub*+(^uCm)9L)_h2|oPhHET^%Vzg)Z_P#jxt(Z8PhYj z3U|u1bz^^;>rZM9;z?FXH@mdkQtbSs(k#?{)S#rQAvNKuxXpFF8J3X}s99*a{3+iN zzj;{-{D2&%dil=DHhVp@cB)|NYvfd0bCl$O&^ZhW+bzz zWohDMFQe2P*=4B~*!5>UrE|Omlq`sb>e|+8trR7rTNU)Jise3~bqhMGb^mD0t~WW5 z|C0f)SRSiQKxAsttIm*I-aHzXw@G~#(0b3+-f8{$IWBkNGwtnbw}R;DS5^kARBYwG>&NL~y!ny-1zv!E z*u4F=j#C*$r%usXKSvKkUW4T`ybrM5aa;F(GRr6S=I$-j!s?^^dD4ama_{5Chf|yx z5_)KDLkl}EMPKZ*w(|N%6|XBAy98M}bQM;c4Mm?O)h0XNGv;?U;`B#RE!n(eTx`fS z0Ux6htR?%{*$R)Vj*gKt)rEx)IL42c9mlJF;x`cYb->2sks>Ya&f365W4!#OH&0{6 z6}+GSwu=|DP9}RE$^C=?ZCA8+qtS>fcuBa%ln>LX^G4Om#J&H=Tn0aXzK9|_%UXv2 zk8alUy-Y+xqm>&XoWNh9hI2;qv%d=DtK(R?XB50__O5~u)3B-NOyxVJ1dF@5RGYtt z!6RhwWpL#-*BXENm~2XP(73lqawlGJ;md`&L1aJG7!{1*tKE=^Kms)b|d} zkZCm0O!mnbi9h0F=~j1&es)M&<%BcT-R)dmtG_2VM(b|8&r%5`<+~IG>4=+fpYeZR z7&J)SRsLCqQP4x*ccnag6=0?igU~cb+|CVr9`WXz5aO@1C)~RA*0?ruz3(?FaTpoV(VN$4)NS{xSxgP~VX`8(|P75YGhpF23n^vB> zwdY?HO+PX`lW$s-IM;vh7|Ay&U|PjKt2@J|oq@ zc)~Qq%G;=Jpz_M?EwHEk_B7Jz@PRgmnVs?=m+@lBn-I;}_w@(2cFdDGQ|@8L(1iKj zY@|I{@)YtCI^0~x0CI%I%vZR1UkFrQshB^$q_~C)b-&PedJeOw!9)8Mz&zrUcS~d2 z4f8h_HAlV6Z9Stl@cuCP?)+fyRXfVo)CaOPZ#1ellr3b^RlK=`J%&Kbtt+d2Jcz-) z!?*BLDjTR)6!grvUZN1KTMP5;*RRjvQ;2YPCP(B{QqH=`2D|k*OtcC6D ziVXDY8P9ZSTsr&(W0>)yF}X~9zJj$_opyR;kpG}{iQ2LH&C77RqoF{Rqo3tXB^rV+ z`Ka&4KZ`K3SyBWCR5vXC&TO2Tc90wNZ%!n5%mCK-9K(ZTIhxhT$#vEuEuO#gpTn0F zwFoH%*EsArV1igM712LtE?IYRp^(Al^Pd0eo*_0z4^|;%?=&1lhiBAuv1iIQip540 zqvu;gp=gy33ZcYxR6Pq>pm)R!-DzI;iRb!A0+-+UQHbk1aq){_l^h>3^hRmp5XzL{ zAfHbQ^tPCxE&=B)lk#PH&YwdpU|eelF+FFyHfcoAwi*XqPjcO!nj5WRn7{ z>Qi>-pJ)lTw0#Al>1xl_2v4^LEor>uTq(;*RG^wTT2ESJ}Lp|)%l zedu&he&r?+Ky6@qExyz}*$QCLhYECjd3t18AM(s>1-*5P(!L(64d*Iog$Z_h{dM@gX!L)yWyr-dA9omgOfCR#2d*Gz4>SgJ*b0#oV=XDIHFe-p_RP3bT~@d{n3ROGMEVex?9W|vhmfSjGr%|kf74bdlx5i`)*_`KgVinQ>P+d2nh6HKN631NwL9L9KiX-&rfP)u359Rq1H<4xfih8# z0!@hc29E+W-dl4U1J?WU?DD7JN#+ z_?hecKOVgB8%awAqrW*QbOt1SWgH%zG-<3q?B29s8+XoVTsoOFIj^k)So;9hiL$aj z2&+AQCYiIn8E>>sTyn-&3p#^zim!mC4;J;y$om#CCIBlnz&gRs=BhK{lGV@1Enq)* zbp&C3kDqD4YIkk}(aIG4ZL+=79vKkxdp#bZQTm+j*Aw%OQ9V1oj`zX2hfZ_Ge!%ng zENKuLZfvFqYmU(r<-*ZmQi=y@(Z}R;Y;_l0vllZ(oEtAQjs9kdA$|ep z@&)5amKBvR5Qn$D`xiIlq3B6?HSnSgl_$)2^yaxJD{3FkMO?Q}su6xL-9?9eAsmD# z(8BNB9Z*Qa3%!52-G~GsUyK*t0LjbJ9DcfkdOCvza6x1Ej$yGunGP@Regm47w%{xd zyVMdPLRs@aNPB>qdna88Kv5s%c6D(e_JwO@{|1S+I87}@f^|lSvU(qda;8C{pJnKP z7h(VpagTWH=|B&YZu)dV%|Cp`4?qvuTjYtwSy78&=~R4-y|uuHN2hN6pdO(JOYgT3 z4?QcNyy8J)7TJz{L1P@8zpeouqW&RU7L8W}72<#Eumly}bS4BaKos`QKWv+TGDBN^ z{IMV=XDOX}17TT>uMJF^~#5NQb9lOo+TK>ua+A@jC7M5`lX*i?*;x7Wp?@ zaPDx`@zjvGWZC_OYIXe#(L2&ycli!9^f!f%)0f5N1^^-@wO5m%KctQZZxH(!_t2Fn zsU6qefcC9+;W-OW#+t zQ38X0*25J4pV6V-!(YL@8V%Rsu3|F???>8+ad}ACwGG@4#eq|W?Y>=NVom`e=4OVz zMnc>I!RvUkK@J#~=iQf1j;}#m%A$)gKzZKrr1c+-@W+sz74&P4{Xv^+zxg=*XY+;M z!weWei`t&J)vifM00nVxtrZ}8k>W`F(v0x0;OH(IFJWXr-bA0}|M~0cK(+|#=;k^9 z_#f{YBe8l6=r5^?sst-4(c9!Y`5P=6OushVGfGl$EM|V1OMZ45m_@mGlb@3s2N_!7 zn}TaI#{*}ClvB?rz20Fci&F6_H`uBoDH#o6nyZ3CE_*om>jmJzOwqIWziFLoRL5~i z8OZn$QfZar5oDC7P4yS<86mzNz6%sxg8w7e+S$M1BiJ4I|LM3Fv}DSI2-?~ia-jja z=(YCWF@svTr$*QTkh_FFBb?>fOF@K-*4$oqe+Nxw-uxyEqP)_w;qb1O%9;Z{sF;6B1bz-{860AIl}%)r1V~YL=J_T20`#y&$u3GDJPa}nMS1l0 z4-L54!Q+DoiR8~X#*Q~NHE`fjIh%9b6+r?;c^>_}1T>t}Pxz4NpI;npJKfNbnW)I# z#E41SO}IT2rQX)r1r(5Q@H5u##&=+nNo*bCE3&to?b^xF3XDn&icJ!D%A!#&E^B&nklCx9voyMY!(=%v zo?f5}!c9kh=^&oyy`0u+BQqGz1L^-x-MSHKgry=wW$- zX7JQ?KqzS+2_xNyBY<(~aQvwZNY7(_SaJKG-<0di2wF8b;Pb*Nznp*8<*7Ud|Fh$V zEB1fvqDnG!`Z~aWc!8tm;CT-e1|V?39W*{fLBG|+z-@SZ+8YRzZxyn>!D^9$T1C5) zYyLR^AOY{t+0Zx$66Cz&`kl6Q$e5WZ+(*uD9REV4F>bqqvR`jbrR)Ur+go@U0{EZ2 zet+N`4w*p=Si=+CgISlS$%pa35Jdm9?^1$LMk#nSmGHrcR|1oJa7ncB=wFPnMM0?7 zcsew$A6p=SvutVHB_{X^FraSsH6#ra2vM-wrSv8k?$j9bQL#I0#7mLb7Fl`Kz$cS)5D8ECjw3ap|VQ@ z%+U%1@rgKU-2|YhDa5^Cue31|14?lM-*u=G_&iOnaTBb53=yPXU-qVkk@g{@`R}F# zK`L0?e%w6?So31vo~?tL2`0O$q#;(R*vAYD$w9E)Mf5@HlO3QJhc{?{wy_huOYBWv z4pd2gf`?oIzSmm`fl`JOZzsWSy~5MclL^t_WNu`k4`TpRr+Z$D9WAg0W;V?d|uZiH(+!T_1dX-J$`nL$~C z4!Rf4OV9_v+t>D<#Z*sxNmX ziy0(w@(gCJ|J|ZoS{&MBkih~Z^+hJ3vz#aCRnSa2j`x!cSi5IqEF-|O_TBk%%M&cl zy5y7b0$)RcxE0?lmbqAfn;9>VdLbYT-GXCZ|FEh6VGgvM3MmC=X@B%IDSCi(lbrD) za-igXai+KKO#L`z-WE&_yKlfQzvU#Vp9U7`I&ZJTe|njn^`rn+6}av>gMY2VsuHm+ z#0f;2aLiaRq|opM<3xVxoXv&sLZ50~%0W0Rsn!E%zu<2pONvxJWPxH|a3JpdmvKe8 z7ux^KWGuM{UO)zFX~)w_iY#bc-&)2!Si=>LubDBxCYkP|*^4(R4Q2$7ZYZcV1jm!Y zhc*M$nyqEK=3@dzvX=S_ULknBISM3J23rZ?1{?mrW0s_B3u;6KMVgY&*FtJIf9`J6LHxD?9 zou?Dt@jqnC=pV;{=aHv7#nko{yiTCTmBXxEK`s%PL9Ivlhagk$sr&FLq|1AkF52h5 z1&*e|<8edizqIZxc`pQykg@_6K4`!C?#sjejA93ApJKBZk?OAdJ1ANXRKtcUi1QV4+Z&WecJ z;FcN^=y-#Mh#LPlP)^vkg?NU90vaD|x>%hL(q8qwGyso(U9qGGUS&cE?b7l&G0Zl`c=zh(T2~WxYhiC};)mDEo!nc? zR<7L3J-*}i8w^(t1$>$4DRvt|zwN{MGUHBy=0}Y<3^NqV6&Uf3`eDp~A@V|u*c1vL zgz1jNf$XU1kZU&4D#rJDhZLAq((qUag*r1aV@U*+O2sK`Xz-o2ntuz=DZ}$mW2GMo ziClb*S=H&kCFm9>*d|{`Ff+2M&}xLi|KOO}tSq}-`90O`^%>QaklBZNE_3eVD>VC{ z&1x<){9gjV#ojuXYI;PXnZb`-qns0sUN2ergN`s`DzsGn&b4l#Z#LV*XUV?D4{zir zmteHS911b;ZHrur$@O&y`E50fJB;#sHHuSTzw?)%zd(CeX`V61tMEWGhWo1PhRG5h zzSJpQbC@M0sr7!M?m%9pg3~tud9uf1Nayorc5Bwbbjuebl?|XBM#wo zmX2uO&@c76_K(KX;A>yM1eU?R{*##tzQ}(>=*&%R)6%qIdm7c5$tQnlaW>vL4t9R^HWSzWmzA3&L2$Fbr_`;q=V=qg7vv;XI|rQCPO){nIn z285Q2ET=Pr{4CI}GmPkL8Hn}9-`-cUSIkc#qXVy-p%f9J;Jpdqtng<~)TNoV8Q^>j zQ2lYqHlm-yXlWrZ7eCeSPrtt3>*=S#v@3h7jl*h5r4j z;pM)%O(AS}S7YrQICs{Q&q2LK8std9Qlf}FtO^aPrj<-bIML@JT{t^9dxvwY!wz2U~?DC1x+IBA^VGngA znF0bYt2{%kRzt{TAZ+8GgF9AtR-*0RDsmE@1hN=9p8*9d*tDwWrsk*h^68TJfSt-i zbY&ta>6qN7KyAIWW}^cc#p+0y@J9PV=OXC0Y=s*yy;SOy0#wju3#X%Q0~qn(Uk~b5 z%Q9KCJT$ehjoWK?{q!MPVmR^6c~PHRRvG0F%waP8$@MR2*+n?$nkg)z82J99X0s?V zJcAf*Uy&n)SomO|?(~sSNAES)gy};Nd~;zH-)NEq_I=G2y=`3mUc%sGD!JV^qEyl( zvS^eDJ=egO5w!Vlg?odm4ni6CxxqDw1}5-oAY=eEsiOa#?e72Hh+;b|+X`y2qq^ur98)bSgl<|c zm`cA4a>2c@g0jEaZWa($iQ6H6k#}C(MKlPbZpFd&=r#8_@t3GqZKV_Fj9~IdS{qZuDMK1A4!GtqlI#@+)Dd)e`qtV%N!`1 z!Fq)UZ?*SG@$fsb`?Pz1a#FFU+Elyp#%hgG?G_%}vxbL(LZN7ovdkVTneUVX=4*yZ znD%R~@M(vm(HK7)T;ZWCYSOm%W=+i&=o+KV9jvi}YR_CBB{YoX$nH5>HK2SyI$-R< z^Cm5puD|cc`6BlN4Gd9se}=a>77N5-_*Q?9<7icMq&-@gwk)aJAYTbp`LDfLIxk%9#PLZsSb1Ey0FrORCIhyHwyAZ*2W4}*cAme-F9oz; zxZ+wcad#G9Uc>?tR}ea1g!|2)A<_>fB>-<+x=dOaS)Hz0JJg;>5E8oZtqa}bf_YoGZ{PyO&WR^qTKoyAl%?B{^X)WC&G+c^4 zBD!y*2DX)LJRfNU%Ma$XCOA#^Q{Qc&1ePVVKAgq0Wj0Lz5m^$KL*R*ThE;|d9Blo( zii&d`>gPzw#ez_I(uW77O7-6LL7@HCcxrWc=QU2oau6Mq$<77GGS^5&tTFtukxGpF z3j0@beP~R)bAcDx2)Zrx+jV8?l`}8CT)>O!le%_35;nM)4j6C$`JndF#|d6k-B*yi ziOuS0D14w`EL<-6w4VUV`SQe!BY0j>U{l)UY>HBQv+3H8wtgexByXRD1U-hxfF!E{ z)eve>YLvy#QC!o>j+b$-H9DmZQ42~k%=D4VIPWDX33~KEm~d(ngf48aV9I)gA?`(l z)b{6~fm4VlY)!}erwQg+n;Q*%c~*H$Ys2V*(3m*;t;}{vtKnviV)+@X{sls9Fq91Z zt#OWsI{o$Qd1Yi?%lVL1Jg!GybW2LeGSRqzu?wbNM`J0lHMiSXi^Vpg`tm?7U+e zYXTz3v;Ah4frO+2P{e&^zc#?^3yn#@mc!z>yr2&dV(Atvpn<2|)N^wzLPAmn5S9$u zyETHxb7pe)CeMvj4?Tu~r8xOyfCwsanyzP}}=L>AW6b4dX^MF|+czVT&+mH%mO})~)>VFub*6J~|F%9wP4EuUETKRq{jD_+8 z|DtzQbM>;oMt{Lx$`b{cux#9CVwS7K-SW=(7-HK#?IzwJ+|*Z^4NXmxJ$&KAEE2gV zL0q6(w#2qp_X%_d?+j56Gya3yf>Y|;P|4XZl#i1yb?urEwM6H-EVBWI!W3x6~r{dff!rrV9Bnn zj$OXQ!|y0U7Xb?e)*-=%nW2)CUntRi74{N9W@A?He$r!ZXvPxg?(eU@Jy8M^LA#4f zpzmi%wj)jaLDs*5QlwooK5lQ{8CTnj^nvhs3_zH3=HCikauNVvZy0sk;&>gwlIte2 zVBXX`&EMU>bM*Ay0MzSDYp6OX4^=k`?Dmg7Fzn1@_CU(>xd2CNeRUsycW}W%FpVqd zvoq@<0R>5B#qJw=X>|veA;9Np%gdv*Li-o>R>7^wY7S{&(AoDsoY*sDvAR4v|Rv z_S#3@H}qtaKqnNQbnJFW<_A)d0br7p3=hm5;? zc*eG9LSqJ`aVXS>WJbs%;xf^YP)n)MfIqSdRGbqO%5h2q-;EUkw_NS|P2>}D!2Nx%h({?+s z+cP1{N+M&?S293QcCv};^jEu_F@93QXS~+tS!yD3+MjP6c2{(|E+|zEo!Np%|9U2r z)wa>RiI4pnBI);)>{quVAvx_G-l6(1)a%Mij`wK_MKiL&`)UYzw*9|8QrMQ7LfJfY zZYejN&N(MV6-p;azk0{+;gqObl|r<7s#2r&XKI7%kp1K!t4=QZmuT~(QLAW%L9-my zA_86JT;wO0>~b8X2MCPi9j+MS8!^BI8GaP?6V05Fe{zu1=RZ^YaG7_%*5oy(~q^!nqXt*-XK$ zwkETo8ZsWLkC{&VzC-VX?m<10*i(aD@A_hy_R`Z%npwf0%G5U!lKkn?G^u4D`0LrS z;*NY<{Y9>ws9-05K>OoOw^*0XAh>$PqRW^(m6i=sJ&s_Ys{HsIFHYK9S8hx8)NQv7 zgO-U-Q9&%9Pn_L+GxYyIt;h*xz(tzBD!xW7%yXG5pF2tD$Oxp>LKwT*Y?y1a83^Qb z8^&IY_E2W0EdPwI6Lq)GYsa-T@u0;hJ1AGa0gvsBG`kV3+OcAoGDUdjzEN(QSRiOW;~FO#joMcnRU?`$3v-?rafa>L&bonE3;YEOC&dcv~m*W1nI; zkF#c2DRJ8>=Gb4}PiGMxJdBSs*5fw5b3ud^+#B8`320gR_PUznB5e_aiKo|uhmNPH zS7Kw@a7s^tFk!i1k0@ug4gV}Yk4dvQSG^S zc-%)e2Gk}5_YZ8*kGOSDEOK02^;)?onA|x;+Lzt5DnAVt5Lv;a2l$%cL0APvv}9gG zgb7}{O(7H&q!$unI|OFsHtQYNdKVea`~D_r$!v}Bdi!p~JaF2y&P_utxNUN(ypa^1 zz%qM|Ed*MfsxUZ~ms7MD`=~x@AKUd-uV<(So4ZQkaI9A&pyju<+Fxok`~AM$qo}rt z*GrOJpKeL)?)Qr~p<0m;vf{tGa#7>!NMbf;W3GophJ?nWIk^yOh41Ilo4mmZ_uL?Rxof~#EO{KNv@Pn3dm+}Rl%tpcm(YNeyQPkXSrfuE zhmRJ*bf227wtmn3_-4WW*1ru}zou#GJ4LqmZLSf2=_AXx#%ia0M|dL748hZFCC2gO zAoa@uEk|~l2%=ZrJt;ZY;9Se0mG-rvU$NqC9n)Vlr$jD2I2wzgpCvQhy193^{+N|` zz{J>qZt&s2mz7`8Xtq}`9Zq94)1rBiihldSvHxD%2pe=swOSyS;~5kQM0p=W0e=hIl9L=V}$ib9K_kKm?T zGIm-MNDN|Yk)M?~VdbsK8tF|tTYM8mCe4pTm~8rIvVOoRD3Hyg`ie>bO$&*z5iY28 z$+%6dSNqEK!pEKPh{}lZWbPbZ0L=r5(3W4p-6$dv8fNI@d|^-hz(rtSz4bmdi#s6^ zmC>MY?*0(^K=M=HL0GwXZvK<>bG@9k3!UgyDO;Ta`ujvb)mHighk&X+Q5r^_+gz-Y zl#)Np(j;haC7-V5kR-$gFP-Et6Q%XUqJkrzM8&ZauRNnDaV1Ha?BK9sHKHsFDNRI5 z5+pgVWEacnBbSsJX|Z06&-)9aP~Q!pzLgAle2fzn8}yH(in7wYGhS7}tf#P+rOqHH zt|y1b`h65mTQC~&SR)CLe5Z<{ACu`u0=Cm5XbbRur_$yD!v7{nHUZ^k7b0Ao7K;fj z-5V1i$&aKhXh$466%!kD7WOuUNz^P&tekH!>qFu28#C~~b?I2)6lzS;^oji5)(Zqi z5pyVhx%ysjr3zRHR3g30AMs|tR zl)t2*G3&1Npe7ZD`CRxHd@{jJ+r5)DG@4T84m4@bD@*$E)gK4E`wUbMmN86jm9^gN zMs;=s14)Yvx$@G+IqwSsQ;g;^^+vPX*2g$xwU_EJifc2XM_25zMt_CIA0Jg) z98UYXgA`qSIyejeeK$;qOgp+*M_JY4#xp;7Zj4nStQ zG9<@1Ocy{NNzU@2{^WS+0M)zm``0Al>%HqAm;FKeX;F$SDl`@j-s>3;oj*C1ml7X7 zaFk2&(-6L*%L@&jaLHy=Pq~VE#0h!4cWP~&&aDX1;KviEE8CZ)!|hCqhb{lkoglo@ zV9kiAhPW#FNaOjL1yM}~SQSfbgzG13wFG@oi|}FMOWvGg>y6a0!<>&d(C1p!NYAqc_Jqi+ap2_$W{1%pZ7r8I1 zH_zP56&=%h{!T*oLmX}5k8b4`M*#!b{@CmHfnac9I?sNkAhby z)TZ~VuECWC=L`GRLG@vFNm#VjSkuByHS0PJprpfXY7^XSoFQ)$=W8&>C3TFuj=0R ze<;|iPv6_PQV%_-)A+dDHdUD$N<=Y5;ZDOnEJnVu?&X>=q`+%%m|)baIfIlbo>LW5 zmU#Oj+1ib|S)4Huz7lt4PpqUzOY442ImBR9b9I#QMT=ZTbX$k-i@Vq&vge!4r{?|+ zj71B*rVA4vi*;^hYqks~8Lbee!lT{I>*9kf3L$q5*~IH8wCMcOjWZfNk|NgT9}9FM z=~TIdIMFAlI7^WpYPcD3 ze;`eLq|`mJHc5J|$jGi;^XYZcVoACOkz3CV)$_&JJ~Mk20PiY#lZkt-KCNf5zR}}1 zFaM9GtB&iUY2q|WOLvGg(%lFOB1$OT-GX#TOLv1b3P^W1(%s#i0@D57ozMH7%BW796InnMPb>0up-9KS z8SUNZx%tBDjp~t(&q;-($4 z=z6es`_*|vD2H@D?UL8M#dBZnR-=odcK)C(nCzD;=|hjd=uT68+$xRrx~e}Si{hvZ+C;K!Gr1v;&pMK%%VQsrf0w3(CI1B|mmcH_RX+XGSe9w`4pudvVf-+9CAW{Nn7A9FJvc=V3A0 z?KSe-&vN^>-gUCmXC%H?ll!u}%%&T2@?~#6>rbd{oYD3^YApAn+10_bQicgoyI;S6 zq2XSt_8;Kkn3WvQct*@=@;d5hQnDr>D1tD?oxxpJ(is6t`&*O2cWYXtEPe4<+8c%hIj;ds)lZ{6V9yA7#RSo9<7@ivps9 z3(@646r{&r)!sPto*s*g7R^4MTnnv4m1yC~)`Kd?HCZHl-ftdwOJXz;U)dnll@Aq4 z+2FO}sAA7gc!Ret6Mx0nDz5sxRj@baCG23eJFd0CS`mxJ^gHD7U_bP57Bm14lW&{28<`Eh>MNP_=lG9lJ zR{|RRY*u%(N|7(mRUUVR9iD#lr&n5)!IHH);w}#GVxS2XhoNyiJPd`SzkY`ZJHgal ztM$Z7ar)_#tTklMMoID5Xd!W|I+@$M0I$G?#nPX!5kb6&2vWa%JJFb%IE&mxO&W(k zBgLOLSbSw~_sNElXl?v^w}WmxXh+|I#rD|6?+N5AKd}x9(EtHYeF1l7OY!}-!$A|% z6!{kL@l7Q7zIf}&uc2cX%OwT`i5_Hv0X9^6p@Pa2{SgsS?hTY-Ul#NA|6CFBcc4Et zSeR+nH2xY&Y8&v49qG0GLcfoKDe4J5|GcfB$eS7G1FfH`E}OxaV@sEX>x;6k>Ab&q8&@ z{TY9EWoy|KqLiQ(689sgzFl&a*661F05^^AcdxpHI%~mDip=~7-(kvt`DpK~ZrQKV zshGbUf$^ovwoog=sXxu5ZxZ;=rzOq9WDD-^&BZzUh%2LtnilT-U-!&Ve}gGkGTPHA ze?oLiEv^Q&ag^_kvF=2vgrt(3_}DuN->-fP3Em#?}`ZmP3sx9U5w z@4EvZ)3WeHhn`W2+OmF!wV2NzaDUk-Q#NHkzy6ZJ@BHdv z)uE_Z#0nz~{_(ij=$>bWR7mP!RjWR(a=CqGedaW+)sp~6p` zMV5IOJ2{^0D!}Q7=Z!0USvOAOBo%sy zDxn+JI|JqPTVotlsRQxW0!p&aJTL7!$PpU-KHe{B5W0~brxC8WWVfMWS#+Xw2}Z+p z+0XyYdI+T&Xy#ZRS7R2^)G**l@1R?1_pRbKCWxh&>u;B+TL0iX3uk8R+ew@!jZI}7 zMTVN#)H_-*Gg>j=;87;d-{G?j=R50TW*o=({+BA1zS0INpS|_$o1#l6;{Z)}KX%W3 zN$$Ys`!U0MbVTUbRY6Bj#qZcxUhRJ+x&>bDET&}VoM5W|@ce0^%#lBOP;LK7*3+tj z30@q!OU+<=@YTYj6*!!1!k;|dnw!>ywf@TCw;15rY^GE6LT8)aGOV$ZIM``^B92R5w&$DK=kShEhI0`Vv zf8kmBb7$LH&OQXXyvI&B6(^BIetg!%ZSI)R*O|$feP-PxV5HC zFq7qgLzkgXlUi)|y$&544V|$majK6DW^3YITZY`Z+r!+(i*L60KEHC8bYKuC|41Wr zZ;la!mA5(9BIC@V)rQ~vu_>wDTtUUct~oJ?$N$SF)BRd&EBmglus@NJhKrr9E~_|B zE1Qv_0VW38L8mbs;THOlLL@xAn_3c#zuMbW+b_B+^pu#qQGU-Aqi^imMoexh%kJ(uMBIe6UDb?&9ZwQY-rXsN z?iy&&+CR-Asw|(gR=}Zq#0~Vb5rW1ZFbhI&7J)`H7eHOi-@N>@^jkYvk!)HG3T>+u)pWa4} ztkhW2v1C)B8}}WqQ$oC%n)N)o?TA0Rs-zsQC$fsePQcbODU5bu=-QNS!D8**E=~^G z81+-RKXkXZn|t0QQl#_8?jm*vlT6D2q{^xEy{$c17tdpq0%SPYH04C3ttacE(a^Tx zcu9){)T6zr&{~cc^T%n32t_}n-{?-lj%Q{CnWz>?rq@ZTCn@$99wPBqtJQK^k75tZ z->^_P5sCL$-t|APdYCp`IDM+@>s+(CY5W0;G_@QUAD-E~q-fc35-FYsgI*H-*<99vPdRCgLf23MUeX-O)ug{Bw zO^Fqqh`<1R_(jXjdV0x?ph5o#PeI|J$;lm-HG0y}$&u5}@v=#Dso_Z2$YcBXFljM~ zf0=NgYS|AV;nLIn%aN*ioTm1}(?3Hzq?*x3G)JqSjeobrZCWhK)!t6q*ckmibstO% z(Sz$~ojlgfJLJ+#sh_a1;Z{-#H5q=bt@F}ONz|u-03BH+`TksP^|NRNtKl_Yjttd) zL56;j?TOm_!;ChHa<^6;Z!+BsId5=GXg8wUYeyPv=Z(;u;NaQ5|VwPORa+Sz1Z zPW}FR%<9_B5sJXWsbrze5Q6gi_^$)-tKg_^Dy?MxbYB+zQ5)6t_IZ<=#gzSCS69l% z_an{eRv5`iOOJvnH`{9dkpv>#(V|Nt?}_%VM?3Q32P0wrke3ShU{4~usXp_bB>$m| zjHOVus&CoAt7SV_mn~R5Yx;7L27xxEc}GQ3sd)@ zBW}<}1o8OU*~9?m?JoPG{yM5xUZK)>4XTd!gafS z7%vRB;o4zkO&x1rtk!wiAYe?-f1na1z`_u#U%Tf8m{*@{AzfL~fTY&TuUPM?;A-7k z_n!FEr1u%UM!54NIJgW>H5e@MdOY&cK>c7|bd7iXpjCS0R@2Ux4tM2L&#q=y*agDD zY8+jBw*3)?LYzP?Q-Bb72qPjgD^?)}Yjf!}`HWO1FeWM$^$9Xt?L*ncHvrZ%Ttapm z-dO+^jHsgK@2A6v->6NM>Yc310a!5cLwq|@&McIq-X@k;Bmn7m+8{1xwZ_epur4gd z$^Hr3IggT&=VZy7O>sJJf`0unXbP2BopMyx%e9cd?9vlDRB|7%kNipQRcgb+QMu7K z==UyxPdfAD##ZnLHSqRpP2(ZIJ#>L=o^#wa&l;_}RT1nt{um6)xGl`K_xIIT-eEA@k`_$_SyQHG4+t1u50)w=P}B5Lm0}va}k$%u$My z4)5wFvD^Y+O1Qhem>ZW-1c5Ly-83Jm>F^#Y%ka~%JDi)ps|=sXO*hgB7Z2krC}VBnSB>;bEW88BI|)6@*2w$+?>#9xztMR6Qjexq?|L+hNNuu_pzGd z*RP%|&V1h8a}xca=x0?Fc`&NsAzI$b$`u(U2idbRm`fj^bv`Xk3oCz zsFz~@$~~u9q<6oLV*fZBGaZh<>};&h3-w5zi4$KQuL zR*h+wWf;JJ4@i%`0M7AsL|-Uj{MW<6d^98tdr1&a=G7k+P&p|Hny{eBelwNNhv|Bz$r7F7ORIG|0Iuxh6TR{_6gCLHBUJ% zvEpX}6&$L2+!5{|VFn5T_nNLF6gNc?`~Oi`8MCI# zJNCHL>d6ehp0GMb9t+*?uvyyj3mSHOjp6j}tFqR%Ul6NM{klFdUIfNR*KFx+@mGcQ zBd9p(i@OIwr03o8cE`tL0+hl73Z9f-3x0+V^Y3APvtDd$YbHHMZs%=p65|MdP}RBn z7Ic(Rze9w3f70o+C}<%C4yFsVzP^SAg`Z~Z!*AeSL|#0o2tg7YW738o3BxsVqTx3N zs_uzKUATpEKV?k%>a+H;Sm;^~nb*(Tb+|7vjQ2fjh25ijom_riiEQW9qHnUvt4myKS91Sl z1mm?O!l)eg)R>73CZuD4_im-_AwC+@o9~+Van49_vsUI>q z$OH?o4&IlLPfO!rJo985Wii?D$ffecWSz(b- z>_ALRB>YY!1s2%!ODbJV2_j)3V?}<9*efW}FV(`smqX{rN|sAau5dQ* z@&4qYW6The>rdJf{`#dwIY=rT5~(8xNaP(J2}xaDkIYOS?h7BCzlW1K%-Lv3{WtvU%_R@G26#v1d zrl@+NLlqCUH6IzDBr)Gt-1X8;T&i)dIKjKRt9yUPQDfBv!Z}pM#6ATE?Hm|V4G=ul zBdkt-pK^1gM-(qrNKw6=9~I?^*KJcYRLi{h92j3r2>mjZ?aY31`!QR3d1am5}zn+(z;Fu-g< z&p}JdQ>gT@@b2og$?A@3)a>VC@t?wRLto#^{3w#zw&Ccy-h%8$@+_44TMoJJJK;?6 zXWEjBX~{<-pLZlPe~~4-^JN{oCY``ax75nyjxdIbdjCy)jmFe6N%gV7rq4N*_RqB* zHg_M9f*?uF8HM%Tol>uU;yYpT2~4Y!=Rb1DkT*l@#bchho&C&7z1u)Nf8F|t$W`$p zrpw>gdrqGHQfM+p&VsEza}Ii~FZbh6#05;k?*x&uUu86xC6SmnDN;$hEeNI}`YIA> z)0bsjSk9u}_ql|~_)(^w)ORDJ$@q!6n+g{GI0q<)>g$2xhTb)Yv+Cz;MWU)qmLIw!t>Ugym-i$9etf-Q0s=GW7&7uc@J z?QhPf&swIm$63PtGWeo%{R3Q3o>IQnJIX&F{5S)rh}PdA0+0m7f@QQqVlyTYf7dYz z;ZifAi;5SZVxgE_NEw(pz`h#&**A@#t~K&;=KZ@|wz9Vv-0{?TGDK~azp_L;>ddFB z-)qa?lKIEImAuQrkfsn!eybD~I^xg7u;sJIwAANTT=Ms$|Ljg$?$iF>SjvITMVy)U z2^*??0(d9-{8Z7tNfR|cf)`_@TJG$B5B!aJl^rPEq{=P*V~WH+DsLN}UNcB-8ozNS z#g)S@+E%Z!_oIZc3#u>M=j8#Bcr3z;ARDUCi$q-Gr``{Kge2|9#|q*AdCA^C(M&In z30J+T@2cV_Qg*PbT*;fUb$f3V7OL1)yL%ylXGN-StAT#IUxJ=ri9-`xmQNpXs2(IhWsz3RR1!f1DXoB*l zge5uU)}~vO3ni!A{?c)Z@2H4oheWT(0+4Z|s|4p>zPalNyo#g1bTs7RBW zm?MuQw9WUoZ*H|Y7FG~{sRmJjzr{^5`2NI&^oM*NmqNT;`nijn+3?=_b(*{rQF&;! zIO&z*O)D*`+HQhf;v?JCv%dp4--HQvUrlq4;<^9X(+ye1xdm;b=Z zO1$XB&e`H+4@Pw7NB!HG=Xe9SC0hco8t>qg)^ z8mbq8XsDl^p9S=lxaW#*Yz6xfeEF$SF6<^L|MFt^`*F1J^I!Puj;Xh;w}6Pcz@ly9G+*Fsz{LPe&}xx(Jk@jYdvU2-0al8w(**^-${daYx)g~ ze|tEpc%X!VJkx4aGNoH$#n*bqR6N+KJ!<3i+M$+{!ilVc!gLGxr%Jzgo#GG9OY~p2 zpV*CTee_8jM9aM7q%3uQUX|WleqJE%&&?Ygw$CBWN#}C)D=vBU$D%KDIKHXzj|IbC z*|*^~B)m1jnt_2!w8P5W;Tk(xxnx#Edids8k+3$Ah81zQ;)5ZY)$CNvAj?wHyu)@!`rvNaVeR5Gxx3kvuf=LE9}EFf z5Y4{Jm*>jbMsZD_|)tb zmF&82KDmudL{c^)64;6GCeofv|ISZ;=e9|snu(z(Enzs(q%F~uWkf9Wy)xnku6{bl zzqf})rix1@M=?9@?S)fV8(a4zOKJ?$sm^6c8>L)BGhj@dn2@^|mF)~hVu?Ai&mZ=}bS7M_;oNc!QzwWpjc-0)PxGbj90a2x?{lzNFZ zp>FIUOq_C6{IAPj_wx@5O>WUikZi1^7<-tX#vxjS>we@M!6B?GRMyZmM|#C_LvP5- z!wt_~k|rnuoA8|orL;eMd9a6Ryqoe(jE-TXilD}|`#?gMd*NYKW5|xl3C3pM;-%x3@*&L1 zpBeJKp`sb_JN25uC+o`lFe`hZSMuQ+dGKC>4s%jNMPsn7MWt%^okb(Ct@j^YHyp!{ z90thgl*bpB78v8#-N;}T`&e}h_?o9-N1Ey#^f0?G1z_s+`Qt0suC$TGE`M1UX^<}O z!|X7sEEzQ8a<%~T@ERM%`%juMy#AQ}*|g(to@Sq;S3_5!j#+F0ruO{ic2=pS1?2p- zNowF4>^Nx(7t$OFrSk)3Mg<5)<8J&T6^UDT*nBOa#xyy#-Gc#f*(41x7a{dMhvO!u zjud!t{#9~M$s#8*N=>eW5$m#ux{4xO_A-gKz+cS}$(NZv_TfcFYFGe!ymXb2RiJ1| zpzV$ISAU7OwQ$bVW~Y%xFxcXVBir)<6wL6Yc%NWWk!WKLh`drW;0RfdToa98E__fj zOb6~f)sR*wf5Fh+c-#kQ`dRc zYm=}5>nqG5DfwlWs^1Fy)x$-|%PvMkRPxq7uLxguWvVNfi-dW!fAbBx}2GA0jAoW;gqS1k@PDt+Ft_5w** zc6VG&>HgFIB}iHO%BxiFnR zXoqu8yfa`B#tN)+YSI)UgyKuo$M)M0m-7OFU_%f6oQi`W517{TZM#B>0-G%Sb;0?cj;}R|Az3!co3Mb9PlaG+?>vwp z?-27ieCPnJ?O78p<%TRWyPvs*geIhul0)cMxPiAE1B^sMUcPmxN58@O^feAMc#xvT zvM>QCGF&!}J&;@FC9hXsKvAocLS+H0OC9FGE9&IqD7~ML&{K$kT5ffDw>%fVZJEtH;@NnE%q;PL5ni%i`d;EJ7|@Pm7z$k z*uCu*AS?i0tt4Bh)rbzxYHe1@oBwCgcJ=#K2>qpLFdZn=tLSkD6?(S4@0bW`gvHEQ zCp}_-XqejDcRpbJY)qu%WgrsgT|s3xD1xxQ)|DHwLt<0p9`K=N8~l~6_K>S*JTJ>dA&Z7YR78OoeO`gB6s5im2Q|qzEc>8CnyeE(ABKXY zm72#vOGw0`XoNB3{XkY{6Z|g54U9*Y%9&Lp$afWcHN22}QUq=m{$=TnZMXJ6MvIz) zf2+aRu0g6xM;Z>Nb*r&rmc^3l1{D^ubajcb+DbL>;guNd#`mNcD{9iL7A$#~T=ly8 zm=gjxl$E88T+4<;tb0E%IMd~3{`n)=dd3&9n7YF!p7T-Q3|)EqA{Y)(K&-RdCI^ux zRR+v=o`;0PC{9Um+$Eaa0A*puMOc+p&lR8*E63{KU6;-idaraf6$^+Y|Md+`?#J8K8#pLjG+^F8<-l@|$fIgZbWo9aaQfvHSI;ODNYVxjN(?bUmioIUed8RB?C3 zHy(NxDPf>kugfraal<>nd@fH_y04Qvr&o-BPGh^a+8__a2d}wS~L@OtE_TzNu=WQJp8|O8Paj zPy&=12c2<2?$fTaYM(kM1fJ4+t!CGh9d7(60A6ke%S@{6U3AI|p>Z7hV2SAoAO}oi zl*%T8;5u{sAs+e|atym6MBzXVE&2ifNk8$0fj6IFt;VF^v>n;5D9@txY{1yZ{RMfCpPV(KpaE0;)vr)0bS+|~RQxnoOxA{Au^}5sgMw3J0B*o(zpOAIQe*6b$UnVERePmX zMq*I1SUg3po^Bjh%49wsZjDde0!++U@++oYX?zNl;@5WdLX+H3$xD+H3@v9dp@*+Qlz{S-+ez z6$HO51wWzivO_+ShhWtfYV};O&_ZDP3~fiCwYSngQJMi51ja=FR?z&k zNQJXETmZPzDJ@zhL%Ik_^MQU_bISS)D3w52G-%01hKkSZ0(=+L1ej~g%*)6UB*5M^ zKVuj|U}5{~RW*>b>aHS$U@EQzD~~~~P3+nlBLrE8{}a&f%IhbIpx$|W>`=!6Etcsc z{4!_lKnc8Zc+8ByXRb+5UvR8Uv?wEr1;w`-+pvUu@PrV3K^#&SGH=ZU3#julf5>4i zL{6x2s)P(M#3UC}l%BnTB(ugE1VDuntx0fZ4GB2_>;r$azL7zeG&n53Oe0v62THuE zlT~4-u{?%S53G=v+5MyhR6@7D#W4W8lQv{s9PKV(5rXWk#q-&hkW5lm^{%W-0f^Yf zP{6ER*qe#M?}N-{*>e>SsZV2#*Rbba0ToM64^h`yavhYyRnlo&sBc;1#B2BYb$%!+Eg%r?Fx)m z^Ep0xG~cI8*%eqmnY-Dc4rqbm>dZVRfZRn}^TTra4O9ewEw9IPYRiCpUOe0(VVzKK z=V6#xL&qY`mp49!(RQxXZIJK4BTc*sHnRkJ~1?MrA!&@=W{kO;~a z=Qel)Ao2vqFzs7SdjFVlTO&W9gj_~JXk)F)LjTWC*Uek8!`cCw&vqgoa0&S&o5xxm z*p>i|ePwM^vYO5b0$aOSVJ`<2fAbqz;8Pl4@oz$nd(Q|Z_OWG!)d%P)fx~GHu(Q%! zUH0Qy)_1cdfQt1iyC1ZW`LE=0=g0RUpIBPj{r-PC^s!e{K*@rir%?m!0~T80e_@pd zTS|{xZr}6UMF~{Hi~(JS)L);#NQ7%!08x< zxaFd1m^#_v~?3;t@xT@C8*vJUte{jQQUXtnp{b0j#Q_+cvTX;$U z$01G3o5eBzh7YP9b4xdTsZiCrNXm7m13Ox4eE@Gf)DTuI>}~m=LM0-rWqJaKxDU2L zzLGk%6o>`!P@wcY?q6LQazLE_@vm;vdwyyJ3p2f!{4j$H)X__w%v^vXpuP>tPyO|DNlgjfQ}=iZT#}Zm5c&#hrVHL7jn4k0L_@f ztn;_aB^UAX?RGbYs%UGs1KqrbKJ=7Uc3w3MQZ;-zS>^2Q0|7vgo!9#3GTu45GRS@( zR{~=EPRC?C2+Ockv?gQ~TaI$9Fl2y#@pbFeVRSKA18k(#(hv18Ar086OKH^5Hlix& zavfrdJZwj=7O{hpsdD-FoZ??+BblC9>-;`-4dmwsr+1pNkbf1=k;=|VUV~jOqx-i- z?mu6$)sfFrn*ajj1BA1_4?;opS`Q8zg}RTbz3t2SByc}r?&2P;{T>oj+&udK^{dL^ z0z3a-Z_)m9vHVX{ShY7D)aGoMk6!&NBJwce)Xlw+N}H@t7}eH4Aj}=y>i02_(>{|` zSvDy_p}eZJ9oK>karV6LatuI%97UUouk##AxtyW52}}Y@h0>&2_qX5HorDP4C4A zk$T2hL?N#OH`-F+x)5-o@XXa%J3^Sw4(kK{WdzV{X|s=jUO z+s%(;y0c!fSNm5xGL&iHy4D-jYhNcm5Z zAtVzHuH}b2Z_a@3ZcMgOfyhcX12l+hgJ!2faewgI|BbA${37ko>HXl5<$9J<{wI#qPxwU>^V0fa zSX0^~=+%K(z{|cz+cxuFe^YfuNU;BwLiEJ-{ZAS5ksY5`3=+5K^?Wqls@z7V2^jgW zX(hI0ZZ*bQ4A*h`kP@i4nLz9L$`FLqV*Rm_eg_6$|b z%|#=&|Dn^l;K9+CVSrqhl-BGNF|ij0T!amyp>ts53{U2k$(-orp14$?ZwxZB9W;#S zX7gM7O`?3!Eq~!$E$>WS!WXUwyxqo+cm3k}pgtq6;`;Yn-h=buJ_)w|)s9*breD$3DMJo{aV`Di7=4mV+=VRP zM~`Y*I+c8*{1#gEps|-jtZm3N08a8UzVCY3I&kO&nY(ig2{(qV$O*c$4GGtBst*g)DdV41 z(n`sE7O;E0l}qc!WoOmF-+MP!md@_t zISvoaZ%UFS7BioBy=NUfe&azE$<1B;8{Sc_eFAbiS{GFJ{*Hz3SMO{e?dUoj0(;KB zFdB#A(1a-WWGkQ~QGLpMrxi9Si9{?);`1DR>@i*^lmf>aH#q(2QhJN?a8~hKe9fhH z!zu=+E|@o@z8pP?Y4S;oGhOWR25hF!o!6P8NQ>UA8fxNK48~)qE!eBcW>4f=G5s{HuV{qw@dX_+o?`pOY z^%Ty^ZGL+xzi}45U$b7|1ASbJj=J?e1y-HDb%d}>qA(|KE7utZ4$R1{dcJTT z0_3ZErqBs1=@OC;>ZeRAj}5?A-%-y0ov_M?h?rjcin6l2YwQ`4<_jJH%P=h~nTL*m z$=Vkk21BR977Mklp9~}Z%r%(w5QWZ7p|tR`1%hhfA{d;yqrnqbrDkU7i_nRyvtz!o zH1L!djM~;YHgx=JE`ih`@<6i{t5uB?7Ga;u%u2G4vTsT<^v$)0;-B%$JhzBRa}(U> zf+T`!fj%dJ<&(b3_uIp6O((?6Zx-xMDK=9_Fjn+zcM&OGSmH;;^T18KVs${LxWowBKm_h zQ8&r5`hl2975`^xSpuiywO+f6GvB_<8`61c|8d4d-88QKWB+GJ*@GB3!}F;W0qm!g zT=RNsW<;75h!u{BB%JK43xpT07@QdEF-GZ=vLw~d)eA;*$vv(dZuW)=0uxbDU2h98 zMY3Hjc{JVHzh^5HOmCLHRy6y#RPpuCyaB4@V|CeMLtvN9ikEQ z-XXoE*gXFVQ!TAIPyTk}GzF$wEWf4KWAuBr+UnPYUlB@=hJ)Bey3OyJF%{GtzDe*c z(|o5%NB-*)4x>RS=c-vsiv{bRaP?rh*ScK?XX%Y59NC>uP2PUZ5>%r0`#Uys%26~c z%b3D90G8U9!#R_%9#C(`@T}hCGb}(wAxXm)U-~vyDWYQisEq~Ovk|A>T%j1J6Bia( z4NByM^vb=!K%3@wYXae=IS3l}TKX!2!jWHTkzYPPS2v1O#K}wW-!XK@i@hIJO;Ddc zwP#iE!G5!-qQv>rG@vjhB3zq3UE-C@PhnC_+dusKYU38r2_c?8DBT-`IxCA*2 zLtZanhZGxi6wn2*rQ?xUNlr)mdBJMtDvEHr}|0sYSvOolWsw++}p5A+X=GPk7X76-Z`WW$YQ+It~2K0_3#-V z9cnJOe+k0W|LMzl%-Z?T-v2QwrPoR{_|H{l1K0M-8{EYc30{RS5BMOZU(`^B0AO=w zL}?P*X;*N9u^+FlN4A>){)dt@_4^$#_?`E~gww>!zQow+h~HnP2aa zox`(sza0O)r}n8&B&u&hP@vLZe(T5IsR-+u%QnjICqdnhX3I%7-%p4zMl?hpcsKFL z2CvRccG*K81%#UE2CpuDc6%Ou`SgLa8GiR9eM5UX-1w{1*t)1_=}Rx;uXJPU%fehO zu!C1az&xz7LjJ+4BF_5o<)5jsP%@G9RQcb1)YI;!(WX#+}ds=Nz!&o)A}ac(l3 zli8`!;&wO2u;OIp$^^sEnQL>dhnrtAP^XF}R3ObsUm%W2F{@uw{~hhU{bY@ zTAOcZw97&h%`VDKS=WdA&BO6@j(8q>$yQTy#hd3Sl1Uo)rhCRAAg!fRseKbkY!(#n#{I8LdpFp@AUGoYM1xKPIl?#PJ$b?7FYtn^GA?ycW`s z=;}o~C?iyUDC{P7uCYLHWA;ZB-!IebF8VC#+qpX>g{C_DmAA$);0pOA+*27&ofj)s}Q({{kDvs>(;_e$oSNk&H2b87&?qRObj5&BAm@*OkrPLYR#ks zd3me@pN_yE{gKc1Y{Xhv_1#F$&d=|_lw`~)Y-w~sR}O$Is`?J=lT8aa8@^rLhs95& z(%^ZB9v_L&NP1F^O=H7wX@`j;&Xa|}gGbI{^1SCmZ0nz!eg|0u{y^-FzeDb6 zL6md~aKlV~#q7_Vv1n`>w(J9gC&4w^!+G&gu^X^pypV+@d~3qFwwTTnd=_PA z7@Fdkt&wpMf!PE6?)w+^v_%o=`vDiv(=Qo_yYQRbYF|;6h;bEtU**OinxP>j?K5ZxA3qoc??}6*{0ueP8nIO}>i%FNSr$sW6=d(+85hUOTtf zKE3tQ_(=-M;%1jM59&6k@rc(4k$emA*cqPWR2_Tw zO+vKwvf?{UQ<2Tiq{O+f+SvTl0FmVtJQ3#ILq7>?H#!mIcdp2*BNTJWYYbiAeHZ3~ z10IHiTV4`cV6feLjSkJSO_A!}RE&pAcc|KV?XGXnZ*_qK6&GitPH;f?Dg7SFU{L%I zZnwmv>F0hCh!accvu7wba-zuN)tWUsD8h(Dv>IZtvTY>uD%*k?x2~Np!U1$1GzW(H$=dr`N88kih`UVez8{X>Y2f+=n zZm?_z9nw-$)e;Uux^!18#0K~f-nCT6SQ&u`9{$Yl(LwI8yq`L1gQm-+?0AFIS?9Ob zAyX12;)uVMZSlb$)nAJ`2Z7Iq+4#a+!P&^NjY6cLQBH+gET|wl*1VA-d~oo1?J-6b z%1wvEeBbM$0{lP@xUOW)A`S{yzbpqXqdzDcGq8{!2w*Q-`)3!M)QleVd02dE@L35eG2O z^rlu*5b{cHT)GM5m5Q8N9w>tdvd?x|9*ESmo^Ec?Q z61m|zK=v2kY)`EtaRg9!x`X~KWL=ZKYk@Jum-?zoT_3bgYb!h+a!)D-g&7c*TKLN| zOc{PCK)EwZ*90I;>l_!#ZcuptQvN*@jiW0ERaOY^Unkut5Y zS7*cC8OtAa(dWZyBp*qi+@RQ9n$WGM!;3G!taKwK0!b1&92E{t&`b(P<(kM)PYeY2 zOq>hR-m7h~k!h_1&sdPur14n5H;e!`-fd;=77SAEf0Xb7|2G9Rh-&?^BI9`A$2w!G z8KQsNC5r6xAQpgw+5<|Ud%6F+S_5qqBt8r%RRBaU`dm%-YGKhpkj1^3Wmpzu@t7p= zl!K&%hee1`gD7>5<2dI}FQIu1Wa$-)g5qbxur9;=nyiCZ4E@BY@ zH9aU-$I+H#e$VcKDXiyijzN$sdr4T9Ay;+>T$TOj=#tO5dh=zHDt0NlyXlj2eN!RC zpnp8x#;;nsOJacn=8=*3A=AuNUX%;Z`t78{m&1u%uIfTSgIR(s-GA(uhjTK!3p&Kn zK(I>e=UvF5v~DP;A8Ii6+D*o7`aKmx0Qp|`)X4Jw2m8J_>&bm?H zFFxczqgaCZW*jJ6*wwmlp=>!i*?hweD5f;gYY=S#qh*-uc_>`%?hk~9(v6O!UJ}6S zoq@@uv;8lt)UuhZUw|R>tWbu7uuRXzHoXItHcuxeTP~(RNGvQprxKxLa_oFsE%F?S zw1xU~AgH!H$439r8BA`GIvu-eUXSs~6z0;igdAg&eG zy7ew7)R+f{sp*jDJN;412H#!ta3<#agQ;c~$XPhiEo30Tzy6x8EE+nhP=qi0`u3ow zN`FL5Air$ydWIg7<=b<2qsM!EoMyCiZv}vhEk#mWmo-hJ1D@UZ)_+3;{K^lc`lXX| z+l4U~Bx<-Qp$78FumjUPCWI0Fm2r7f_7f=5W)HSn5KqLD#Lg)_Y*3KlboXcoq*bn2 z!-@u4oks@U8VA7%s+;NXw(ypj&G%|!)Hi^t{TJ5vs{e)*u39~7A1%Dep)hZjb=Uki z=kgeRxGx$ai=3Er$^i`sA--9B-qteC7xbPew{@NaRE3IFe954yd0N3(0UR3x6R>7( zPThWPT^^`5ZvK^`z_OTW9}9R%=8g!aSh{C*O6Ms*sX5R%tLKql7Ps5}0;*VyRZbUF z#e`FEUZxGNg4|6J-AF)*b1Xf(e4}YlKnkVShTPIes9yR<*=<7{X@1{J|5MGYRAgK> z94P@-=&l0!YY-OS{W?_;Qn9K*ezctP7#w%GI&ocZ0g{ zQSPpF*}G^7aD`JSGx`EGg;$u&8XBSF(cDMW=U=tXFsLId8t8TBn35LgR*3-%t3~9|t zzQuoWVy$^_ehzxrv!OFe4-)}CVgGAuBIv>hu>O@Qn|cNUgM)fCW@xK1O=?~R11^)JldmNIDHS%0xYq#@uJE&-oM7z&kOg{&08fK7>EZWC|JiBzu z5C(SA9g!FD|7AAo+>7%2;@4oyZ;9XK8shYBJyTo%^Z)gHyJe9q2G!q{3>Uu3J}bRl zmGFZ9w`T1d`l)27U)}zQl7Nbk)0r%A@&#;57yVBG2JhY9ys;Wk3HRu2$NsN~=v+-% zRkH*@@Evi;YoLCy+_}p6pV`{Wj;cnGCCdx6<%{ka1^gGKXMzIu4>AxKvnU)R(2$V` zvoxq--p?-%{{vv+rqdMyRq$dj6jlfmubFDlf9CFEex8qk5XY{$us|Bxa&7-#LOvRE zeU*X~Of9)v3KIj>pPJcyrEDBB7n%e8+H5$t5~5LWZ_ADse2H zDS!s)PG*ca)t!19l<6zAC-y*?ck%P*ud5KM;p@xzJ)IY73grAwXCHj4gHd zNnQgjUT@c8-f|=o?2p!!QYz2}>;CUg;3Ku)fx=V#)fN*{`|S=f1Zo(US{_vt5D8Xr zah4GoC=Z6*dlJ2tKxY!Oe-!@80X?KEW2~-#lA%fbdIJxlrQVY`Qi5Ig@owz~AL?Z5 zNH6v0$wI;HgUO7?>1O~OrHN#8OWENyWJQ8d1qI{^kvcJp6sNtq*0Ce>qT&B)mW1_- z&rmD2-u#q-3+bq{YM&iahT@ImYTV|MOU`zmA*^EyG+kJQp%>RwkohX8JT){R649Y^ zhkt+xy6xUS2T3}m`dk_TZ=oU}xTwMdZuM;2v$c4u`yfceI+UkwAkMQo37vm_i>qV% z|1#Rvweo;eyk2Hi14C>moI77{3mNNnF((M-M9z0X&xYNHYoPMx;lqGd`WCkZ&O^HT|m4^#M(rk)$^J8h|mlrY> zUkustNQmjOxGHE5XWGT6y$g89$t_j;{rxSQk~G~qIe54X_XofAq7sy5QfXK#e`;|I zM@OG^tuSn_Temdrnff19Um2H0_xw$Vba#WGbT`rrin z`FixHmj#jK$<~qLLclje6UgRnMI%oX#PXNN=LK9)LuZsvP94O(SMz+A;gCJ`>y5)n zP-)Um^%7{ASnGAf$S&fgI{$m2#k8Z^dCo1)qIqQGe{IZZU}=2?b+aA z7()yv-_r*O@^D?>NM8zMy;eq0Oi&`q<-N50z~R=CEv`{_)%7tw=ZzRuw5=!GTd3hv@(QQm_Q_sC$RGl zUW&LtjF^Vd%|(NZf3h;Zob!gjOskK~$>qBF?$y6m8k>Ks120l6XsW~48t|v<23Gx5 zSu}4X3`cB`#7NXQM#d6=7vruFW+@dVNe^qhwW^7&i&=kH=#!EDNiZYmMNkwZ1-De1SuM{G(LA_RW}1 zJVe-q=3gkCwq9;en{BDU3U)=-l<|CdGsa$t&VyAL+{wnIx{}izlTgKRkT^LhHwuQ& z(RD%_gwV4+YTIE*|BAE)eLVlm7ApD^JaoHI=b1h3_$wIAE#codkd=(OzsI6+>0trg zzrPaKMdp9nGLfhzpK%6Mo+OdFkpQ}Xu2}B4E|C`146qixn>5D|!fPs;Sc5KR&`&GJ zv+S!W>8nw*UB9aurqkshlcVPMZ>rtNtBpn0z#U=&{&j_M)Y{QX7=A{wURkk3Dv35| z|7i#-_2|KmrOTN`cAv!J$LHPjZCnzT+0_ouOlu5UcaeN-YQ{tQ0RxI7A!B<+2W|S2U~-5U-pj z5yd(^*)m>;zt#n*6Tn^Us#`VwA2ec+pHo&=u%$Gp0p zwq0^B1=+ULdGtf|8{Y>6!!bk`*>uKZLu@;--FB6Wrr5zTp=3}%pGhU6KpE`HZ$P&> z(-`Zx%Buvi>?3RAt%iR%g>G{NmO3!nyYz8ep{BxC4c-jQD<&$_SaenOv~<3?^y>3J zOojhG$_s-(ka$=AoHqm?$nvQplLN*XUZ%UQ#kug%3yed`n*kw^1^5m-z6TBO2Ye|O zDA(!j^FZu`az2=EQojp!c)=hV_&doPb${_kTr=vy;D)WPJCrmAPb!FZI2bzdL$`&~ z=&N$A)K5yFmO`mW1zA>d9`MulMCU~`Xv<)k!-(Rv*PTF#gwtxP`g)U~`|>H)lzBdm zuj1U{CK-RMou_$-WpIZ69wW^At^&@W9jl57!Pjz59#rP(yJ43g&)bXFUq)v^OFabF zw>-X+c>4XArx#D^r5zDHh6)d=?BIE)mwn1bHW*JTNGR+fh7t!Vy(J)DE41d}z+c!* zDAII!bzZ6;k`E2HCT6-CX@L#%+<@8-tkL>V2YFi6(eFa%N60}c7gtmB6XdbC4OAAqew{i(prWu!vU2%y;#0y7Q{Vw@jBM} z#$t>B0%GC;fslf>uv^D4wS(JaJy?jo&80zF^`VS0@fIG?tQN>x1mop@{xDT^0wxlN zSzYkKjHLbC@W<3PBXg*;+-mL!0)b(4Z{>MaSs%x1GB!kk%M@o0#gIQn^GAKCkdz8# zC(Zm!94u5Bqs~hDhEsu!Q1-X#KRmW&5dt~VH>|^*`qfXq44?lrVz2Uw_Tz~{63o`I zBy;b0Z}!>fMG0inv^tg&XE{XDA>7Iz{E7t$LG@!EiAXVRt@7)(A=taIq&|@pA+)7C zMdQ!z9mx3#WKx`<(f&{I5|xCaKiq*(9UX<|ynAebTHf*~;<3hgr%?~YG4$-nRn`pK z1yrh3>6?k$(0K;{Y4YkrE_Kw>z=PLv@BJBS!UZ?X*?-C0w}Pe0LosAP=FTnC-RoKZ zuTRB^7FLvq=lF^qsp1d-VJ^d+B?1ISbUyE~_o#hG5x+mYE0r0Nxb2^FlkIQ|o`8w6 zzk0`%kOXgUN>tqm_4f%!tr@gZ?aJ}XL-LWzh`0A5sK{fc-%gL)K*j_yps{! zg4bXQgOId_6?M3VXy%!(2HbUMlB3l0_88rii%su*UtwqMB;0}ew?e z;ZstdaL` zoxm6%XelATbSE*!NPC;;-*@? z9&}W-Bad&PKJeXepbwJh3(pwev?<-=yCkVn{#K(HY67UXA-~`7+Z&kL7G%In2$$NB zoJJEiHAGJ$aCIVdnZKW2?by8Q(+SsQyc>fHe0}<)P)1u*CD18ZJ6C@V3!VWx3GbiT z64(_CYlxL$!Wpl%6`4~1G%FjEx%o!r-TuBu_^)?y5&R5YPy!MSBW;TZDOvX1$akiz zxZ1pcVZJ{np-T#b4uFrU?jW#6$nj6Tg78vQ7i0JfNfA=jhd2x|+_+~IG2AH1%E+)k zug|b_Q%EU-TmG!h{iiRK2TcWyJC}I(F5&0FFXWO>>AppP%D?`ko0^tcPQF81tCbJ+ zeF=!$-X-ndzjvo^dnQUmq051W@$c`c41>`Y(FP+(VSfIm*$5bSAeTMPwa+hD2(}5? zk-&r%O2uDx6g4gT?yyJE(ztW3r*&i>kx37&RwIk0OQ| zs{n8Md1bGx_tl(=*O*RPh&~e;|v&-iuK8)0giOBuagQ zQlt_PW~`A$+b+LNwJiaB(ec9aF;swE$3}1J8jLTZ@a**=1T}&wqIg?3rgzG@ZdDgl zk%LMEO_2~TmCcFv&hGRDK(c$Vc`%w1tzdrY_sM4-@ng*$33U7TAGZAvtAyVU=xW-} z0IN6Hxba%5y@onb!yOdD+#J2MjCc40J*lJS52t#Z zLDo5sm;_1qem}ktwq`x?yWsPQwc2Q)FnopI-mmBfxs_kgM1Dm!qlkt8Zo+WRCu#Y| z`k!o`HTUMf?IAcTMOPIHZOWQBZhzWDv*e5}25d21EPI54Jhwjoux0OBVZHTOoU zw80J3?!tC4ma))_Z3FSN537^X*K%8eI0f9(l>Jrl-EE6^iJ^aP|I&f!=1coC?ULJA zeL)lD^s$@;U*@%}jj6_Zi&OBi(T)u6pqxhM+a!$y%`l(Y0Cs`6D6dOPy(&)WQFiu8 za+kbGMJ#Osl6Of5?5(0MplR_9JFK-Ezkjl(I;N4oSk882aBkz0pD1gTc>HNqU2py% zd)lH{)4pYFSt-?gk$b45vME z2~U9 zd3=r+KQ`Nu!jzG3ufmd|mSxPd>Nd~_fGgnv9NH?V3_MEdu>s=Q@-K>hFMezSu#QHf zr!ql$PoI^MN8gu)Ks4J9S!(%biAame`w5`Nab@{A4J@B*I%M+(hkJ&B{9LHBPkwPn zsqp)pW1v*bp$h`)Y_6LoWBJ3T1F84yLp&vbyJy5i>jz{Q8w0>fO7raq6~xJ(4LQ&I zq4TYP<1E6C6ZDW4{xjHv!sJI0+*`ynJsR!eH$g{R$Fbi^ew?W+PG3+(2$OvJo=$b1 z26Plz$2nX~?NMS@XF>zZTX@iLIFq|IQ-eRZA)J%(TvZfUKekmO|BAX-<~_0cGQ_z% zzz&6klp214?m4YQ*vjp}ZV1h~a&P)$|0h&XV;PV_0-kz;C`jv>Dw2EeOK!HE#Wdh$ zIB3B(u&7Vfnv;zXAIo(h;YO?&3i)9R+j#GHn0GXdG>y)3_yciq=g(R2E4e2pGZ7;e zNlrSF-=%5LG)qEoXE1jrzoS}~Ecx2SK+7}0TBD@s-uj3}eU4FMR*d1j;XSqe@@_{4 zaoE+lP`4;)h+a;qxGfPUdi=?i z(`jk8%JiOMmD9|&b?i=^tIo0X0+GSZd&F_^!|Cd?ms>2yTJJ`Pe8xYuHJV26LVc>Q zHhKOZPy{n7R9_V;Rk7cHEO?ILTS^+sXRJdsyHgp2=3$i2@jdCW9S#{^$5&4lP9^iI z!H{2!%ZL7QUR29#l!9)7f$+UH#s3PS4iR>0X?AZmZz*KjX3X7FKpPr|Elj*zi%2Gs zIQgyAiloUaUOo@!lfKo-4Yglo@xs@V#t8391A^Dz5;xgO9_E!rQ<7XO<22d+(>r0w zTV=^y#faZdSbP(I&^qcM5I9%cwOYg%l=gDY5ZCjNBSJldkihf$&h-xi7d<6;|8&(j zhXs29tSnjG(CdrfdiZTsa+LOf3EF61sKcEP!tHK^6a=-(%y@FPk4)jNP^J0dKfAAB zIMKFMNsV4*lTroSLRsyEzeRS+3v}fHhzSC5*7BjRCc-0Sy!hbsSD7G=nvD&ad2e}PUoa$S#yEe&mJtuc ztt&0t5$@5!yyxztG%8Ob;aD}1y~Pk zuU_SArQXqh6Y{b#yGF%%8B^mr^XMa~|3#VvfFYdoEt0?jgtH?uBidVqVuUS9#wi)a zK5nJI$ATxYA#wca3OBcv9x#Sdn88CJ0m=E+Jd*~E1p_A?d4KMxE5$tUwnd1q`Y}a> z>#e(3S!EIFW0VXsFp>7dlOb{SnD&%&%;MzF!({Xl+3%uln?HQ^!>c+LKI zc2qXk4dwD}0?)Ed3Kk^!Gy8kaeT+5T#0Xw^vUvL*kskRMnLq)^esZ&>ZHN;ygm-qF z%~Rf%MVfoRyZ-}g@*`{$>gC%+@)T#v?A3i?8c|{^fx7zqsWt?!uS1S*pWZpq!j0%L z8L3K^bJq+2@|XQGAqRbf5C?KqC%otyCV<7dD!+DJKxu-wI7XNF^#ha*`R)hGQLUGl zgPR`ESEDMaoA0nk6hH{?E2DGMr!LZg@<;X6Iqd8vM)LY9i0Ahb*?TTlcko+Xqmk={Y{Y&vnDPe2YonoSFmLhhgEaA<(9w zs)k8xIyMkg-32g3Bfd)s*T=wa5wdM(?x2I5=AT_qbtI@7oK)49siqOOh(8^GcoO+k zV5}?X9KCkGI>0v?;Is(*D?T0Ee-pR=`paDC1uc5KTh$%u{_sawS&~6%dzimOI1PNH z^F7@nX2F^=<#ZX|Nabk>%1*IF<=QO4wxV7=9q2M#El|g_yXvHSRd|vO@3tVdU-Gx( z*(0|R>+((fWZVvK6g7qDgXg-{jb{)L%bR$`BW1LHNYs|~>r#Jwg4%o%Wt=J=xzHMq z!ywiCv+&W%xA{0Kz?2^^jbL*=W;;h~z}4oJ_glH8ZOg(O81GL?ODjnJUHwMJ>mk0; zW{2Ym@TRgrv5z$I?@%=?h>7f7FwveNgW!zYRCT_snE=)jOLvN$K)%1EYjbjzb|Jna zW}@55Bp}~Y`fHPp((4yv@8u8n*aPKqFBrd5Hk`7Yc*tY?gLN=Zg6laAt~nLf5q?mh zRG1|wd;L3Ua_=OZf#&XFK-p?9zJ4+Bw(@}9lq!5dtd~!AR#yxHYWGdN-5prfjYUYhtVwE>heuoklrmR zz8KI8sE!>yO1j(N?r~*QKZG5DL0G4F2WVI_2m6lX=RA(s0w+7Uc&VGS6XqCmSDpMP zUI=0gq-Qyc~IDV{3a*=4@N;AGxZ6R@ZK9&dE}7o*I() z45H7t=#1*46&<~KoIjb%>co zSX|t)IYy0Ft=G4#fgE{TA7+y&8q-x)pJJG8qWDZLA0o3~(?}D?4$oTS36wf@h>kes z^Hi_2&o$!TtX~d<;C|V3K8?*WCtduGnp7GapoM3_Ud*rCmS$HLQ9z7226iRh(f)RPXHG^PyzJX2etdHMHXQMvk4U zqQo%yd@f@Ck2Syll3|+_aTlVx*BQgBpP~GJ|0gBkeeE>=u&PHYoPK?f_UCM(iO-wq z>0o_EhBcYJO^wW!iP);kVMi~9OQjg~Rq=BhV(8)#`R}hG4|YyZr(O+4&F1N8Vjsoo zC<_(J=HFaD$YzO`Jzb?yaC}O1omhe2V+fIpp+kERF3I}bu3Fgj{bmHSQQ_m42Ug1D zQ}z_I1Jwf*7U|>(3+3YjRoh&ZfAXZr#WJ-a;#Qju)tTh|^Ts7$ zNPhbf(Q`K6JV?Bhjjs+)IGPbxZn+!_{!n%8yNcXb_1z*Fr{Vw#caAG}a)ubBIm-fs zECt3zOIO@T6hxP?QZG*)_&mcOee16=({|<7t0%cK8hBwXK5OPGJxYIFYLT(Nk!uxy zyC_+j_D-*M>gK>j?_-owuV9wqRw69Z^zx+=uhH$tEII+fjYHeEZ@~W*kv%=W#c$&` zc9_VuwHu!|e=xM|M4X;thVeq4E%Euw z|Bntr=&d-WBFd~o3*l`cSGz)P~ELnzd z9*wyywrdC9`f1s9LO!z&A7Q?cx~HVHd+Y9K3}v#E&2X{BM7-L_AWQB%*X6aGeJ|gY2GBzQJ(_-eGKMZ^ zq*6)Jpg7Vr_6f0&HG2W=h?hzvV_EF58L5J#LH#D*3*hM@GH&Pgo9ZPBDNClx4WCOV zqd|i_+ZlH8uJ;WJ@X_kAY_?|cWD0mH|9X)fXWbo%d{ZI0V5jpI^+%T{cD|qTb8FW< zE+0|J@yfUuo=H+T%&*7F)UUXe@=%Ec1TT%gBnv)0*gO~+sUS`3p`CB<{QUbHnwAY} zf8Rqr6|p7^I?_R;Ce{l|kT7s21H(Oc9?A;qTRXQ8dL@D^tind85G#c6tyX!HqzQ|P z_)0VF{($EhDNN)m74Y)xxXa%HOwUul&=(t68Bo!e1*_RgMFN-)2seM_N#MGU*M|-2 zvsCDx@o{ogQ8Pbh@;5`|v4&>uw_~_7qJ=CBTy8H~io)qU`qTWjwd3kXNoxfK_m7xe zrDwXEU0?^;C$qe1hoh}Z~Ao>=j%%NGM9Cm*8mHvqD_#s zCc`xbgk8lZh-=s=MHepV{5<(^aJkY1QS$B$3fYS*f*@tDa$4ES>itO|(z}nagS&#= z=#l5Z_aBS3VFW23ipQ!<1ys<#6a^Ee3EXJ#@?WRev*@_6w?C@9Xv{HRie{Ph- z;-kMS)yN!Q{d5Dcy-K#p3>_qt=c^YOFE#eD|H@9Qe_h~^eth*pGE(3q!!jFI+?bTQ znn!|_&E<0S$SL(kgGM6fGp~+^aaT@kl$q^c5B16wrGY+nhW&BAa`!LnPN<)iiF&$w z&Ku(*%)FbJ7%Nxgfo&U)2{cVRx>W-9Ssy(7QgrqS#J6D;)xnom{M!fFF-jg)3dL+4 zdg&ZP<}>cIMRA3^40GWxaT_1w=7{$l;ja$qQQ1BWjPpgkl6cuQ4!j_-T%86#_pofy z;36Gn!pDUBsp!1e?&v^*=EGJgA|}@~Znpf_roDGQG^}Cti^D9dSX#7A1@T(v@t+jZ z1fB>Vjchn+u$7NeS~9ETiLTuH&YfFQoG+%=M};>KZ@pPW+HA5D!gyX~wdE@}o8I zTMad3Vj^6DET*BmLwe3VlI7k_FMBEq)+79F;Tw-~LvyEI4PuIq4)d!_N~uXWK0g|c zzT(p>q!IT}RCrhSBdhzINGg{b+B@~mRSV?TxJA%er13Xc{JZ_`sBVMC$^1<7b7NF9 z^nNYx*KWo7HQjd7l!Gz`4(nHAn!f?XmotW!As>@H{w319_6_i(!EnQ~J_~nhSMa&@ ze{#E~CnOnS88pc_6eHXyaS9@>&B+kO5P#V&sPOI*IF+2{zJE9EZ z+KoS*NaU0-dYbk3goDrjMVfN)`b$EL+7n~w905fGH~XyN8>H=LWReE`sM&?l??LUK z6p2Rp`>kxOruw$3>cTVCGutK*(0qO52`0J&N>jIlVK6REZ^YW2N(tfk9Ma@&YD)=2 zJ@?u(Gv!xv-XcKz*SbiFp>$)LENrt>nY^m%>zLan+cDz(ooQ-leW99?qs3pG8n3Hw zZ{k76gBK#?kxkdU-(P5o`71u04hK|b9<{Io{M}_szGq zk`!@$u7Y<3ydD%8(6?dN0y53)`=uw6wgxp*mGZ8vqJ0bSCKU!( z0z_LMsoMaA5EySiC5T*VGtNC0eo>_bt`1XVk8^9;kQ;;0yRNnoRS#4&Pq(otqJTCr z;zF)aIrWVAH+tmxZ{xiA=iL4w98KR!$WP?{P+IxtKF-6gfH9L=Pf0j`y1?x5xHwp{ zpNJ8FEJa$HnwI!Hxb77wmFR1eLkcIBK>4Yk6w_sbf1h*!8iA1P(?FV7A1NC>TdpZS zdljPgn6ud9~)lV{ZGfm@Q{~JG5=gf~tea~u-b#DK4 zT4F4ZKsovYKaLCgklW(b`+1oHeN^fe&C3S{Qx*hO+jn(htZFR3b@C`u#+|*q;dH+- zCkJHGXROhzHIH80{q>Hfr+q}Fai9B~P&7j$*q>*)ExhvyKaGk(-Z9(#JMcLl9GYo# z)cKd>gA^{NWlf)TKoU{MVPL%e`#AmuDigGvv@10DCRAlOTy>I*s`OML<-5<~B>4GjB zz7;2+eWUVy8sb9xNTQ7{In`YsZk`@)eRKI)daBz+WReT*>+fB38Gz;i=rEGblko3B z`})~d#FsMfY=L(%9J#4FpDsedp$TnraVUn|j(YfEah>~)0UB9Qu&t>qe?3PhhVe*9 zM7{HX|L0?!P4m~yS?>5}Rmh0Y{1@(q!^7wTd(WYAo1Iz{m_$8_t6`#Z^p zi2=u(zezDcNkX5O50a3x7CL!=WegI7%iZ)nWwhj7t9I#>4&3I)41mR8;UHy>M2y&Oc%i{-0Mj|auTq;TX zDE87RzD?Uw$gp%?opYaDhh=ieaXEmE7+dKSh3C7%XuyJzA!pleInSFt!18l%`UTy( zsV*9~3I{=9QtJlK3^Bu@c&T~kuHhHdWYHXo>=y5$ zfR=XkDvn)g(_u6&?eh3Ohrl(~>*%h^#%82?e{#_y*2$k%mzB=zCYAFy(G<@$N>6zN zw^A)$)a`pi;R-K&_)b#A^COebU#vGxh4xw__Qx4?vhnE_+NZO8M4j-3Pqr1jPNDbi zH#p{lPuF0hUC!SZvn216t7JERkmD^BybatI+DP#r-)luo-q&2ELv{uBNIy=meG7Mr zUD6-&WO)*5K6-=W8TxmP(|qemQr%xz+~@R-6UU?tK*IQ(T6uGSq8ToaxXH4-)w}*b$n? zV_U^G7YqiazGwHGH9bX`@Sngx zquX?O&npLH;(PXiPLzn1nmN@ zKUSy=jnmFdNR0gphH};bO}Iq}K;sAve%{CWS+AP?J(b}$HB89VlV=PEm(%ap$udBT zkbCfE*yZSOzT@z|ZY> z&tLjFh1=B0sG@lh0*k|5)r@_STr-|q`>xi-y)}oMN`qK#!pDR z`e5;(Os5NXhe5F&>0ijK^w8ODu~MDQ=C@C$V#FUxIDTm%KfQdi|MckKN6wfe;dx%` zX0HI}5=5mD;Mt7-IykHwv&l`qZcwrYmr$IfI$1pxNfSl_$*~&ibCfYNTPWQHr!cZR ztqTH@`iSkQR1Hn7Xp8rBF>6m;A54#+3Z5)vMLVtLh7Pi_HB~IQisVjAL0_ULF_uAJl$M1z`3o~jVVB2>Z^A6g2|%b z#TirJIYB%a?cyfsmJ(C-C(19|aZY5LnDF;;(6s=Fv?)2cfq7zZ5ruuGXJwFaTcEih9noV&CE+YXO|boKBCH z5G}#Zfjnst*_wq@)KhsqKz0(!#NzPo7z}0bec}$Nq>NG7sX5Wn4SbbzNSE;$6ccahk=91Q1n!f*{NhEz{Jc#m>sh}|iVk>UTcIpntS1=Nhp z9KLP;yniH%j*7YAYq%5%(fDh-nF@qyYr5>dWiYWt04Rx2hv-YttDN@ccT^xMe_!<( zBl3EP$A0MpA@5Mu(OIp%b=OS&~5F?QI{L_y#7GBB!y1)vi@Q zO$h;s|CnD2tb&2XHP;*vj{bW1Qy%OpNMZwW4Kl!p8YB2AzzW1Iaen1tMxrI-Y#OLy zRvy6*7QpQD9yI8mR@+vK9-tcVdHpzL5cG=0eLfsCLAsGOu>NmyJ~5MsQp3sVPzI73 zSzq(nOo-hnH$%QtCt8Tz+@oU* z-IxeDX;PPe-l2r5xE;c}UIHsc>HS*>c2vJ)x2F%zCo&OHW_o*m z-bH_pkEFPnq9*~`EnS9R3%QECmo-Mp(l6Wn{v{sn_Sd2BW(;W0d+%@*RIFdD`J?yYJNkEfDH84T}M|INr+MW}QtB%LKQ}5aTW7Ks1Oy?XM((8Vq&ORA(T2 zjzZZYZ+1vIU8tI)uCh$R8y?4sJ!)f}f0LST+r$bACa z=kXyHq&{e4pE6~YODzsmKw*O+5jJN@96*YoL;Jw#GYFRFkB}CU)fNFYbOfYF0e8B4$cYcz9^4^c zmL3Cww{$8eKh@L&#ic^R4RawdO?PuGL^b1hT zJ$8ccG6El?;Xu$FcfVSKd01Uw=|`iHg7_zE|BQ882=tnfVW?ci+MoMik#iki+mQn( z@zMCbM7Mti#ZAl=ZPpYB!ANpv^3g#{54$Ac?X{oFY53Vi?=pWihk!MByu{L?V*=EO zcIc3K|l7k6*W2NVO~~C1|~%gYP&#DiCn$mjCPPEY#ORB%(kq!ALmT>t7P~#Fc+Z7sp0j3~f^!5Z^2GF9FdN(^@VF=Ol`@rt%!<>NR$*|YHi?(@uug#Cl(4K%04-C8 zQ?zBF;7J{9{d+*0EjV(0F6u@nED)|skOiA zdwgj@R#lYWgbJ)&5MT~e_6<=X4(FaVxr~FLRvtI9r}8^M!m0UV-O>Y_1Vkx_fOM;$ zPXlHx#K@1UrFInJQe06A)`EZfGlSj zoOf0cA$p*yp25dOT8PuWg4!Hl%4>XwS{^_&6zK~Q-W3C|j`Tonotmv?fU8o#o5PeL z^Jl5i*@_Irva+ixfD)oa|A)B&cnPQxaft5$Zb`7Pq1G(V2+CPt9<*};IKjI~jm5}V zA*hY~=G_E1MUzvE*zYc2K<8;x!_fSd`Xl2Y2Y)r@s~-qU%m>;LzfTa`EjD!#ULMW70RTbC1CFL<&1rMH#ZbJhs@mU&4Y+K%*d zKCuq2EKo|Q@p;ag4(#IHEDnwsVc^_$J>)~XwIm1IA`c_$8qNo>55$b&Q|6Y%G1paR(<^CHQ#sj894;86FluzC(u;mV62!8MG18u-`vVJ0(*~a2agjUBqq4=#BLR^2{N+|Y<2F5fpg^Uuz?kf z7%#5e@qgEt=xT#-61C5e=9*wriunI#!u7y z-##q<8Xy2GAi1-CiyOptrQCi1jO{!|;3XUM;bFsQAClE|8oHDJrn{dq&jEO`qH>cf zjsleZ%3e{zH{1`_cAy}F%L(32#Ip(RW&I)l zDmsQ^25w6N^{E6faeSb%7TR4yh~ZUPRU8 zGMjq$2zl^8a#uUN&+aSgC0TJ#pYDi2^ax6*&TLcTeLCRKZ zENT~LOIEM%8_n$NtEi#(dVK>zSZ4Qv2)5e& z^u1wCk$w@TDd$m+h0LbSgL#%BSglquEe&U9>b}%#mN9&1-hbJ0kUUnXv4i9PY~gH) z>Gci6K5pMjdd}cGz5De}e z>L>N4w3h*;tD7^cwjUD;OgmK{b5uw9Q5;v|t*)cT(nQ3Mbn^-g6Y(31zYW72l_X%i z`idwEV{UE0b_Sb`?3N+<`RaGc=#zql`1)T?Gi%4WJ^xtFT>Im}dPZk}b_VG2W!Nmf zBjSi|Stw zx^la_kroRtju94xe}4%&IZhv;I7T#iGCATT()a~)`hR3{!y6HWPA-fa5QbHw_F9k} zSEt5T$L{;rJA~K$OB)gYY7P9kiUuXY(%gFag=>C^8Y^pX-v4`Xfc^DN6BhJR7O4FH?zqGRlwZu)0DAE?DTnBqC(X2GK@Sq2KBklf*pBlHB znKWpYaBw9Cnt9=iAVvA5(dT_Tj0>ASTI{mRwMwOy%Is`nB1jT0}0{ zb+Qt_buD9zTJ<$W(p`D-$mTX}n`5Iw5(`_U=8BP>#W&v7cNaDttuI5Y)fWE}PmT)& zPi}MAct`J#l}kS8u2<8`1?KYoaE~ZN3Jmtz=IBX49mM zY6iU(L;YEeB&l7})#^pDp~-Wq_(BE?6+V>ltzr2C!HSCG>dVVd+N8@*6*+_hFueqR zRsHsqy!g~P(x&LH%6R?8qI~)NNl`9HhIDz^vajQ+-LychJ}Zkqu;@RqOuS&=3|}5L zxQ0muO+xa4!N>E)|?W&spqGXSS4`mHr(GnCp#m2g!#n!qBi$My9>BXah^1z*rzLX;%}UdcAy)V@Oi7X?4yh-Dncly=Hj1AGIw^Fp#`MVCf{;CY1m#8^-uA- z@NL;N<~9u*j!h|`O1eD)QQ)=pwz1vlJEB?-p(iL>hutgRZ3M)52KI6VHzQ5iC^HkU zPjmDVMj>l6nT^!_Y=F@N1JC?-yKNI)Kj|d(cB}8XMYo6d2J4CD^g_bmfUU3=O% zU>K(U?l$P{>cHAThDARw$QQRPYz^J|jmB6{n^tBLzeh#1rB{ykBg=^r-PX$X-L*|+ zAH+dE5 z6tA{LULvO9sSYt*!LH$by-M1F(4I|WY}4@egijGS1bQoWX7FqgXF&{*MA~4pr~?bk zY>cJ#XVoFn65;1dd?A|XoGK3FxGfspe)SWgyi+cl| z5O{GAz=yU@%o~g>pO>SyZIMY!SV#N7|54>;?`N1hQ+yUb55bAE8o-v+bglc@+YH@} z1CR~2z0DP=t0hB4viNtdtEOTpbG4+CU!^0{3Ko`6x;HuI(jj2@PM@d85Ok0^zmgv5 zI1q5U19{C&6+9=fKV)c8PC|jT*qN-T|ISmYl&hi%*^HS*?BG3lCq7s(AOC2lbt@g# za!QM)GxxLG2~|HXWK45n<3lp_4$ndqniu#2jiEViEB?G6<6MTf&%>t6J!AYu0bhIw-` zfS#=lJeP|3^J)(6-FF*?Pd!w>_6mTS2kY~~=HzTOyf9SL8&d`qXNI%U+aVJ_fFbG@<@Do+l@-K(Md z-^vpxC#mF|W|DG@GLa@sl)kmbhA;<3bkM8Y50R?hGnU~_qIr1fs%y! zOB(Pnt?6R~o~~fmu~#LJLbut-4|Q60)j*q~Gp)4(d_#Ee$k>s-T>wo^g1Z@I+hZLX zV~@RlU|_2$(#YLHm>6^8p!RN@EH`V|?9=Ci$fcP)LZBXY?^e!By5n})IR|u_i6lTL z56%ufWG(kEQi;rlF)$DT<3o?`@uAN!f^mbV!@G||Hz0UR79PHS}ZVVc5}$F-MeS)dPq$AHS&!MLSQd$DIo)-R|fL z;k`R@-^6b<)cX~~MzInFt&~DU_dhG4I%SQOmCm*gf>^PazI<8K;ophby(r+2DDWwg zO%unbRzIS}^Yg2ZPS3tF>k8CgL^+Ono4r{*8j#l_U&<%{VXR04>nXBm&Q3M5a4xg( z?ML;p62sEIf*b$HLIkI8Wtoa^9G>RxBbezT3%N22%goiwQVmPxRyVIjnF{xqK5%EK z_20-f+N8XdU@F{U`oNr_Mt3d8Ox!t)P^-aUQF^qT{I*o9Y)fq42~g~sMNClcaoYNSs3p0wd^ipS$$S+-7`ODcV+wt*R(VjbnB?{`Tuyj z%77}GH7t!ZNJ@)zODWwTAl)I|A&rRAts+QwC|x4b-QC^Y-O_h=@w@-V*>~QKanCsq z2i>&yc&*Ayz`xB@7$k~Ee>sUEQN=uM*6QX-R6aO8IAoGlmt^7!Emb|Uk*S+tR4b84 zHV)2|bd1T1d6SFzA@gm+ts`N{D008DS}FaRl8~3VUL$p@a?2}OwXQ7HytlbQ9;wMv zOk4@2s$XqnK8@+`YTsSrYt!eqeo;<|&m7(tH_@(=KNrFb9;wr&Io;N7lJiQ z{rvp({gb$X?yPTq#qS0&vA@6#9EWVJu$i@IHjDN=Gc*boOJ6|>HKfSlGa9Q^&+gPC z634h>XK($nB;@$nfF$SIu(_wANVJC>AYF+yx_%&CTjvkX-hlCwiqm|y==^AT%DtUR zWbO&dn-aEk(=fIY@ylGl?hugcvx7iS_y$*|dCMOgHUz%a%wVG$@p5sUo;+K(PSa(mSeKlgdIsM zUbOc0b>#=d5I48tu$yn6VuKvTsrxjE2N9k&X&WgH{9$DtNi(4~ugFLGeI6xk`|&`S0dajeX^dmPgCgNnXU8T-dzHn(Dt34P}c2K}^r z6<(T*fR7IBp_;i*g@CSaTJh9qRkf&;!=eeSJU4x1^qh0XCv892a%F}h#R3Tp4zS(d zM+keMl!NT?c1li38-K%~qc9K?rVl#dgowV?R4HC5?)nDU6oCBsvvYnR{w_kjJ3RPD zJ8||?s4B!Af6OBpGg%OQjswwVAP=cni^HD(F1nCtz9*p?W{U;)~=b z!T+v+dHk@jF|3CDcdK_b$FevWCWo|)^Z3F38uk}$7yMdAa0mjpqk8NaW9xmee8=sb zxYrCr@_l&0j}$SLj4$(p1LGrB6D~%G3ak_X482j%aP|kImSr0`&P4(?S=Tv zsRA9kp!x<4_;cU(dP-FT+^t{4e*Jj&MK0Zv#GlZy^ZoN2k7xE*44sgW? zi=Z#scN-1{6Wyq*sXq91m4Va}VZ5m0CsHGImX(M!uXf{&r=zSFkCFq8UD}ObzXR56 zU{3svA)$xU%fc?8#+amDd+?0`@qmS4aWL6 z2F);^H?*=~0IZ%2`+oVxt!>dV@rDcT#-(l1Gx5aH{O1h<6!VtG+sR$eg`_Cw-G5A< z&#w;ukl?rdnW7`cqM+MaD(RQV>g|*dZxyT*L;$y;{&*C7?OR0^a{(0~Abrw6T^W8w zcn)(rzlIfM_lB5QO!cENhX~~Ad=lPW?CB*Q?wI#+MyAZ65o!C6lUEvXeUd*J8;2Ag zpp4-`d0pdajBJJ*;=Y_S`Uj;c_!;{Ei8=L5H|qWnjZV_euvRou^iR~c6RYwyP_P2Af^tVyZZP^%~8@R1M+%f zqu?Qxnjia}W?+w0D&0=TZ|2=@Z^sflmg>WAdZ~EPl93-u56ia!>{Xl-R6hffC-p7< ziD331Mpa3Pc+DPqSg;Y0u+8bCOWCk|EA8z#=ESd<0X2cvHxyIYj)3TA_}vVVxBj&&FNUi`{s3=YUJIwMSGWDs#&yVQ`dJ-c%rq6Y1cPf(Al*TzW8YuDN1wU zY++T6_wtj-Nw!uDkfL=H!qT)Xe_0I?Arh-%KBg86l*;;oHMV%FzgH}jsqs;5Qth$= zGw3vQL38@#+N-U~y06>Jamey#L-4KNh_=hP5l4ZYEh%{HG%uaiU1m_V~)9`eM*soc>QYsGvt zP8=JGW$I)Uk?=XqQ8zqF;-h`iDaMA<$*-JTQeCz4(Q-=-iuSdMJjy6tVtU7_Ru7ZS ze;!o6>kZD4t(~qdjHriGthg4SnH2H6wRhf4F-LDv>Nz)4RR(g+7;#8dry}4ApV}Kwi>XW7q3U zLHKH<=nwpn!8n#x;T3g8U|p~m$MU@1GINVg^nHwr9Kmm@s&Nur%2`HUc>e+~9cSE7 zEaGWV?zX6S?`N$s<>8ZUMShC@lSBBwv)tz?=|p!eQMG1=o5>}lF{VXh>U!88Nd&{$ zQaUYUJ4;|MOY3y{QtMx(A$Psj>HK1fneYSIA%odOJ)dAt(f_4WF^1QGWN<2Ht3u{? z;IG-O2^Xe>mjv7oslWX$r7vCLhB;{G$%Jo)>omZG|5GvGHLl~mLA>Be_wp0&o4vwe zp)$v_);p%&x0LB9$+|dnXR)TwvKLM*-QRi1#ViY4&9bY~&+-V<$aZnKcF+Lp3UIBz z--rS3i_pj(5nZk#=Hc$oP()^`GMrtJ<>97i=w!*zYpq+s0{c3Yk(p7K*ZtHWy%>#b zNgSIYKS&SkPyX$X3uFkm4(v3c*Jnh>5Q}d9Bx~|Jio8N9OdMFNm;isHD7A7eN4fId zXFwg-Pb*JBHj)6FhF9;nLH1OUnX6wke`Bek4D?0hDwAngkK?P!uv9Cj%vnz$sD+~H z(Z9yb8k0ynW;2=&HCpc|Wk#hjn#Nzx z73L_*3`gU*I%c4$$oGsP_6tSfQ$7TLtAW7l&8eBhDfw>L{U`# z{1|GwOs=?wOhxMzwPL1f7Nf*lx*Y1fC^&(>~}fJR>RTCXTu%Yz-lO3+3D}M31Br6J$&`?HGwL<%chJa z`CCfNyr^pDOirS8%MgZ4GCg|cP=anBhD^b1+VaKjhQYm;eldlN@6pjlf6g}?q)6Ezgxx<~fx>VKNsI3Xlmo6a< zVh4F2%JS9TSG4~5ag@c7!+B@Q`3*MZ>oBk;Hmqv;Lv;wO+*fuOM#Ab6^U}J`W!GF{A5@T#K@a)6JK?_W(Y2VXPDIh(NE_`_Xq zC0@CGUn&0d`O1Qbl(3|a&)NQQ_u`ZQu%>sfxTt;c-nAb)Txr*!#_PT5;cfyI#gFDY z3e>km{Cw-kopiA^3mR{UPK%?qSq!T{f+&E%F}yoP195=m73>S9Z!NG7*kr4sr>mqG;K&X!-$)Z@~^x6!m^3ANY-P~2czzSW z+LZ5u(40qbnWvmW{C=J=uK(BO(6o{B2I4kyO9L%#20xFYIboISoO-nroQpqIbiIUP z3ULmH_%}6|Og0F(M7m91la{ zux7yF>|w0+gmM(1(20)fJntPOLS?@^P6Cu_zuam0YOczM*LTH_I*S5%xr(x+aq%rg z$G*x`o!pJN^lL@taDTP16S zLR)IP9B+mQ3p1wHc5_U+?dzT@Fkkb((Ok>(>d2hj4N-5ok(5NAU{ZY%FMipPt!Gv> zSv}LwXfIYQUzD33Sz7Bq_RXz=(+OFf-gF5kz~+}+Q;qu=C-oAa^Jg@Ab|kgq;bSvF zF&QS|Xp-P#Iz#CY>KvSkJSsFV&t1ZcpVN<$1Q#9Tu{plrL6U=@FA^7zFYZ2$MA5mO z#Rr3%f(hAwGp_x!zEhQ<#z=wm>zFlxY1jVOj{+=ZXwDK#QRiqmqi6T)u4zG8uM4ya zu_2);^BO^b&?hWe^dXbnwy5A{mHDSuM36oyWlM#4j+S30#;nw2iC{aneq=~L6B&KT zLo|6z%YAEe4L|pTW0n+tT``KRJEcoi1|XOIW*> zpA?gtaY>d%*Ri`9JgV;BZ9w0XC}k23%u%g22vLvaRwA4p7u-?&GS|a9P_z|-E|}k2 z8_DI6FQ8>AUm8;{tkhn!H18j@T)t z+IwyK@x0a0f&jCH=}51Q8l|-}6nEM&?XT*c1wl=s%BJ3L(~&$*4sL2s{okhl^)0fq z2q?h$t#yqtEFJBbEccyED!gnwNj;f3DRH5o@NFSx&z)WBTI}{2V@socrs`c{a0aSI z&+`Tr7)H|s!`Z2 z-O7gX=a$W)CBA%>1$9UsKU)61b}&Sj8SMIU6-TZ;fQpmP`4^*uGNdXy*XoAIgn_?R zupp_Mc(4(L1>bVF(!f=C51S}gYwgV4K^w?1+^PMnI&BBstKw=?7H7x$LyE0);DI_B zK$xz*Y`_7_EFrMjL?I>?r!*H$`@9A8Q11jb!CG*PpEfwL9T ztkH>q)%%8>NBPuiE*-B;c+j;qJ@RasBGv>@CDp6$6Jwx5+FlTIF;avFyp&esy@0A{ zX-gtwTPJhZATi>!U1h|z!JElBk>)=w&BqwUkd}vL!?!WvJM6G!Eirg&UXb1CCr1at zZ@!Q2Qy5M+?b$K5zKYy?Ie6aE{0p=G7b80?SL;_#>%b7uaHEqBK0iY@q{kyzU9Xjb z0G6W@<4g)J+i>lB!;RvJ{y~ADm!)11Xu7bTF%8Ksz=`n9&>~D4#)yJRO#!ZCNF- z`QnQ(`ccW55E8`FK;?|+@I#a^y2I~JYKsW;KD^Bb#piS01V->yA!|-dF`tc}TX;OH z^<1KX8Fr_hJ`r0oT_=%h3i$vhs#GJcZR$RhiA6f@&NJGs zU+55h%c(woD+s|GZ{p$(R4m*}W&-_{_tI9MSJgslVp20> zFGIfLAA$N2S!gDN{27N3q|J-TkFh~C)PG{ zOJ-yD)zX2!<%}pS`Unf%i2Yb{(dd$cb$7zk!!HX%aO$P~4V_&deHzaKajK$&S{)R1+1e!+}wek&xH|j0*FBMxGCUz~sO5n3| zz?bJWj95sp+XjHFkGJZ`N8JW|U>y!`O@8o6wgKFEm8pC68A`H&BIPn->^RR9NBEYD{UD*Lekg6b;kpr6zUlT>jBJf4a(W;_DU#Xe7dYcx z+%&F(a`Vpe@GZhK=%GjM0tl`JUqeW{Ss(L16wU~9LIL)4JU30?uanoGXrTZp=tMQT zt2A?ppIu6LpJe%O1?i1>{zTakCw{``?`Baa0&u(CGV1xn=R<04Y!tM9vf+&10v1c= zw+XBfm_|XVj%Ey&*D6>)VB#>~Y|_s__>MjbSCg>$ITT1z#w;6)6w3(gyo~v9tUCw(Z4-GHvw!s-$2M;}?60 zf0f4%4*7B%RY0A!po5)(iTCCc+fi-?6e zxYst27z8z_rNi7&S07S6;C-EV>>$iTx8>)$D*6Q=%aycTPf9sKg-TIU6Z)PC;ev}7 z-&He-pXjJR0n7=pH@0e{bw3qTK?5lTQq%32&0Ya@O<{Y4;V}1fxAW-}KBj!Eph}xQ zSD-yNP4^z95BBE+pX)p>PC1bPTO_;tnnHYI7_cXve`|Xj3bpZ>H5U}aWOcy7(;ip* zxJ_(>t}Se(K3@@BRe=z0zu&ug4)wkwR{8YxOW0&pVLK^O%;#J;cu!L@kOG zPK1A~-sE`kR=toeLi=v9CL&Wev>9K$_o2Ie?@Avrm`>@iVTg^#DQX+|GwB9ejl~Q8 znOa4{xn5TbcoY*iF}7wgch?R8cD!gbOoPnkJ&zN|jMt9SJy{4$(#Js#>?e)7ijRuV zGe;ZYe_`P^cF|br$UTx3f%#bUsuGRHJ38%=@>7DpUmhDU(|O-Ux{}dM2J|+;n61zI zSW_0Uz+)<=jZ6{x>LHUgo;Ow-!Pj8%3sdv4&W3!+M{R=hmmP4UnhaQk;nx%5q?`<3 zYJ{1_6F5-#1w3PVHQ|ZS1aL4ov72%PIZzy#X&QoyG%<`qe**$vB>UvZlek0puol=d=3ZN7mzkbcH+l^YP* zd;^BiGf*3vg;}T1Apb+xuU3+vGIG3iZtS2kSo(i{egPx~rgpy7Yc0dl0H`XD+(IHD zruH?SV#uzP@4cxVzWyiCvc!ZAI$3VC_t96c6OlxC?s>fcxOHf7S^@`&J|U$_#Mu=+ zd~~alSO@7~CcvT+59$#2CwEN)IEPQNxYG8x5`TFcAXCa@m3|BgnsZkdq5yTa!x&ta zHvt`Bpxi}J4{;qQXcGdsx-LE%BU`u<1`Tnx+|LI>v=mHBgt+*!)lNO{3TlK=014TbSElHFH7rURW&m#>893LTV zrf4Jni(}K0>uZREMZT2=WUQw#UgZ-a(B<}(({wSQ7L}O^5wXBY|Ak$LiWLbc?l=3n zE>IMRLX?gmN36s40bgGSH1+CtCxLWOj&aTf>{)|HvPj^?evoLe{M%3#5)`aU%JT%& znWI3@{vmUIEmZiBgWgeWU7ke%;_>(ntuHY5!A5$7&#m+Iq-K9xP?;$(@zeSp43i=SNRKiEM6- z=&zDLQl@$Te67FIKHNnB3rknyIQEkFQdF_Qr~dkU2;9n1s&u9U$SA!33c_(iRD@N{ zR%}2dF)+gaN`zTcwt`RjP>O+{)&zJzN+{gU&xmA&hd(Tmrq~w`j02Xx^4m0LZSWw% zqST4rpWyRFU0N?PK$!5O*E7s_>EM=q_YwK*7iN&|QAdj@A|%fKNYKBA!3X4Hf2UVB z14&VDMQbd>GOEYIY8<*lLl4wY~Rf4|XNK zXfNOs&A;F9F9`7=>DzfMQd{5&X$(oARDwZ!{GRDT8cniS4TwR-nPw!!!3ar^@C}?C z0UfL9c8Au*b9VShHnLtd8`Ny@vSZO4&@9bvUB0hn?r-;4fK3xj-hN)dKOf6~MSVZm zMc`Xad^=Uc51AF`5Co(WvIDG*q6bKvvF(3Gt+XTWqch{9UXtO>j0=j*l<+*`c6Rtr^WKK7!;~ zrt3Kv6G37mNs{Wmf?^tLe?HemrCbM`T2M>mM~oi=rT&og5=13_Q+6%4t0*A*WQO{eFI{}ty;{p8hw-P}9 zSapn_lw3kytt4rvheQi6_B^|Q^x}M@s0Y=9;>I23>_9EmI;u#SQjlpCbU=<&+ZqRo zr0n(fk~T7^)Z)tZWifQtpDytG4Z}13#$T}~K<9@%c4j-y1$q0>)b2Vo@KNuZZUR^o z>Fpb{!X`~bxJN(+Do*Xx^jaCiQh+X;WgHq_^s#^*2tN`MvZD)o>sK@JbyF0L>CcW1xvySu)4Rp2;xK5+ zzSniHYvSOPbOt(B=CgWiP}Z+^M)kx{tD85U^MHC-wk$dy)YpN(RhG^O^dAHatam&^ z226g~U~lZWJQ9I~*6IoPy|ehT2{c-lv6>F(?E2B|J7^NJU@8#KvYLn!{6<9TOdRQ1 zbkL;&x?*D#Gc^^|%c25Y~Mq$B?0>O3tC;62O%ybPVtap zcBK-sT_qcSkql7+?2Uu9cA*|5ph8w!)ZO!ow0l5wU)M8 z3eOitTi`~~=Y6j|AA!oH&7Q_q4#4T^b&`-QHa9UocVljbv*`GG{?JQYBdWFBj;fBY~Va*xY-pyo-gVq?F62}Pe%b5rv8jR3!J5Qo?f zbb+W;6FrAK)ws2pxB)fRFZsIgJTWk!iWD|d_Lh)Z?t(M#peup_k8{@xXiGj1TJZDAoOqgA;XSbvW}&;i_Mzm5Bn- z@R>)G)|Q0~b~d<>0Y`ia5?S{&bPwosyEy=U+6B}OzI@TOA7tZVBBQkDW8l9eos)f) zn4C~jn(F*%|MXKFsFDv&hDGh4(nP@gd|xs|@~`?0l)BvBJ&eLU<8A#0{Yt!0Y}5aP7)lm1wv`nw5MQCZUO1fyJj79 zEe($0vR^Jm-1~6oP{fB4h5oC16Kdxn$cU6XO%o}=)a|!9GsKXT%DXGP2Pva&qk=~VPOyT7H(mnIJ)XtlR^o}b|S^J4}%OP0b}&SGMNZuuBMj9zwMm9g_EpB zXxsNgm-&thIK4-?6xYsQL6N(@mTM6}3k5qQn^~{tf2D1XxYqPId#i=~l|I`Y9!ubD=Q}O!^bltHp zJqlug{P2_@^}p6aA?X$i2}&q9YWycWeB#YlsILSQH~tnw6rPuRR3<|?kwt(Rp3e!q zU{?5)<`pl>5#~0ovQTmr)@x6>4^tDTRBT`-@Kvq|P1W*dgt6Pis!;Swst@MPcQe5% zHjUz}kE^3M7F>M_rlY4od&vpk0$^Y|RB6vtHe+Xgb!LYT9~8c=QmmA#Ym zyFomsu!8LTK~eGv2}6Fv9P_r?$^9R~xHEcLLhb8Md}NRRNEx=Sywh`kuBjNqMl+m; z`3+vS8>fB3@pL1?Ktiker<5Js`{$ZYGHf34l6*2_1R(ATaFb>BHhz12?QAk?QH zc=8A@lQ#C_=9l8AYu?Nsoat>BZ~w|v6&v!(EZ`kl1K-rreJS{6{5svzTN9&EjG3SU_gk<91&TuwQfu}lHkpR%{iBdn@R*|fhWE`Gtt&f=W91r;3H zMoaw6T%I_I_S42Y-zi@HltszXxp77iBm^JNfp_XY+)_15)p`;vqCH4lV-{2?6)ZLA z_4&zuye};0Fh%sS*TEm8a}-6iLI-(;#xOoE&xG?u^E_XH$fwhB=d-{HE?Rq?bFXAT z6(Q)In={8=F0ksLJ?%B07E8@Vk-wJtoZ#$bHT5#F#?&iUbU*v`H6z?}?R&*G;Slw# z(LBx%Ry1FS&-?Dp{8JJF4U1o}%JG%nXx&S`@z$kRGf^@t1dy+Nnfr80W5RPr?i`5@ z&csw*=31p{9_OxWeWuE-j=9(?^jCG#&lXo(2xlb>Dwt-F%Nl#!Nb$NVR)Lc0=^**V zdGXh0z=ABGPJ2|0r%X8$TO6*d*h(=u+)CIl3T%tQ*kmipBlU!x%N=)qX)IFj8QOKzNHfyX5ZeCge%0@KNpCU0?HQC!jpC|7w z1{^!K(qb5KmA{JfiWhNlUfOc_5|S84 zkF;!<9Q@;TA~XPV@?rPYxag;v?L~?@S;Kd3turq%h&}h zZpeiR?sAe`6-@0>uc@u7?i+;|m>E@fB~KVQP8rlta(L-wH#)b9k;k@&wZAnmsODa~ zy+saxjXi%9pJyT_wDU|?rne~y_wsIqD;5Wpjk+eNPP;p^6-TZsO?`mn$Cbai47<{6 zY0ZxO9KrPbpZ7lsxs*PMYIF@&an+;&PpgWAxvk~y?Ska5ccNjYNKNA^YGsPKE(zR$ zFaOmUXo=&$32YEV`I+s0e1lE*A+0_8^+;zE-G}%1zeBbxu{d4_WCUH`a=j9$drGDq z2&`?nUkS`*xG@V4wVc=`#-+&L+oS|!bRbGAT(?_iWo(up46ls*_9Dg=a25X0&9pa! zEAdSrJOziGcT{XB5>EP?+YbJi(&@`bTe&B$&TOG%QWOGQiI@=?n_Z88W^Wt0u;L2H z3x8m!N8SID(GjmRaET@m@OyW3;wNhi@WaWwr&`lg1=@J{6eBuOAt_28+Z?e92xsIL z-AOjy@1Gu9b4!T*?B?`u3d9VTlv+<%+#>(`w%B6ouo!Vfxc5-O$NZ9$e*nY65qTr8 zu-ZSQ5x5W{h{jj^$TE6npA2vAGw8Xy8)qqZUOWN0FLa_Ebq$G5D@<r!bs@NiOdP+3 zDJ~G3V-Ro0f^8oFC;>1}mL#lZxFGb0@N{A!2%+RdG%K$Uh?1gsvIFOrs{~<@q%Qmg z-l77C6LDG%p8*^SCL)TPk#@WZX^?p{YDZ})1ZHt$IEU`J)dfL#Ty4WvssQ*=Mm}k0 zsg{StPXI7R2e^wK=7b9fY~+TtY%Bm&f-Qfz z;02^$P>>WCphAI`ZvO@O;)BgEInq*zL7OS<4tV@tH$b|2P3VX4l{tW@K9ASco&cVp zX+Y>G1(Cu4v=iVPR(lY7#bzd>YXm6bu%)E!5d?*i!O)fmL1w_R4zS39$QGK{u6Hf~ zz)K8_=yfnlrR4H9fPNCFu(JjMro7}>FKA*FgWxV^2T9wLry&rpF!YIQhz&~==|?{R z#m(4z(Af`tf46rG@Symvu8%c{ezjVmSo>ms4ca`dnMAF~bp&;07PxW927tx@qdvMO zH4O{`AS1g(6qWewH*=B@kVpV?_Tsz^fXsl8T>fo+Y5w6{=?DF`e+u9Tb*~8CK?HBC z@bUp%lQjeH@QZk^eH}88@sgD0D+q!Jz-PH5cAGUdguuAUTX1LF+`*;c{58d_#&amR zH5?@%<_Z!ba5wZ>mRn*(y-1H zGT_1pnC9cs3wt*JI~P#gfrn<%&SnL4r@CuuNKIWhn8OyH%k29!3XlpNwT%BEj4gHS zp?fk-1cyaM!Zd3rxam?s(OrY{cz{&{c-U_G);OhrJl+d@1sv}3V@g#*2Fc&O|#n+qT;0ib83+>d$2 z10lV*YA!JV6pY!b2zYEq{7o%yDESyVrVye0CUVpx1i>BP-M?=mKqwEa^tV5tvij|3 zJptij&?$P-7d=2r;cz9cK+p~!iA^T_K~{qtC7k=C5D40#J^(+e3RQ#>XTNoU1ab?7At`8t$A1H|?qybPB`v!y{+R9iC$UW%<4wY~4Ag!0(F#j`P zVlc%Rii~cN3(!&_Bo>~JJInJnR3L^;($(Fg3wiLAomS@*YK;9e~Ssx(Q z*!*R5K16|jsPLg2#s8;*I>*jpJpjr9kg^>Vwtrd)0r_I_IO6=vvc+=?;{R~gUcD+; z&j1~1&+5s&Z)XU27XUG#k9FWFhw_U*X9C&HgA!7At}e7Pksr!1!7p+C{}A7p2d+Ln zZ=i?=N?cI?zbwbKbplEpQWu%Op2Tf~AM&HSm)Ae%Em?~&&aOQ8?4$@?NCL{Hyie5s zA>OThR9_|we}JwLe1>=t#{h9yO?R&hdO#z?|2Xs2!-N>{)IQmkWa2zjHwmk}K$iw{ z1U?l&-=f3;y=R>_ap1}svRR;_NoukH4J@GG zY)~^ZCee+*KQ?Js5{#&iZiT~Ff6)Dl%ZaDo!D?R|8SsQgU;68^l>f@lq`6rbnq-%> z8|a>)qtJ{u^xm`ed*}WI1n#nHbCZE!B~?%U5Q>8nZnC~OritSLaZD)aLJ>lnGHX+N z@i(ReI1WI}ee|b-dd^}!#neO@BUo347hlLWpgN~VcU+XWg;Lfftim3`kE%Q+G(UWT z0}NwEMC5*V8KPNc8wmExeNT`Sx}j)SO_)LrAhM-J0OI2*GItLt##E?2v`G6L&~_XR ztCzUE{a-DXy~dt{j>yi`fsm@eAa!F)`ep($PSYDHgaAsLI`Rve6e6AuMc5EhoGNT9 zK}egwoDSLj9-~2*gDS-O_y^3YZ4NgGev{Hj;>-c;!0-#O}z&XjIsW8 z+AxIKJP2;}ucPO;rWI+3&%hk*e|DQ^*YQ-6LzkYVp&dBH6;wcZS2QdfM>%o`Qf#ok z+WZhX5QN=oE0>aV(%tW? zq@V+D-Oc16yPfU+5I{W$fC9|hn|=UtEq$%;r;iVS(gBnH&|!W0BQ#Suy69CP_X;J= zyrBW5{mK7p3rsQK4j|SAn*tF%c{^tg0DA%0q+woyLI2u(!tmw;5NWvYMGAB;luW4& zgc9C|(z)9K_}Lhk2g=Jo06fs|RB-+{{&*&SEn8HkY2KOzzr5kFN7fL3J8Df zYD#Dq@GAgc80=PJBM&AqUHpy>3FO216lzMyDp-jD#E1LryrwN%9B_F6E<4M?myCcj z%@L8E-ZBAq8L$;JcVAIyLyV`r^qd31+ET1BmH4l}bWThHfVx0-TwlTfsGtCtYK$=6 zui$L#6fxb0`TJe37XorJ9N+D$lm#&q4UoSv?9yJIry?}_G3sGJ@KAB;jbL8O^#F!$ zCi6x@2%)js(OM857v;~c`Ue=&f|-t`e->|4l+THR{j7=J0Zi+J7@z?tmP=dC{&0t+ zxD!l|W!nY-!q{uL45T|AP(dipcW=&+kVhj8#KOT1%?; zj>^!yod03`fLgk_;OXC_ARPy4KA5(OsY?Y-gI;}4Y@RDjsAI~HQ71!=JPj{yO`e8k zb$S1vjy`_?Ma|b^_0k=Qrz;Ww5@QplJ7zzB7}Dg|fpgGBz~14b-fRKz#vFix!`WxH zg*b7}5i32wTF!*HI5@FY@_uKA^KW~7Y5&kf**r~N&x$=nip$Oa3&iPH|Hl6ZTbiCg zG;kT3_1lORL2O{M0c@W|L_A1*!0_5xo2smOh`{HRx-U=!6dUB^XKg`{ReM(3$X$D$ zduVpw?5f^lK!G8Bcd-0W(oHNkO<0hd=DfITq3|~qej|mTOFQ*f|H4Ag-kLfQ-TeLq zL9y{oe5{0EM4Rrm^B6&=bF)Q-HVGVna9@FJ0t05Ve__FiJ5}X#^ML#n5^afA&8OdypJ$NF!{CfVs>9IP~^aDzSq*?sBWYGSUJ*CtQ=(cxwX@UO$ zSC%tEG&{==IlQEzW4YY`LP`6W^B4P~7P{s5N~PLL0eqU~3|d|PE3zKy`pP2&C~FlI zYF$B(SD1Tfy#7~a>BU|l2cThZBWmM=h;y0&>|jEQ&F(+Ykf}Man}3Q8GxP-Js7?Rzy2>B5ikHk7ksgC#h^ArezHzcc zb*v>d$-M91`^9u}%^zcS{(-r3wpTtghJK`E`0lb(ha`W zwu-G)=@Fg?6IU1CVv6WIxdum*Lk9275H>Z!@f|@F*b$}TwE5;Fnl>>9AN&NfL2HruYJq8CA7n;hDc}fVVF#R zX7FIR`RRvYM4gUyvk0^6>yAGSeU`b4H3o77_GcZ3$y2CoLD+0(8N;+5F!*e+@&Y+i z93C(+Y_R2Z+bO+UFw-aqg=jl@cXaOwoD5l+yLL~9R)dqc^y>dF6`nyt?l4i6Kq}nt5x%5-4aPfzT z`Hjq||QW3mC?ih5L%mk;V3CvW8W#|k&qio*$~60)ngF4Wvv z6BDt#zVeIbD`)9_{*5K@IGVR!;q@Q+(MZLL! zqgI0p21k@ppD`=GUR(V7tJuFB^W#P6Mi&#=&xlNcp-b>xQ@s4a{yUMDXZeX}(>R$L z^(s8yddo!jtqxw~;q@0}p`xSRRBRyTh~DD92vMgz8_A!}ZW({wH=sI{&wJ5WXsnec zz;WOF=a{#uK{evMG*p;_SSVUP8wF;BYujclfv%d#g@NnBwu(8H^)Bj#{P|sSPv^^9 zi(g@+q_5E9Nezpou2M_P^ke)QVDl2YMmgNrw zR2~RE+h?f?dR8W-6pEkj!=myeT~SYvh{jHC70&*6Z9Z?fvrtDs-RZ5G`!E|N1e+l_ zkmoO1AHl9?l{rQ(ieBAaKIA570} ztiGRlcF@r6rRx#&dIg!w^d*nGY@6vsZq)ZjB^=e|tM-)B49q3bbMe^HYf#Q4@L$+x zjq=Y4qnz5W0LtcDh1t`|QqAE{p0*rBzU zq69F=EvI@0WsO9Z9S+8fs)Z$=nn>7aAEO_Y?>6utUY2q$+vgrnAk&TPBl_Vg7v2f) zS}G1}S#4x$I7}vCVq!7gRaj_H^zfksoGt`Z@5-ipDz$PdGIu&#L9@H#AUI+fr21h} zBDC-`FR&ACRg6PF^X~E~*ZR)-uax_aIW2FVGA0N9TqV{h{0U{<(3+YW9qCBU*NHTg zqHC~ibIHq!X}HW7ZF8YDdb4xMiMXOp7LL!&-Q*n?F6L_%0=Q4E-C6sOsMz@TiPCjOd_w&#VtJ(GrOfbUNoBEE( z0E$?;5Os5f4`w!WLXmw0KUFPvcTgFQ>l+!k^~1$2fWL)zQ%{+xcupZW;ae+np+vbW zlC+~WczW|8N+8%tU7n&_Z<|P0vhV7q0Rh=O(?uW?_JpDfoB7K;Sme$-$Z`-!Xt4Hbb#mF8!o5Fc0T+K ztH8)pzK82-4pR=FEA;cdKqxdkL71B3r7QP)hYo01SLioxa?MX>in;tzyV_&c>U#j` z3R!Dain`!YKb;5GhVN`Wo@U@LB!{Bx<=TL^aUB$sB4;WCh|f{^aE-AF$dr}?27paX z5iZ<0{UuUAgR`7&?xJt*Xuf!-VlzP|hCxPXzGEvot4->Fbp>gXZvA!be27X(66Ks* zUGa9(OxU3aFQVJu>nT&I#(j?4IWcwFr9XP^8;q3t>BR`YXM*L6Sk}2DMfwB7S9f z$y9AKc!%Hh*-I`KeR_f6dDMagxCm3#F)n-iT;k)8O5yI5!nt@Fb^YtC;CGW#+X$89 zgkUH{DB@P$IADDtr=_0zAqqLuR87e*U!4sXcIXv|0gnf5JR^Iv{G zQZHdYdQ*tS-~yYjD(ZfKBcM{Ubd;PEmo`KHcRFfZRc^I{Wrzchi6`qZ-jmBr*;BbO z*1gZbrvVCx!UL+Ar|0zmNf+#9JOi2@xqO>=ikx=}{i@9O;Ls5MsYe1TrImamIdQW- z@lW+Cc~e5W;No&UyGp5Y;`n{ytuO`grP@{zdPmFRMtB)$-Wg(-wsuN6%m0`idcIw^6PIC0I8n&hZfk`u4;v zAD)W5GRO!;Q&1(0@z#kOsWiSj} zG1m;a&--D*WW%DWIPpjHacQ%cGNmKoaWt!P+}HRg`QKXfwubdS^`U=n2Q}>&80pzyV8g$hPaAtQ3UysZ&N51_STySb|+t_ z7$&!$eE0rE(1=PHb-S)%#}PWk&{=W1@j7W!hoJE(01$rOuqyIkTrV0%$MeAUrP8d053 zrhze25biN`lex0$~K&^eIAK7+TlybHcmZ56!p-AtrDB&?a9rZE5UW(z%7s?@< zvGQ-O$|Ps9ewts-?Fn_>X{ROpwo|SycOdJQ_pV7jN0$MM7?<&RGn;RrKkc`SAfDa- zG4+*Ebu3N0fddH=fUa?16Bfz28vQ^N=?13vZeM9|U6$7TUYF^rwP(Mrd1<83KQ&}-w|qGvN5 zt=33-x>*KK8JC^bNcuim#P?$utq4NL72mF3n# z($RvtioT>lY;6ir?_0#nV%pc1?2BZ7CN+q5YzYfPRH-Yk7U@C_Ml8g`v z4Y7215G?hzb2x}(VIBoZsEwe71LI5sQmFq)j!ScQ^SVfG08eZ5*;Y}>xu%Nj9qV?i zi>jfn^t=5bYU)=skfeaf2nS}kzNYlM^PJhUk{D+?h+Lwpj?1K)YO;4uiP_p^h^viI zg)k}37;w&E*Ia%oCC^FT!V31yOCvluA!B4ELv>?howPh|5nfDKEFM3nu5DTssMvNH za1y*=$aT>@vlLz=mHmg?_~N%n8~2$*U_}Qeh!edWccG=&s5=!iP5kd&JavlJZmE7d z<)KXG^?!yIhVX1hsVQ>WasLSYWiiQj+*8#j7&7xjs(ZJWZHWkth-*?)^!LK^!dQiKTRa43;8P*|%<#Ye)DBqa)D! zFq{ojfYjj&Nx+{a`WbYn5?bbwSuvUst_OaoK~!2LC(9hoACqzGRfQoCq|hBRa~cJB zSI1G+hyshLEt|Qa2Ywxb*6Pk?Ucw{>J59#ba3|0QDW026Cb9tz%*VAw0iwV*CtCmv z=$SiTUmT=}t|jnixn3oQww4HF&NcC25(#icq+b0U&_JI)!T>Z{hC%`f!S<`@OB12> z(H#E6Ja>9-A_a>J$b^un=&sa*ZImo~46uTkz7t*CrC=NHR-ZzN0b}JS;grZ?c{1ye z{vNj$LEs2t*{G8L1JE}88Kne#Ke#wc0iNDN?eT?!g-TN9O<{W}w?EOZD`&vUh#S;_ zQ&~H3mI=RNEd=$&p9saf4X|Zfq~wD z?l}zX^dhA?C|!2Ojsow%W3+Cr_XYz*a6gVf07??-Yi#w|=OPMLBk(93?ta(;jk7C+ zP2(*SZy{*}C@0(P)!Bkzw^r#78UbL)spyMzxcX?9keG}mniT)j6_AQ~b^~;)RdkHq zr6e&xDv_O;tseou4xP@Jz*WqqZ15~}3xHo(Xl9+xXa^jxJes-yBkaCk&C=N^2Ub?9 zRyL;vrr&7b=^3CLbCyj9O6`L$yqSkR-v23wy*qOSvP%{dCSzNj08Z8wy>%=wPvc1f z=mA1QI~g4`VLf}G?>k?|Sqm~~L7_<4D?0&GVDywP$Xxk9aPo8Ch%P(;d)xTLx&YA* zi!KHlHt+M|M1P+&6DPohdUCTx1UMh(@+xQUS?-7&C|mCiFA$(T9yv zhF_pt6NR3YbgqZ+5U>ht_?HminZbCnF{cBtLd$snLd$y*5khP=CENNHcOk|4zQS{L z^ncTZk?6i38Sguv!GuI%?5&2MZbM-`0$B+qP>ws8HB7$=@`ZY4vE-kQv;pYl1Kxeq z>_;I5l8Xoqc|dKlc9yaz%nndnU~x2+2ZtrlSTl$YJH#t;HhHo3KRkqY3{{yzBd4O|8m+G5#&PvR93FjA_2Hp{zQ4eMWh?=k%qry>SzuS znCC2vmsH?U6nH+vNu;-M({c`Ak3@8s3-?zL25`mm? zHPH(Q#>Ftn97#=r1@*SqNjZ4HA@e-$(M_|`0wLq#I$YudaCVPBk6v%mfPDXlsUTFW z=e`(V0?z6z$$ul8;1~qfN)~?tp1!#82AXGa0tRMUv#S{Z0}d*!bqIj}yBkS4Ku}HR z5qsLb;9vF@9`UD-?f`no{RUy0#uT7Sw58dR05!R!b{9~|Tke4lRH_I! zwU+=GQ$U<$%8L2}ilwo$OQr=5&@s&@F9Oi%&5~9p{gaz(9sj3%SQpYYq65U7h})EX zV3i0&#JP))u?~u8Hf#jfSmR3uzzq6{z7_(E#*(%r6K`G~$T@Xn_zMkSJ@;!(_5l^y z7-(*2MyQ4}j~V2H!F9L$!ODy<1NmK76hD&%0F)G~k6s4`17>v(vyuP{gb#XOi;5G0 zkiRrp`g{a(X5#Y3_Z|p@Wqj8v6!71yZNt*pDhi8RBaaXn_kr@zk~L2m=7jrOY#p?E~2tc=KFZ{^zJwhondu!E6rXRN-jNs3<1% zU)WABRx7|#Q4qZIHQ%wVqul;LpKB;XRfM$y05IeB=oh&?a`vG6UvCS!6tV&L|nWy{?L0ME_4Gc2EI0lY11sZCk*D1j~d zexGtc2dtP!`6#*#r9{4Xzdj<%A-%4D(3Kg~*aJ>6ls~7*`9p6Y&IfGvWTcrKTVT%X!alO7# zzXzc7r{_8+0faPfgo+S238YZoufbr$YHs@FvzI(T_Kz@EYSM)bh0!kGVa9gYJbwZl z#+?LI0b7N-lyFX<&n(O{E?WFczprmhh8R$#|GM10`GfkYKRi;lwRNLWC3yeUU2kJq z1w>Ch?_me()u7JgR3&c;7!LJK6aW(fn=nbUcSBa8LOSb^T%IQx2w*vdD>JNeV=NH& z9Fm%c0o<#5;E@pb^&1tk!KQt=+!x#d^>+2LSI`)9wD{sZWmfRnTML7y$XRM5H!u{} zg<~p5|8nCAOJ{O}js}b}dC+e)ub5FF^Uf`yvzr&791z$+1X!f_0K29yd=hN$fbnv_ zcOV0s>Sp^nYLUkSs6#bhsYJYA0%v@5oMW39lyP|aZWl}L@|z$4^L0Rq5is9I!|7B& zdpUp1SUYo_fBT4?Q`G8zn@sQQS`EkfV9*-pPvbsS!UO!(Dc@88s|0=t@jI~7@^pUX zK?GkzQY=)k1hQuzqw@r=B-F7^``OtCpjQuqd(pK`r#>{W`Y#h@>mDQF+rh2zyJy8Y z%D=MMEK?m0u1fEA;yxw(t20JcGa5iqUq-ZO#sb7j3kEd327(+0rhOqrqJ z%ej1(7GIT4L-8%3onMFj;sNZbdFtU#KSTU$g{Pf42e1gBCLsW+HL#l=4OG+fRCF2X zgji7`Ogo6|{N-X0Api6zl~PW80C|-twfg@dcN6Ea@CAZ>8_r0>fCcJ)ciH8s%yi9A zek@IkFa1T>_&>kd3rIVaqOK7AsuGoaTfJ4_9Y|-C4Ra@rAgI31jBDk)V1nT1e)^(8 z&fW*?g~l>?fPI9GG&d%&J?M6Ab_Ff$x^hrUWtgd2w)mGA9+Jf%0D5tAIn&rSNrX^Z zH?(VoL)0&jnKD#cI0N^Ge@*kjyG`9|H3djJG(5eKn`PX85SH!~K$6ybE}C?K^yg~2 z9PX8()>5!ofR4^s#bARSJ0KvI zBb^0g$ah>_nuTuUUoXk)^o9aAbkf{D*+otu2$?Dmm>p?>Y|yWY>~Z~cX-S~z#5)T| z0HeEZ7y}pZZ#EeW;TI{#n1lP;jA1%3k|E8N=CBiheCLp+=_JZ^~#TWE5#QVev z28ew&UFYdvYdmE$l>ZmPG2IC(5W?{%@9t|@cwlNpW^@+(%iuHGO4{6VA+Y<%E5jiG z<7w=C^)&j|#p&BsOdh%D)zL1YG3^dVJ$=A)U{|PaTxM12jHQs z+SUpN2HcVJDyavEAbnew67)0`CL@#8FJu4?6m7W9ztrQo;WUHeJp60C8c`1#PGZb; ztpQR(%eHqx%f8r!id3VudwUf%Ezgh#(3{!UKY9Pu4-RQsln*OZISJ>%b&3$8Q1`=a z@e`9mc|2y!s?b%m#Jdi^Ew(Aun>eTpIiuYq#HM;3K6(x5nha+o`TX%$w@wtvM-7x> zP5&%dqC-QH>c!3R#eO#g5vsnWWjt^Pijgf$lT z;S0uYm+Y0)kXba(k6OmNgRrZP$lalU&+v|CN1j=6Q@Cym2KuKr*UhLbAR|h&qr5(k zPD)~qr!Awrr7kXZVvhJAD%FjSy?|3*Yxk6gzj(}S96qJh2M1Txh;=V&WVvzii41G( zVV&9Kz*y=ma@W&7jeE^ZI3;rTd1zi0JE3)X_Xx3XpJwv+bvR6z7*bg{d8!UcWVH%e z9XnI$iQ8ND^_)8Wtn7QoA2=W9X$pfIqg6^&KC1-o&SBtdxpn=WIbsMcMRk65*`lGKnvzT8*A(J*S{2;H?ADE%XaD`PSc|K`q_j_qmP-vI$xOa)%7-M>w~{8 zYC;jU3Vh-Gz;h6Jg!*WgQl1z|irMHGEnL;+9o78(6BLW`x7P!)iAYCdcoxkpDU$dg zDy6UQCy>pREc`nMf?^Q&8M!|&bvs|I+4d2mL|srkYYkediR3fAdKyn+;b6p>MgBZh z+N?u}kYF8PsC27e4i+WF-kNXoRyPC`e6q6G7i&UA44=m>Ch*v4iL-=d2kKvV#X=)B z!?^r6jz%Nxhck? zeSKkW3g|u0Fl>@Q&A*BSRieZV+40|~4(-e zeC=qFT^E0?(f$U5lxC{T978i_$^GAj>rYCr;7^GCRYc7AF~LDGQyX~;5+Vaz*f}oz zc=+L+qDDN=s(#%2=L%LF#9I$j3#OKl_ zZ4b14SBD9+n@o@<`FpPH;|Xlu+&+z2Q&v^>HB+G8?UL|nu+(!_%u*vuq*Qw8X7h^Y#-euP%xk+NYtx~M=8Ju%H+>HH$B*AH0LuN<2rSHbKz zY7nzmv8->bO>j5+OfdV`%6dEC6LO@yg>@5qg$X>CnM|Nph6l)dX5ykx6W+1 z*oJp)MlqUHO*{1$316D$HXdTCkp zYeq2Cr#7je9A$aYMh?=pVCirpMw0YV`Z8y0B43}3xHh)uu-)7bKOt9CGCbN-$(ni? z>O-b|AJT7&!Y_T4UQN<(|L?CVna-UhuYGdLgO_QRmMl}FxZym*#cKA?W5v-8UCl4e z()E@NrW^OP8)Az$@b0XYVlo5hg%vE0TUtIoz5+Cu(oiUQDyEDk_r1$*t#ogpf7V5r z!*iy}t`GPP`;`)Z8HG<=O&3>|`|X{!MyFvioZ*%tY?TmdRI+NTp@ZqFAkZkYf9*P*l!W%@}gr0~~sA1t$?%it*c zOboyCxFXIwR`EsRw}3noQhDwbzjUZx=|ieV6By!jpwPhE`~HF8BO6PZcGlh(I>-%@6)vcGkwtS{@7y%ylRpw6 z=14Gz!>IN5yyqryrMioy5zDEU%)|B?P`%=-Ne5q`+?$8SvwjHa6)AnNK!IVH@?=Pa zckKi5NFPo`!+@Z)M($pbuA}3!xFzZ5(baKFBndlyXpimgbvH*IFUvl2kKyh}SVL}5 z7>@tM{n%+~v}f~YQ*`Y2rx{Wt9pdu|Hx$<`kU^u>;+f3oX<{V4c$WDx&Pi_Z@28}y zG@dp`0>C_^+f~OpDtHkaM|X+H36R(! z*@n+S&wL>qZ3UP`d098MJO@k1)0IY;aw{YVxJr6PT5RRdX{8H@7> z$J=FaI3WGcC$nP%l%%?drpsp;4;j7fKeBQJv{Xyh@K%w z@>bl^stWm@Ix$q3;psu11s+Ysuj?kVZQ@(Guu)oV!%;B>`>Q!M5$t~F2^okvu1m(C zjvgITH@dRRCHFgzkMg@)ThMhUNs+tUT{_kP z63=Y?5`OVLbqy4~6W42A7s#Rn{&)$q`;)XiD9Qnb$KpUB@rkXwvUg;tL%8Cg?CrGM zIOQLakR!I)B}(YIJvrFk5F-fTh}Y_bGM~X>Mv%ND2pat1_j`)`#|$fR z(iCnl_bM#FoZ@Ug;Qk}TADZpP#EvcAUn|EZt^o&U2`VL_`vOVc-&2o=@*-&E-0S9n zl}s-#WtOwjy~E|OPrS&z`@_qj&kbC5o`i1>x>O+2_CeB0S~uCJmgwInK?|_1VO9DE zmKUh*kXMK!090*})N3r+_tfp7yadm(zdy@?Tr{jTOs>5;;Bq>}4LfpQJEXq%JC6+t zX;P`{gA)IwE#f+|^RGrg>Cj3wXVYwNB`voO^`~gG&1$vUf=bVU_xk;JZW){HNl}yP zNJnSz>_Vm}R!F4tKdxd3k*njs!%kdz?~>q~IUCx@_dRt1D6nrNj_I>Vh_62=nk`Q{ulOxkKAW3K*t6mi^J4~SD%P%L- zI|G8dX>9E$jA3!Ym>1Mtn*VAuo^4`*CL|VX0LV{T+Z+Ch(BkbRs7BU({~pEsJ+&JY zdPk@S@)l6Y3sN2!ntwive$xK&U?V6G6h6u0#=Igtat4qmM4aF#Ct$7S2F>rQ>_Clb zZ(sIq4#7G}J2V^t-yurm2TLs0qhz|=bH$ydTch#tVf@_c{nsn` zxmm-dil15^`HwE}(?V*j=Nj+>2%J5ZU#7Q9JlikksP?i&>x_af=j3`{9~RhY5}S2W z`5R%EObV}6OuJy&d>$!+;7Ej(sr@uarYvr-!vrcB36WeA?qxa9yu6~im#SUfu89`X z_KM>|8@c+Hr;%O@-_vGa@C#6;N40%%K)Xc#K=j-9W~(T!;EQ11uQo&+`RL7$LCnk6 zrv?san3Rbs zDMiJ`Xp*YckE1~mLy{#w(tLB=v8S#5^|s)CGP-L&4p)Zo&M3K=LdNcCgt=cw=2OCs zzX|HRS)&vUGQ3DxgJ`=PiBg0zHA2rLd`>*xCzDTPY7Cnypd%ToCvkjqvKcad6HLV;3f@DBxRUS*Ec`nYQP7ruxi~ z;Jl#J+(quQZx=>+!%7^`(yp5q=t3&CsmOR)gNeB@cM}-ApWBXqJ^b%<=BF>DZK&kW z%Jzi@8Cnak=Ma4)zy$8Sv053(z=FrAE{#sF*iWb**V3*F17JcX%kFzsl((ed!yl2f z6)ThS`QNP6%0C3k6GYPM=`8K5womNGI8YZHu5oNv>tBrBOGVE2NIpVo{EiZXLW7_P z_A36-y^5jk|LBwZgK$kpL|_B8(nC^Gu&X}QH{Yyl7MhZ3hyxv20qqan7?_P!4q z>-L(6wyhZkLwSjodMXNHM}+!rTE-<)fTfX;6+DmE&I!q&A6AJV!<$VlPxC>x*+m2{ z%GCPlK$_gaFqAxU)c9W&V{I{rbM4?U)`~z96d>#~)MdS~ z&Zb1f`rIg&KLsNS`i_9ayMv1dzr*v#d8pWxR$JB|&Eor)Q$`cVxGE%5zY!dXqL$Bv zgC}Aw8=xYlvq&@IXMe$_OcXZ@O7I2qX=IhwQfE-<_*kX?k?^C6gBEb7g524~3dX6f-n1cQh#kTjm%a z&z&efq42Nrv9;?;?ongpq(~ayGbQO*rrNm|rXl}$;hR`FzY5Mdrsxsnt{6x%ZZMyw zH?)VzFzz1B=H7g-wJa;{f!15ATBbZM zNd38C)s*j}I0tuZAdU|T!AX?V^nM6K*&>9^g2p1`X?_OYdQn!hhs5kZSYAy4a8P*-0VIXt%my$*K=F;<(J@M@LOMAZe0v{V(_1imsfxL z+DhmBw|VEkr2-I&?2$zGKP9krU}R)#{9QsE+}*O=8@&ahLna>@^d3}YYt%=_K_5#_ zBT_)M`-Y?I_EYFvj&H`%AG0eXfz1~-)9e(P!o*)NXz|`Chq$6Dge%on;mz-RL!ivv z>GiT_HO*oPOO4@Y8sBezgpId7-$uUn3Vf~!LzhU2o&9$31S1xk{NJV8Sj5mi$!Edn z=%KGsT`_u4u+*F$JYEDu{MUxiC>oD?#9jnFFp3Q0#sJ}P?!J3@?FB_5u>l`ZGY=+8=h${PT-vZ%V+NeXE4*!1H*@(FnxCuxT=aCi}dnSXY2!k6~J z2($}adjN9>o5K+xOb8~ zV3n}*6saZKp9&H~p%sG^q+9Oe_gQo2)9dQ{p&I%yP1f>UE`plZjJ`0g*tO_TDSx*8 zi2O|ig*)X72j}H4ME}wt#J?Eq?bh3t&t!fwHbzCdk~G4HcmdSr73kMor|hvo{%e}= zap4w{F7076s@Lz>Aw%HCp`FwFcWLe$8@uy-L;NfkVTLB*@7Li}Zp=MD=`;9(4nyoO zBv5)lbokrb^3}i=`rJXfLiFZ1RvX~M52f5xppVL~en&e4o%s`34hq(0ih)i=zxQZa z>j#e4-D^_fKsXJ6&este7DWRm2NhW>|=kcH`JCEvN3t2%|A-(x3SN^oUOJ|ej z%$klGVy0z)hhDCR7jhr{GO$at?yWtUKV3v;)de{|4^5!e$)j)!dC!@p=U6li1A^xJ z#FR$_U`$2mIYGV!A#9xJ$PRO(?dC~mqq2XbU9Ejr|IYYj*AF=B*P$#22b>7hymVd+ z0!dZqy)Gv%yMoRELMF?qJ0b!{80QvKb~o_gDWRa$TIiB56o_qlo(gV+?vN(S8-}v+ zg@DA2&DO=@C)@Mo@0t?c9NzbyfBm`_jeRGah-JE9G$!AJ~1AWi>p0J=)SAE_f ziErlo#0dW*@7>qScmNY?UCk~P- z6`Pee_-v}oMVvBIX^?f;P7*qA?8Y~3CUy~oaVXR;fd;2MbEdcpW%#5)MpK=&D8bm2 zuW(j#J z4{rfv*#%S{Pj@%AQUX1Sh4#nqnu`40a_5iPZy+L9O@g*ofDjE<)FH1h*VU*)S1qLP zE0jOOs^~X`iFG7BuQl8{#2YIF2#lZdNd+aQCNMQxyhoX0yzs)lNuA>Bo--`;P-^N4 zUMBh3+C<&Io14z5XIQA9)LON#=tX%g9d23mcUan)ACF)&C6ZJqG1YUW%h)bg$GLH+ zQR??TA$CS^dFB;l{Bb!(o%#9^ZzXd1n{L4;ll^@Yg3K#eXBlB}Obi`8sKd$NXnH64 z=r1$KpYXJ|tLml|K5#iXZ9w!7ki@wBM^}L&%r+2%R+>m06?i|Vs;G6#lfuDH&d0x# z@cd1}HfcX|q<6e}sK#RMm(jwWO!(WPb<kR?!sDnkGbkn-ejhswFI;v!GN&yl>X3jHSXkO-m&)W|Q3$I`iZ`+Ub1%o&}q ztY}woY>0;S{C>gt1ATivR=1Rs3Y3>S+@mHrAos#tc1+BY#n|^Nd6V)&1 zOkWeFiWSPy94beV{HLWx-rRrwdE`!|5l4Gz>RjO6Tv$+D9PQZV+ZwF1w-?1Paf!~1 z$hZw(oj*b5#O#01OHS_<{k?g(mGb)*lYJ0Abyq}Y1dj(gW(uKv58bxbHdaDQORu9c zx8S(wtIj9-Dn{ql01%sDe|Huvr}R^dlBz~4exVP*V{qb8we2VW zvq6+&o43X3OyNWgx@&&OIpjJX1s_w~C;YV(8fzjB_9SF_Bd-L`KXOlG_H+w$lRdFl zV4%L){e=>APyC9~iRZ&98-WDqw)tyd^xjJ7jq3h+>_o=og(r3U3kDwk>l>ygLI zhn-NrgHGU zF0ki<@mmtY*UU)}=Ae z7XM*42nNm133yjSApB*t?wchwYtjb2!Hk{Sb|h+aO2E@v;58gRR@YQu+fv+;-Y{Bh z!%_+rdqY{TbCzXSc}e_sdH+&Sif7l;k*0GJ%`Mf1V{yciNcB(^QlLafF!K0m?(VD4 zn^uNe_-08}kh zn~-i13fJ2hv@k69&I1t`<=k(RnQjhU5KMx{ZdHz~+RV#BnD=b6_ba?_ z>E;!yH4c^z3E~r>+UvjCZijY-Btlo)s#blCPW#?$Ho6idNMdHn5-|+svE|+TPzyH0 zb~xl!kK18yetm$RZ_QxhR+w;Cu)}L5(~5Tg9pHcT>+=Kl%oQG**2wM02+)_!1bZ4u zHQ%~QOt~mqFesfiN{TrtTXv7Xg(^(g-_v8fXRnka;rM7C98{=LfGG(Dmz4NU)mtip z?A3ArbwqhaxtT4ZLDp>jYpBv5w66=2>N%J+lxb^Ye zpBX7AH)zquH1f@>O+VNqQnq%&Lx^Iuzmk@l6B=Zz(`$Rzr6UmUs?&cQ8MaUiO)b`3 z4UDqd4dzJ;!fCPa`oSe+6Uh`W;ppGkh_SYsmm^PVrs?_ey}omBR9`60e(kcF`qD-* zidu@IKu*tKY?_(g{6AFu<&$aS?k*A;S4zcXwJWDD&0Y+s_~ATX=6G)2%co5QhCGiZ z@XA+88=5e05b<9bO-x_+ZYs;T4o&bZUa?YR{Y->Kd`P44$mT;Qmn&M|5FQ|yib|_b zJztOJ*M5t_`5pMd<@tcqefu*xPs^5kdo49D$mvD#x5AQF8CSc(oLyxoMc`8Tk|G&! zX{bVDkIFTZa7%tNLaG!HdPa~YFz5~7e%+2{=*WOYJiY%`YE!KWTytRI;xz$ZOZ1*Q z`~+AKc@dij5J;P3r%Cl}eZ=xfo^MB)0ArDKclSUBOPP*c4~792%o!320P%VgTbv=V zP}-8-nH?C_g#VuSAk#(E!kanERvjL>{3kN+ZUA2g|`lGIxT>vms4f&JBa$!W8uGb@{_okc#e| zQ^g1^9}0th>}DuJ)o}=Wq^@b-DC0z7vr;+#)CC>9DDMycS-{sQBH5^ada3GEI%J3} zRaTO5M4(>0={Td3ff%fuD=lijXA>S)=E|k~*K!1Odt~D(@Yh~4v_)Qg?0Osf!Q!NG zWxX!NI-ZX=|4SECsoSe@!b9(_D(ZHA=x09-n~q0vBnx6_;v^v=4sXbw>x02?$vz8u zveYVi1=&2V8$YtL4oR+L3xdzdC9Al}3xh z!Yz1+DBO*RncGpY$!0z?Ku{T?Iq=%cJlz-CR_34>D0c>u^UqBlEy%IZk_v~Bp)}5= z5;Me#NU;h9T}ZkJv8t<@oV5;1L#e`uslL|Vqm1Enve9aZu#lC(&cX?MAXmVm>}gTk zosifsU3YdRtDB9HCjJRMq5Q~ywemFJuOFUA|Et&KuPVdLz5$(5kLLW_pEL-DU#4ND zl4%6vg^L|8be~bF`%x+je>FLW5k%8`^A9yXZ=2h{!CkaAzx}e5ts^PqpbUBkTf`Wx zsA4s%wk=92@vKmkg1G0b*-9S5gWPOJ5h>vbO|n}K7D{JGjVHpJ!jUs%&@!v~@5d+k zhDU`gG0Hzvu75wJ1qSi=`!$d~d&7>5qiySy7rlXKxREYWnMlv&;MbJ%BRbkA#qK-a z^?*_eg=1sF1f+Ds6($~D2&tZ*a(Ps#?vROrO(Zn!>j$t3aOqI0i@i-0=dCBBe40{g z28*a)RB8^c2w|79yK4g}7rRbrJs=0+mMsy!=^erM98?O`wN1Rka zqS^WMuV!kI73Q?<-`+w&xViSvZSTuVa0b=U{cFbd|QX;xOplu$O4JzrgtqgbcVgeb9>zQd~(&hSi5fZpPzz)aCa!A6XF>wAc)0S4<`s(|~DL z5PjPI!DD#%9`|M}_(~VIHteIvT1C_O=KGAHn!7QLF*2nXP1w3v#hjl4xgi$TR9EY3y*)Pus@H6(JYZQnKG` z6#BtwavF;%tTKm|F~@99iVyE=C(k_)v%bNed5>NiUa>u|itH)%9Z>Q2TXhBNw(n<3 z-7D7nh$nXXP51>Pi#vxP-@d0O-5x*u3TbS+F4R|IY<&;o@gF8ia@GAF#BQyh0Y`Rc zuzA+4*d`uMjr2n1%PPdE;eX!|yqS*X0al3$r4dLFqCJ@0<@R=vn_Vwu^5p6hFTgTs z-?JLvW?w&>3<@FFNa=P7b?#Gtn{HM&s|j)LqpLn3S@1--t+6rl@mb|$*A2j{z^T%4 z6-G%+y?ak@;-M2xMmqg*pwkZ?b~gCjvEUd%%goMv$&#>lwL2(qZZ&Dm03W9&#`c@^ zPL`jGRX07+T1Js6V+_Iqo6*aK5!xyURv;(CQLL2-Ar?-JrP*p8{O~PlIGD}gsF5|m z!>%_!nJcwh;5zs|oCarvcAFN5K`iyBpy3Wy*NvztI@avY-%tLzqMfGRqDiM=xHdVA zK8*3(@_Pv6b^ZrAaVo+@Cni*RzL0*@M#H5eZ^F=xJEA^5}%P zgdoX;o4N`SjB*F*X%z9hKPHY$lLjg#woFUDR^Fu`GZgk>+ z3R8$qdu01+`?3K(4^4XqgH$u=U9k>KqtV)zVxrYrXqn%Iyjgu;c%g+$c4VVZE9fg0 zrrL}}I{iWKXhjz;Au|r%r7XM3nLyrnLU(!<}{x z1>*#q!GP9%#dULWDlcVpEOC`~#FGluP!5{7L=S=c&nRWMOY#0JZ?!KD25&=ehw>T4&#!?FJ7W;+fxAO4 z7jALtP`>IUsxEnM!lvj+jc^zhW8`X%yc>VBiWNsamYOjFV}4c1Co*=&1E81&^~27yxHA>QoNQsicDu!}N`IMeoD zJyj!ue0yd(8y$Jvyui_6(5AdAZt7t;{uMj)z7vugg=y>DS^H+3P0ZG?9 z^Mr{cwF_I<%?ec3J~;w-N3EdRiflree4=o^jxDEEOZ5m83NdxQ%tkN&u|M}A@{(@W z*?Qbb4UCW+vvbiTo} zw8r!P>X7KfFf%j|+a1ATf1f7%q(qFKm%d%J_rVdP%5jsu=g>JOx7dVtQk^pC`S2e9 zqebhY&FdNh*{VVY#hnz!?%6H%TiRb9QDyd}G5%4d0CDT={Ok0SU0sYfCh`I|z8RM{ z*3&)(i>rmMC&d4}OuuFf-md)QJYxqbHJx>hO?mytobKZPc*<=SgHXU8bJA{Grdyp4 z5NSRh{1#VeYhEPqyXYu=6B!w6TI_mqrK?X4NEC!2P-$ag*gqTAm^17jijp|lvrFJ! zyKwyBsfLfz5U~`4mylWn92(8-!_c70L^1A`|Kxlm-9jhTB1Mf)?;<1{wmW){Ib6zj zq%Dc3T8;6e2c{d_)UGFy(@0|VJ?UijwaMAKp^-q^G#7^*jhEKRT3=JBFcZ{5G}_me zLq2lk4>&_?ixu`V#Glw>mk@dpS;D_8NGMWxl@CyE>XD?OT^Y7pgM<`cuurY6!@ex| z{w7m_Yhc?{usS)XwW@#NGkWnTaCB^#*tGhcEe&I)$sX*)Ds0$z^c&k`X{0M=Dv?xz zDGthb!BKwcwoWBM&r!=S*#dFC9g21_IR9WiWY|kYzb<2}0=j~yyw>e-=KTl0YCgf5 zM!oHp>qYeZLDz+EbW|KC;!+k2;OLL_P4GpI@di-F=u2|Mms zwVSk?jLwO(K$|`5-Fyw!S(Ezq`0bGk%kt>(4^=Trgl1p1-bb1j$g zpW^JlLkhEUPM;F9hI)@t=*NiA^0%@GH=#y|b1jF0ifS=8{yG^StrF!5JI!O$9(nV#I6TFA|Zer$3S+?yRF zovbU^#r1gha3gfEwNw05r%d@fxL%v;OgJy7iC16MavaznDJb{idwRyKujLiS!^qgJ z^0#t4qo2MW9}_+8-&4$IGT2ztNDcD0?rMFtwG`%h9cD9K3huB2S!f`z#!*@v6nlf( z(R%8?X^VP=#o3Dxm1zKZ`eI0DXF;M&oh73OH4B%tse2>Bay;*)kevD5a7GTYcrcpL zC?h0v%BE`}(u8KJG&<f`IVl(DXhJv&s!L|`{a)L*+Hbm>=o>=4OpE3=>Z`Rz%2kn!Hqisv^O6_@t1F5g zGs*BCMZqWxkrgt$CcDk-9d2PryL4~s-f*A8qeuf8*L+r6WFUlVeFBkorHkQBDfK9u zNRWGZ9CSK^A%izxqyic5+UT_9AcQY6t-RsI2{qEB3Pw^_8$~bZ-P9oLN&N`z{Yx0f z;P0doN;h{oDhd$7$HeeE$5|hr@z|R`s^*NZYHRrEd7HfU!haQYqV@gy(lsWGGOwMk z-ja!Da-UD9JHC2Y`I*AgD@Ltm`R8ESySbEK$aeSS6!!{_vEtmXtf=C0EECF>*TiA{ zVIy;52@jed(7eSus61cCENO8RCHCn#P8={s*wIkhdhf&s^%p&bgi;FT{CL%xGGiyH zbZ69qjSFC5wQrHCJJgy|Vkdd;?Q4mXWywgLwEYV@YplqK@;nl+4TzEvNJ*XaomWB( z7TJY_RHbiBFq6lKNu7uuQ4(sckcskWxZI|`(c%c3?YB9Xg&T9jz-qsZkMPu3L4qu> z5tT6|R}!qsNt@;|=Pjp3ea50v3TLq~vqEU++O7|ghVx^LFSfPzMil&^7``JWWca>4 z>Lx$7XP8@@UH{XE8Hcv$J`d)Qf(1+g=wdnS$uLgbem)5579W)25Ec-dLjW4{#;_w4 z%!Z_H39S`GCbeT%jum=eT&5({hx@6d+2WOs03dF0w7rlVzvQm|y{h_#vuh{N**M`k zF+RFM>3)=hIXQY;026`%RRO_YRfVS&aDS=}dMoU{igI|Zg4BT7vCCNIS_<6TUzsJY z4KcgIlSZXqsXsf-4Y@MR8^&!X^?5Da@6ggM*K=r%a-{QTsSR%!<7BFmC|@(jkxOnd zES0YBQx2iZxaKWGrk;XbHECxw?|G3wCQu^0F|c_CxAs^^F=vfB31Z51I5mBDsYxq# z$I{~ctwupRQ)C)I7;5QK$vLHB5NsY`<|{q>sP$`?+$41-Bv=ib$E0OUc9d2W4dFZ< z3mcu8q3HAD6w=!aE->d+XKw?SrkkB7=$+uP?vPt( z<*&?N25!$cPwX5_6t(x($^Thf+ZfOrTvXYhTfg5W$7sG<$PE7-A?Pb@MT;LZ8X*`u z;uT)U*}xRhFXvEq&~g*;Zb>OE%R&jB&>+OgvI%=p7CKRH&{?=Qu0rQtlUBi4>#Ok) z{u29zd)&6TP5`VDPN(o#L>S31jVN4;vE#kut}hz*B_+hSM>BfH52R~|LU1ihEVo>a z0++l*KYX`7IfarzG*Ep&r&{0FqH7q1fbg=@_%ubi>+8$+MRL)Q@sLz`)ezqI=jFYm z?Y%XklbmbT7Y}|2yYQ}*hpxiru5#^cxtKixUW$Ib?t@W z@g`;#mHKF`XgCX$=I_zVSbhD2JqQ0jWC!6L*>x(G#lp>8O zOu;JD>}NCr?VOhU;)dsgVhP@7Y~Lrrya*gd@Y{W$4=J zVux(_)Xct-{&;{n@sH8{Hp2AM;?waxwq$XWH+$ihQo(6Lw+AMNoxIkNs( zOTo7KaLV+eH`35;^rfqiBcAhcukj)O5oM6bg1iu`s_nvG_%-oL;#AG`ucujyHi^}L z&1B12_$HrdMm-K^4Yp7c>zavrS}!iW@!GziQBYCZk;`Ek*G2b**kmb2-MGcp?cY>r@y;C2zNnGA32ACf61@bDVgdw`NHxyj@H$o zesabI>Kh)-2nJz$=pcrP+|+8oezyX?-i441srr~fniP6ok|dh`E-rUKfUWXRUQ^TatL6^(`6A|g&n60LV{G3hnyiB`4sVpD7N=c zD_Y6_A5T{umc{q%B_%{sx}+Nf1SO@ryFn@G?oR3Ml8Q6VyCp>HR5q%0ZAbNXAmjCG@vXbwrLB!$_n>o`Yr19eHvcYK(*PWbP!m$fD znbUnLOsyxVuU2mQ$V+t1DXu&?fy)>`zYof+$saXM=+pv zQ+mkb-#<>vmFO|_4DfN)cK@{Y4uhhgh}Pk4^>RK{ts<<(!m%72lBapmt?+6LCeBtG z#|grzpzgoyVpbzd13Ff3X)&iKmu;`rn&$h~u%M>N+dqxT^dl`~<0k&%lHW|*4MUKN zrsplXNxJu*M%^i@aheK4(`Bip??j+0l|(e#W=$1EzLJ+=Zbu}rKv^lv9{CV6A3TuU z`L?}L_~gKYXpT?*zU0C&e4r&>WobBJXHJLYJC9i>k;VH5=0j$GX(E$c#pw~8!`|tE zy$N4k=EaFs=skHQ42?epy7z5cNg_xBD>n=gbeb%3h)6oM;)xFC($s9)!{YzMcCp{9xgg*EQdf%6(+x=Kwpz0mqR@*^|`lBcy7MnjA zg>UzRDt-Nn=*BOHbpZ~+4fYvXvTc5Y6x1}Fr1=B|mS0>-vK#`uQwcO(oWJ&4@ z8pO91@dfq~gaS{P%3SWEID4Tg{p&Fm4(vk@Em&Z#-aZ!n(cJ#y?1?Ka<82f>w%pHm z2_a+e&mc%?7S@!%a|pKT3X-?-7KsM-MYvVf4s^B{J%7DyAT;0FCQi64Mpcw?&Y4lq zMX?%soqXCq#F8q?=)+Iw)Vp0D3};Mq@%Bxk^lPKHqEUQ4XiPsh-MP=`C}O>)d$CNs zxkBK{bq{IC7fDCI^24hhJNR4lRVT_qvf*lEBk$2sY{yWvyKUidyG$%Ra%Q+SvD|gs zn+Y~XJgARp)Nq>8FAXx4SjWi+buZ^?8Kka%({8FquX8R=T@e^fRe(U;5=_Sa`y(6S zBkRx9dWt{O+UnWnQI7D?wk@Vw*-7`SOVjHRmO0ibLfP>swTTX49&C00PP%pA^N!M6 zrUTNc6By%x+kF#fzT_X!x{yt3ibQRFoA83CC(&x*;9;{bXyYSRuBKF8wQR`;8?!4kZ)#JN|ejuhG+6_&Gpan+N#g0i@$_Y@s|~bsb4M0R(znuk*tTXC%b9f z3rPMJGc`qEN!K6zxrK%Jxk@RAQst^#kXbZ*uLw8nPMa1)rSi{6G?pYuwveSqH*Kt4 zu>Et?gth4kf87tqtc9@XvC!*o$txea(1!1U*llQcp_gPshexNF(+4$<2&WdtX-M>` zL&@)r)KL+-sB-T&$sn~t)uXS>nmY2<1eTo}=pKKUhw$a_p6%OA9}`uYQ64rbEXbTV z^L*$1sr=oZ)qd<$g7g!Bo-rf9Rj64dZlq+;(K8}NTh5kKS@ z1)58Z*%PF#9Rut$;YJTzFADFA=pM^6p)pgmGmBGCT)U+f5`<6uDPR7H{qS5I*jd={ zWIhn|a3DND34`3FzNXYU3aq%kXM{~(-KlgXc!u3bSH=>sWuln8^Tc(E4`0;ifk=j) zkVqFaO!q4hd!+1=K7D&v{`ncRKM|eRHglijDTHr(C*YFgwK19~BWk?nIl2vJ|5U@l zyp#{F$MO!GIpi)SBQ%kt_Yvi1pX^wR=83NNxG)n6ox;b3a1FT|aN$*orD3*yY~B|! z5BHfGnXcmDt5o-aSP~q9**Db{BXeMN94J-MO zT*NBY=)D-Lj97y?x+w3sMSZBnqSwgJo^r!#Blf~X+)xWwc%7IpF8vipjIVtx7(RQY z9F*7H6p;Rc-{Bd^U8LtLx0-KD`E;1fHVKAFeADa%{t7nXp-Jp!Gy(}s6xs0xcUeV~ z_}f(~b`=*(#~;Gp7Z@Is0%5wAz~bx!#LK63Fh)kNU3vtLr~xJVsK?F6ucRP%$paA@ zO8r_o44y2Qs|*8q!pA5gJFO>VK86jSpm@&*A6JlP>vgq!zuzDeh+W5CzPdk?B?ZkH*|P@zGG92C{2yTRERo z8njq{KzT@~5bY}RJ?C@!B$y}YKV=go>aP-L`df-IhLmFsmm$kEU?x-yKh9T8jZ~30 zIL2A4UX?d z4IGa-gp0rY8#rIWyZUm({@JfM)+XBJGgzT+lpmJU#4-iNj|eu!hY#!=D?UFL)NYYn z{(>3>`E_`-?ZL=<(9rb@E}q}5(bNr&u}~JsN5Dq%njtSssPRoRP_Q|sqHVgi1}h}i z!JtaBuFg*UNXtOJy(24poF=k03srL9n-NYN;JM9TSouI>3{jb+`}w9!cXB0QF~NP4 zwjEgZO|5dR4)IF;E!h}uI$0^t+oNGL{u23swh~C~E)#D_Dp12Y8Ty}<&x73!l=EtE z6D(}5j&|VRj*&%bPf!%@yKzbsZ!AesNM5-dfaQrK@y^XiM=dV3sbBU9(H=ijjJSo& zQLKE@Azr?}A;oUAin9&`b=yrAug+rEht$H)Km@*bvWwRp;a2)LK(_FE&E3dF0O!1mq}lf8Ft6+983fzo8g- z75(c66JsU5bZM@yRRXkjFI4!&fQGHFi@L zHCD*B!{uwgR_=lsuOgPY|H=u}@RFPbN1e+N5KNbIj}w?=OB`?R{{EU2vO(cVx3l2M z=kEO*H#yBAo*9T|ALr2w*t+`@$zOljuLs(~uel_cU%@F*PW^Lw?2nbzc48yTIh*%r z24LsTaceJLxQeF*+$(5fz&Qk4Jj{7U{_Wm7)gr*8iC%NE;o0$ebM&!k=}VcqLt2Gs zTlT`S%JDZ~>yDD%O`ji`Ap7hXWAK588*E;JY;86wcLUKtCB!4UHvfi5_Un|e%cG!< zSs|0_G!}3b%|as5yUAD0MOG4pCTSVbRVs})hF14yiXG#U;-#=4h;Q^A4$R{#WDOV< zyHoD;6Nj9P!h`ZLNnIAEQ*V~qybbJ6+P>bO?Y0>%XG}G^M@9(_%$Q2T_g`v3$%4N5 zy-6%$?Ze+s7d*fpzAhQOEBL`td0+`s3s&+;L%q&XLv% zF)%QeabSedBgO;(k{ckQ_^y{RFgDV#;e{AysmrOTG*1-l_CIWWfbpkwj z?oI8F;@+a{%#OY@4}Aq??(W6#J3pL zoeDP*{AEc|ga>Q5Jl<$8zH>m5Q?DTio4v}LZD7=>A&_{Sj$oc=NVL!>LhXTjlVNxR zRr#d|#iRzZMwa@+_MB(KRXt>eblH0=uGC0;8O!zdkUA#R=-g7Np;G)l7URJ%ywjQ> zlQbi_hNky$&U=dTg?j(Q8%_*-t85j^$`sd7;K&Y>6JEWdiN zD;k6IJF^Eq+oQOK3YV*s?n;DxNqqJp4QYVgU=K~08;(f42KfGKs0#{DY&>d6(T6?0 z8`*y&bF-;($en@m?(63gz_o5gyFmC{FPvamQcUAFU4(i!q?w!ccT+~9;S$iSk9+h% ztO-Mf6veHEw)F7UGM8Tp4_Zo!?!JN1ajxbut&I% zHG26LJ8F4|T;kqi$HA$42`Pw4bzt2i+@M3P5D1J=Bl|aD zyCbgEtpMH()VTsNJ%CR~>G#@+QD}_{0mcIACPB4Op%_ zyOoYaHdoBHwCn5Fx30FTlU&!<2mb8|O0d~7YHDT?v183~U3bDL>6uL6rEF{0XXdfd z)=X^ys8k8vY6A~F{9$_S6iZAtQcY-m+s=hba%g{* z2``%?ku-ue0y%!oYlb?ac8oF8Usy`F%}+3B_*9ao8P5iJRO6Hf`}}OsYHrSmtJ6Dx ze>i7!>g`!%%KAuXZN))r+0}<>?b%9+XGtnzd#gvivsc3mnLw49aNa9&7_=SZOR6%# z#qRWdEjUj(KdN{dRxnDKuuzE_Mm>!zTG!4)v2u>mz8Ljs~_&l*mWwq^*`#TeS@R^1<*!h#P1d`89Gtz|?HtZ3wiiw}C3`oRbK#CWYVZW*ff>r5fWgWg=mY)N^e@GN4!P2 zHDDy8>b<E(d@3bf4Y-cr+%}&LAG)~(<3 zn^5BV-ipkYE_fc@wWJlvA@DEhI z%8I+0RP9ncrrypY4Mb*pixK>grMlFz#EKy{MBvFCfrCRz)Pj#RHL^u2C`Z4E+*q9D zTFK8T(dP^gXpZ!I3^^c}vCr?$-wUV7!{_S!C|@Uj}^u_lzTHlcd&UCVwnW6hr<0^+!iq%`;3t@+Mt7?8;yR6 zRGxQtYNKXcc7|S2)z+!b(Y6{i(PeW7bJJm7i6dSYGB0G$6@1kzPo1sw0N@UUD=Sp2 zC+8sm+UzqxD_PmA3cF(5iPIfSU(~a(0ZWHTF|YQ_oS4Z0nt%lJ3irZ=g%hC7i7~H| z_5W2dWpkgk5av_FGdEtY|I(uDBHGvgpiaB<{&4Sc}7ACRp)9cvK_t{u-jmj9zU;%j5XV2SU>3t4hA1m=-7Fj+=JpKagN zO%O7Kk6{z1SDs%?rtY}FM<)z^whi@`DN42Ts(p1DB5&T?oIi_MkX}*ot=Ea6Ep`Jg z3}PLpJC}$f%k+&1k)(d93Z2!DnOCP_>VFwrNe`&Pxl=D@!w=U%xnh~%U#z{#tc(hC zmEWj{%4oMAd&r0CCQWau-GOmUC1rw^H zk6Q<6qZ2sN)t36X=MhrHg_HlAmy_`Ogt!Mjc87z7kA5<)L=K?#r+QAr3by3Z2=p9T zb%b1oDG4nvT&2CTDSEBwSJdytNLBqCn-(=3vyYB)`3sxm9330rW23((EYBYt3~jA! zySC0EJpK9Xx;1ogkjl~9Suk5EjM0-y{*X9PrlM* z$7zaYZ*n#Uqm{z;rY6$8)x_Py)LFJAZ*mSz3AffDdtZ^M4(lq5u70*PB)ZD+nkiYV zR9CL7w_$38N>JtZMAt1=oXbAId&wH*x$t!PlTq>1b6+fXlxBy_W9NK&W2dwZYQoqywF zOBh|^8-j9T?;W+jF0Y~5>SBEVIQ<7R)tj6dGJVvx$1Ps$_fNg!3%fcgCnyOsw^N*d z2>VCji2~n@s(+K$uD+p*8eca_nEk}=2mpS&YMQQq(9sPF@h_`VS9Np%czi;CR~CF& zDg=A}!(}<8Polmb}Pc99>=ycc}XO zT(e!oT0SLs-{Ng#(40+*UHe!4lHr{-)8stvk0o|=tRW!mt=O&4v%1XMsyVUif6#(qIjXoa; zj?+~i`!5>Gsxxk%pOq{gSKKKlu<>)MmI-4}`#cee0@*Sv_vxU|)|MiNe@ST9fVKYH zZNrA3(!W~A;+01iKR7?BW6@?TgNc^DyT&G~V&?Jb_|UpsL<1)JeR1O&Z{_qU>YMPd z<{q`xNRGNe;bGiaCc$Nq5xP#z-(?N`!fv1>+!P!0*>#ssQ5~IjWSov)r-E3_JMmP%ep4@-*XULr6=C``@?=I{3E~xLA14HrnT!RY`-X%P*Wcyq_160IcDN4(}>Cd%QJb>GB#er^)qz6DfRt_#6dDYY00m5w@ z^rT?%e0v;&d8o9w{lUTN@^IZb3l_LI-E9H7hB;BkDjZ?8O@w(pl!V8vQtOd!=}5R!N$0T6#tIU)Qx4Bv1rnLdso$4mRFl#%fTm zMLOc(w^AqIMJsQ{@)E3@H#r0BQ)ml))jw__4 zFwiSb4vaLz)#%EqfVDrJ+)Ju~%#E7a$Pm$0C3E}4^ur$`foP{?=0V}>H1nib*sV(C z^uHip~nWRzKpRY=0MS-A{5 z+N*f82b=l_J-sV$+alu`)^TEgO{0HMUXnNQoQ_Rb`@nm$R40!YiEx6nFmpLn3zcDU z>`ksx2PB;KqtN#!u?&;ug7XlzMbT$W?S+%5EsF|>zuY?rcH1^-hI)Pa=yAsp!kSKd zSeM9(B#x8I{7^zO#~=_%ZNa+M+4=ho_BkAEPew=yEL2>qk~nc~k3{?&j%PwNmc|peAq0&BDIeerzQmw});vV;$S#dn{9Z^`XKb_) z=|VH*%;SRF09FHcArx6&7=NW`* zGfvB!j438@v^8pUzw^2nXlFxO%^^44osq(glP5BX&Y80D*Z!XAy3;EX=YGRYHfms{ z)NnMXIK=|1-l-gZ*Yh*|(-<1texv!Py;~@Hz3fZt%7T2 z^Rs-VfPz?)?*;G!Xsd$pj5y+pDvt=S>;`6PdCHerR-m=6?Y{Szc7?L@jiMCMmvb|( zK=Y}^rK!O|hx=0K@sK+m`khi3M|HJvl@BxC@j@SA;=N~{9KabH`DksiufERuphV&i7N~S zUyA1(u2E1zsZ#W4xV@p#I~K#tMaI$RzL6x}?V3TjK)#lQzKj=gGzx`^=SF;l7U6bW zG7N?4fM-ifRZ02q7Mil&!}Le=hqtPjw?c%@Lkh$-sJ1~Te12D6`L>~ z%Uqu;1FO^yU~89U{=HrKD&T-kxYdowky-N8lNiO8YH*N@BG4SZ4mkvL%^qiQ#tv6h1xk3f>BB77o zW~4=QoalG_wsjUNXJO(x8ltTTEaY;YaO$M4*{u_z%Hb3C<%7!NBKl;=WWNvVwH}|( zrydsedxT}PH%C24yS3Rn-)u3cXm-2;bj$y{l(K|_s-z=3v+QVUihV9MY zi;KTG*~*oA*!ib5<;T^*NL`<#Tp|x^l+3VucGGZ^(p9^AY~(Ashr!wNtE50K?$?<@dMG5si*w!#HnT6&C=Jm*Sh9J#mj%iE(44F&EP7XuIgBMg|CK)K za8$IJFwg&fX%54xkzSX_{WEd2ptI|L3P&UM|gv)F}0>am()!p=_m`R87&xA7=3~ z-iC}6U(Cll>#rxBRX2>wpi$!ZSlFkAdbd%@u|$6iWLd5qYo9v{Ze^j!;jr2Yw!%<} zMETVV4O&do`C4H_u@^{RR(HvTlbm7V(6X+8tep}*6~Wa8xbp)Sqkm^jog+etNPB3! zXC>{nsD>d$bvoJta}99SdXi!lZ{Vjh4NI;blX20Bjf= z%U#5-m5Uk@N~CYZUNJAI0F3qZK@X&VF)?a0Y92IEaoACV7c}vZ3R6LCuHnZ#aUgsl z>^pJvujM~@J~Jga4$alSS$*C0_1mQMJ(DL^lc>T|nLMuK6oqR}hj3alWbv?!v`b8z zpSV+eu8x}gD zr(7g)MHL1H@f@M#nKDQtYExxe78T07s)8Mzk#X`otog`_1Cg@ToS_KcbA+KTI5`QN zZp^>+SI~PceH(q}y1H@j$f{ah@R+4YBSl7B)sS%B*Dqn|D>Xf}`CbSgQeMbL!>uV_ zvQ0?9U(ENN$;ocI^pK{_>fLBVM~^t-Qn>)3#$0@n^3++5*UZI^6mN>WwV|YxX={9^ zQScO3UBjd#QH6kH8PN)+UYuC5Vj)#pY_R;^A=&sRv1X65;}`}p4C!bX9;xzt3A8TB zDu1XTWTYU*FU-#KA(}Hh1i#3a3Zf99&NWdPd~rw^+b*; z2X>(T4c{3RjLst}d7Tc5(_54oFFh5{WlV>+DDj?Cvtmbrqe6fh*`N%1Q9|F#0O;Y zRz@QN(IT&J&WzG81JFR@QoRqeL3I_y^cO~rQp4n9R5$r;?}?vZuTtQ6 zv(Jg=xl24SV>3paGX>dNY;abn4>AT3Cs~=}k~qzTQ`*#^!X(l0RUFh*lu03+-Y&RT zH{V4MCUC7RI7G|{{k-r zmR_(SPru#g;xD;iL7qN$C-qR>)oXGnEK;LWR8nLmR;N)xo}C`{gvcVlG8b)1jQZ?z zHhE!pL%{dX)1{bFG3BwfGrctbRVA`1a&2WIMdZ?k*Q@=~u93W@O4abB(_WW(gKykN zszbxdD7xHX8cM#Ln6?@3FjoG_8PP+a>O%-G zmTshh8oUNQXJ(w1HLM!&^RUu*f{XcFI^Po6QHMDaWZq}ydw@Aj;%E=6wpAuRGZ_A$ zTEO5-XIrg$$H+R2YrtVpubmzLcr<=4Q{Cgo#(?bjP^bM`*X76U^j0VdFmmiZO}4+<y)EG|3+28TVI4ewz;y zpej?K)I`&WH4j(=_bC3`srFKsnohoTd6Y02u7VeQ4qdIvdo5=1TJH!%p%^zBYpAty@6vnr>PFs*_&@eT zIQtfm8=O;(lM4};|K)h>msi042;;LYd_g?d4M(jkn?UH@3fKGXg}*&RsG{8X?YB~c z63gBbLU6WGv>u2ySB0II+)2-c&~B;(H_MI>s`l1Ps&6%5eugOVXbjuOdkcEm>!9%; zTQZD$)sG3wfW)e2zXbVz!B4cihnr2)0U^-Nku)9uB-sFQor#zq{?CflW&i)Ikomsu z?$VenZvg9#nVWt73+0Q|&u}O%JYJ?SFkmQ}DtrI4;zp}Y z0N7PWH}s#X-6{X8!|L~5rUqcRJDU4c%cfgEw0|Ym{}n~UF|SSOp#HO`1#Wi)vo(Nl z)s9Q&(iEZ|j8OGQSiOH(YgMcA-jkZwQt$5oI&*LwpQ>#ay+U{KC)c3=u8!ZJ&mn27 zCTA4i6=SDC$K5WsIKpHbTmp&}a3Nn?M;SsbJ) zPgDj(=DSfXI>{#zO-u|G0{&a6KZI>C4<`2mFFRH$P=VYJRK&5p(t?@yyuu`Gxc9P| zTK{=XY6wo)SqcvXB5s%#w^|~oaLEXFmdfYg**cPpqc$vXQQjL`IYsb#-@G;Ui}Kp2 zRO=sj;AV+*C>4eSP4^u!vm^oS(KJ^;-DG!%#2kX7>a_oZ0h$Y$-k|k@$T8~rSp`IN z!gR>x2Fk$ikB@seQ7)<=cL1n4;oqx_@AqfFHp^b>eyrmNCKM%(SJ!W zb{vfX`VvqpX8T!d3b0-$h4E>6RUF<>1Kx1}Z7m1)s-pC??TVqzd}X0)8;SDO3J zn)U7iCcom$A|QZ)DL6GGoTb48X*kclo*ExWuqMPUc-m`%VF0?e`uIHX0{tbpl%cD* zLSO)_Oh!3k9Y3N~d{NFq0~!K|(5G>U``;$ua4B0=PxxP$&qIf(!1N7WF=aK!07n_l zzr$^HJ_s-i`-jQ4rC(pNu*tb%Tbk(wWZ_W1uj)XvSQU)c>O-K-XK;H$*4>(v$w7k) z@&5MOl+yWw=}X~$s8_yziCgZ(pBMl4XH~3A!JH2IpwchoxVDOR7JH(Wy_MX&4%YBC_*HSjH7u+kj{!8$#K0}219^8b$tjf+rsBQ2nEL#RjaiZrkYML){Z}RgP7a#vtvaB`nRk&7#lVDtb&vUrm+m$kv}NLwZ+9i(`*Y^_5ChY< z!6kioZy8^foNwgrgX0mtTB8wJr)Df_8%$#zXAWppz#Z=YXS&#tMFosM%9QPb1sJ-j z8DusvcLg>lvTp&S-?hQmrflxKAU{6&*v*LQr zj*|(f59YNxViBBqED%|~@Q4R=vSNu+227ZhCQh1#RvuVWS~Nl~px&EURdOuAW*}I_ z+%(F9;G{&)Mz!4JTG|IRUaLJG<*?@{5Nq!LdkdQ2p515H^rZS+Gh-g-Vd6lZ@jMV~ zQ)m9Yc)~tr0S<|f`S!L|pyq;`W|zhmw325)Dt$Unyk{J~>k~vvl&vhf8~Pn4v+r*} zW1+$yw!z`oD(x4|G9E8z!yML9?rZRJUcRsnILuky@g*q(iaEU4t(Fs8z(T3H=V8-sC?!g#VKcrDq`m$eTMZ0|J*)8^*`p>`y8Fgedv zhobHI9IZ&$t9>~VqhjuCxpSY}b^?rh9rasy@YGUkzKTEvJKeg=(3<+@Tkv|B?Wpyk z%1f&YbrMD?1GYF48_Q93%WzlF;AmQ_HbCq5aoz1l`+79_d;N zHCVTFcvE9Ftk&rc6wzy1);NU2@e7@?@!P0}fuSisH(xzq)-sWDb9A&5p>tgwY=<#pVzb3Cv`7Z0pY#*?hdC z(--P8xVPbI=6RYJ^jSC$u`aC$0O`$Q2dh$>K#)GK#!kBJQvw~{rD6K^ zJy;5wCKVoOFBX4i_m9BVu5oGm>34)wiXDU=fT>vKcCURzBu_K zu!-m;(!M_I)-59VV6lc1y4tp5GG6#)zN?!wu!PMBX(TvWsYDrEwm z{s{ve8DlN#b$hv}d%!I7LT%(NXdZ(hIm}t@*Qf^b4LRMfeE5=izqbT|FPZ=A>gfD3 z28!mozMSyydz*p(w6y)`_PJ3+tjbr*Ou$jnn}G92+X%<4E$E7?#eq1Iet4Zzx{ZHu z=7*;Qn>!^~Ec*AuVrHF0KX+#obb18#YYCDPnT1$+H5imJb`r4m*(n%^fnwT}?zt%SU)Y&2=h$BqgzA!zc?6}*B767q_&(fb|el%3HS z=&>7TM>v4T!HD8E9k!jis%VycVj}_JJBSkU{Ib!X?9-Af$jD*H)!ge%3{<1#eWFYH zG=6f?iGAl2i#L{t^&+PxrRrZvofxSgGk_tVFWj5LjL%8UB-22ywSbk7*OrZzYQUQf zBrhbU3bGy5xz-b-K@eZBxSfi9EEB$ zM=04_DFv~!yr{nCn}9jlu_lF*+1ZM(WR_$TLM3S2Ae=Cj=woIOljhI97rNZiUt*}Y z3|v%Gd=@94-ofx%&lA4or}%>hPZf`kuKHTOl5-g6rzng8oYD@4P2rF+Q=v%>0zqcd z*rol~*S+DZ;-4M=>R2U5m)j-BN}HmwKGdsAJ2LE+=&zi&5+pmlN}f*5_N7a=`8V)KLnfw%X_9O*VVh4D;WL-}4~ryI*#I=BFv4uvCK(}G7x50Hlb`IkDn@cTyq z(3Wq<2|?E0{Tbx-&%gZ#>6D0Xkk4`6+i@`jmy7@)l>TF<`Y~|>$X#7-+HeFBcj29% zpQVvLOUrChNA+Y+7sR@pN132U@RzfeUTxrhixhwVk^aHw6;tnRgZh2qIuzysRl!fs zcFhBu+y`xRe?DrKQak}_r3$S8;;@cP_};Kj=VRbf8IV~C0!ml&#L_AR6K_$e37g+1|8H!Vl{fO#ANKn2ODN!>OlrM{}9z4@&UTDZW+mQSVul${47@vm5r)oo# z4t=+@#EAoc)e28`>a5<(Fs>>>jyhrBow@+kzD;18+f1|p7tThK3j6(Lb;kK%7x8Fw z;u>Q{lTH}_o3kNPtj9B6#`(6dkw)4@Ckb^#JO85i(wz-Db$A56j}ieAh^UWeIE)*% z`hbMvNrI==(TX#y^Ogau=dEKxhW0mrt#p#`#JY2+1+aynJ#Q@yT6qisHp@vu8|T3) z8^9)q^t^rjZvC3MkEI;$W{|fP_-TtT*EytD+x_|bS?Ldb)n>{B(s7K&mCkvrozoTo)~#k9K6!_1ej~bs#>Cu;d&Q3#WO#!H zkOmO^@kAo9_r0B+&B9;$)@_Xc2?J~xD1OX*uD>eP6%_}jToMMAa}W2`-@Y@!zSdw) z-Jht~Xf^+-ixtk)asj(RvB{6y)m4b%zwjmL53b~sGy{^HoQtl}g=QOkHiyXPUogDZ zo2oh9=rxNX)t30;yBiaIvgZLGGHHP)5?1lMobzjS8Z8%>-=UN{mI^_AvhD{SJdIYt;Qz9P!I534q))f~<|an$ zO1c1e5#3-A)T2Icbwq(4K+glTj%%-SRdDDg2Bc?I#2x9Nnoy4op*Qb*;K=FU#Qq6W zDhk$ltUFtUQWA7GvsSw$9MgA6tKZIt8-2S#7RLg&*7s-Dr1SLRrE*jyorGa-wDZ%) z?}J&?9|u$2*OXMcA7`^^eC&VS5;}TmGEGTOk3{8_%Hx9f^P;`wGmsC|Z|eJnD`38i z#C-3~)ubDh9FWAMnvNalM3;668^35JrHXurH2e0`8=v%@i*E%Ncb2fbmt&-QHu?ge zdr|0RfMcZ3SL;ak2E}xK%rW!aofod)Or6hOQlJqCfPxDW7N9kV5~n``Y>!W23MVG zac}#`ddJ&IgG8n7c$FnYU~2*~-i5_(R-^i4I6{sT*+opHXcDdQm zafDZAMyibyABHSP(2^5p_G158i5vaf-oR2MsHktlSKQL>`r{$2#%Mm36e==>g9JztC6^prc^rzpTU3}-F{x@-FqsOP)FB`Z_Puw)eSdi)n@t_dfdt++%1484O~je18VZF-saa$SMPF-Kh5W@) z%VhC#c;fk0mRv=ycxsJ_RdLq;eg-4apCxbj zl>lKVe!du*FCLlEPE2r0KT&ZZq`f{WX%YKZeSo{2g!48x!Ned+gplux;VZxm8>9MC zpx2l7-ikjQ$+*?ZBnx9a@4JuuUN%Hw(L6(mtU)}ae3UyokBZ6F~;*CUSw~&L3h}`CQ?m;HJ%x03XN|rm~@yY0sfIDln=T+0RxR+hP!RAf9ZPZu35UN=7zS3iD6XJFI@ zO__GzMDvB>7c#30hrf2Av|8`OWc%7)yuoC6%K+XhLbIU3Xa|fGOHX(*K_O11A)UZ>rR{0oS{+ip5^MRFIrCA?5lRv0@%PDhw`tYQ9 z$4n6T4oXJN9mmiUW*UmG-~L)`Hb9gx7eyJ4L45^+)emf4OS-5P*^9Du2*39n9Em55YEY$A_0z3lQpZ!|1oq%|+@N2~q!!biZ(c|2X zmpIo!3nGBX$HtM#jBfv0&r6j^xBHS}I8r0>%=%Tj z5*-RcIPmEF^#&pINFU5WDGBXs_a7ju``|qzh+6ucX^Z>mGSWRmF>?a5SNL&h^6HK` z{1-aelUr$|D);} z<0^gLer?)p+qP|Pb{jTh(`IhAZP&?mZMMx#n{E4l>i2nGJn!br{hhgd)jjuIGhjQ5 z0yiG*45DY;Aad-g`#5S^x>QK}aq^mHWkY*|9~d}@0<%{9s2 z|Hy2)=jtJD(Wo*a*qkP<$1ZFpYRO2bxS|%Y&IaS&=JG^Rylhl4Eh^YxRk^F8SS?-D zJW!1OIp!CQB!&s!h_y$j9W605X0m;*tek}Dwf~2OJr;c+nyF==IYC0|{}@+YMo$a( z&6UL8ZO&W9O&u!u3K!^OO3!sT<0h03l@%G2O74}7!dO9YfSeJ&4V=>E!=1#_>exHLN=mlpg~Ix2zErl zfiyL++X-Swp9NE%r5ZR=&j)UR{K}n+7Vet2UX%|WP8+uvw-N==l?!`_pr|EJUWSmk|B}#T_6ls_zv9$cLL4D?4k$RoN zr5xoSnSYOf3PPc;2#c;iEd$&LXOF*CADo-(=_9sx@MFBnKtwaec z6l&2FOsa7FJIf*o=pOTX47W*%%K9!K$h<-%ZMAPnsX~bsxTA39Ei|EHG{Tz<;BtE| zC{h)_D#!DvAa$}D_a300&nblGDX||73&zV|3+k;O9!)D8DjxgTD^2+GsmvN`sN73W z!>XHjiG=K;_x1aML`3(^h83@neY1wACD>aR1|epxstwFO;@v*}|_s2i*4`AA!vd!2i(PM-k!&7@7?U(#StHq@P*E41EahV$h`-@jvajd%tm$rII z*)Aa_+H>ca$^U{#Bh+K4s0)Wb_T@k%@$EQPvJ zmhg#V<7+vhUVtZ=4j!6{s?;8p|F=er@;m0{`Nqb_x*7Zj1vU3iumw4i zX(;KS)?`2H9?~Vlg5hY6#9wEqp#owOt1cl85W)K4F@+(v6uD8EDa_~tEuMM>b= z5pb5f*nSRGKxSsB#L_k}@cigSWY(RMw^r$%PFbfRhvKW&>Q;ONtNSGjkF24#_%+F| zKrGakE(<<+ock*XZGRedV4OGvRmRaPy8IzLL& z2kp|)|5ZwS4(K8UKeO{9FKiG(`Nc?hbkmGc3A!;zf8%lLA!(44Y$AnvVCYPJO0R=K zBOt`CGRt+Hm{$z{$+f~-R_N5#RJ%t=+tG$m^QYR)U(}yM%4lMLg{WFxZsvaC zba-oWe1}F*po68dZhcX$KI~{bvXXOcSoRG_1fQipvK~HJKRgcNR)!E+%YtN+D7RI7 zU@1WI;xu>d_nm}QLui2iPG8f{QL7II`&_w>joP#&wh-gT%c35ZLlmm}hvkwhkNefL zrMP6T+U=iBaT&VrZ<{Vp6i8^5fHbl__`e%{3POJ}cvyXg7b++vAqc%>1jK11HeK#; z$0kyvAjHLJ)#w53XT^L)b4F~nFD9E979d*ia5~Gl4&yRk(*~IWl2c|{~ z$uh|B-_?>RU}E`Ul2s?zI@0zYEL~`rYj8kw{YJQ#Vz)_7)Olg}M=8$Sryi#=_@(39 z95A<#+MRq`lJIRlLK}t*3HBnkqLt43NLi=eFMf28T^a>a9Afr+KS8yaMV3DDfmdvc zJV}`n87@f_JHdJVM?OG0>aiaZAbo&A8xE3+#XKi|jzGEt<{~@3H+1_c{c?FST(aBk z1z`&H8HR2Qa+pO1+kW*7DGn)&PP;ObPcPB_!DT6?o(`my$5PMuku;m`-z*M*RZ0|b z@Uc)$rEj&+(fZ-B)%V(kVdV_5Rw3oB1nw)J3UH=j>|U1%KbDZr1HA%_n=g5UOFsd# zEAXZaGGO%>$}r^6c2;8a%13q`>dsk|uuCM1gO4G8{h8SK`Nx2)bLF>~dbAXWP*OeJ zvGr%h?g*#AMA2p@Sw=c!Ej38_^rbP{=a$f+pUA^OL*rIuMgOJiv*YucJ5E#&ADvf4 zOiLHmn!#YnZs~h5^o0wfd)xZW?37fn8fD4#o96Y%Kik?Wl(*)}! zGSyAu;Mq%i#ssFtw0O1)2V)m*iv~Hss}|cj8`cQTm}q27i)i-bRT7k1zs`zI;q*2b z$gt-mDg3^$`WOE9pSttV;~7TfB;pH6i?UovfjxksU$C$y`9vT$m4*tDLWGr@LXa^x zwqBd3uj>yrIZ~kM_cxMbLsw36L_!L(KW`;cTC-m9Iz`hz(Rq9TuA_;`-p<}|&ZOqv z^rule|5u!)d652Qh9eJ|$wDS(Adgah7>F6Q7SW4>fd?&-fy*jGXxd#f7*cnrE+mYB z2Nl7EOBtAC+HKbtg6dWA2!?@28Nn5VvyEYouQi5>&X@s)%hw2Of7#kO_8!eV9zsDH zys$Wds3iR+l)r=ws3^Z+BzNvNkv9T4LI=AX$m&@u}(GKSgiM4FENoNXZ;s4nl&A zcuYjBG{WT>!>UqYhSAhd2}-lr8IdF6x&CnjDc(#za#{#&hRWIc?sTKNt+!;jocjMZ zj;9bsMVvTO>xNLtB1O7*eU-u_2}17ilBWp>-mTT8*PsT`YMDOW*-v*DTByh?s1m2+ z0+!wgm=OQVG1QK{h+XHro|AI72%VZqm6tmdX0&E(z8Pb_JpkYbBniyctBDVojq{OS z#J*wUdSqQ{!BYM{>c&?XFr&|KtXulKjg`$lN@1{}dMJXCF7#PYH9}Lk0hpqNhbhq$ zhXADlgSofdeLjKpnPIX~(G9{Fx}WGznTTKoDaT|%r%4XZ+X z%jzB9B*j3GKJz`DEbx10M4m76~4Z?>X%LKlGhzpwmTp0abYC<4W`3B+u_29 z__=CtF&fXG5H`7{Va>U+d-AFxKn7wI!HYL8HF*Ql$NVL3NmxAZ9z^UCH7F_gS;Fw1 zT5Z@cOXg7oprXj)54qIX{M-H(<8{tOqw7zp4L|J{$~~?G)1*Xfc;z|^_gT91tp;z? zWmw9Nv+;FeB*pgR5=Fe)WkO<&1K;KEY<2HaI8-enx$FihNhH+Sh;l44nFKyh>#heU zqV4HElA>Vhs&8QAH4dCVZ)I9UfH#bk#y6SC*^d3Uv{qf>A9p@9d&}N(T?1zm;K`dk zN!Ohj?S}i6d}I9(cEn-dXq`}G80ZlblW?9WGnbqdLj#g+2?(x;&D*SQ3&1lHkG|+YLrrLBp^YHCHM2c#th8V?@I(&Wj2OA|Dycx` zM=3AcJ=^$xaoV~S>5a`{@m>@1O&hl!v?MDY+YnOBNqRb8V^vo*YmvZ|FM$`!exJ1l zg;k7!d4CWMW~lP>Q++7v{MqB@S6(2mN`_uY^mNjz&ez$Mt@1$mPDWDA=`FvR{pVeH z+shIq0sK8b`nwhlDvIi6_S;|xG;C}R3?WExWqnu^i8!+B+uietLkVMA|U-CxgeZ^ z|7N)EWxXSSIG)9`?$has@~L=@-3a7Q9eWe@HS9eQpfqYZMP=0k3%-OvVH(LBY}RVR zA|K&Vp;!E-e)C><>EWxmN!GpFF<6Oom}%Ak8gI z;$ag`M0Yp!53a8E_8fTZY;`<$Se;z2#R=c%wo`rC`5T#8RccWY_!*)^C4|eO2m)B;fr;zmM$<#;7rc4({6Av56+3 zPl~uHiD1GjHVH=`@b0bsw_5Ckp1p-l$dMmRb%Y-77@sgG+U`pjtT` zZ@$n9lWPi7lA~D#jpWlou)PRnH&x!kB}zg-@Dp+t>$>NlF!!5}$HlIcgZJa+XU)>S z29M)7MG4oWLPK&DhZADIpqvE&bTova;ci2E#qtQge zz_^?1)ZthAhnh!lXs37-C+4^sIUdwv|5PPFznPB4v10*El++R;RTk!_hc%6$X+iMp zZ7G65>`*+q(Z293Uu>*rJ2KqQdh%BWe$Sh}InE@Jdlxo)iq!|mkgHLXpwTR7{Pq%) zyRn;C4Gp+a($6+}Y(Zu2T(~Cv`P=FT!kO3@}q6mSN^ z9l!Gs|JO=(j2o1HcJhl$SuHd3KG=;c_O-V~_dozICfDIQo3WAoz?kZp(Co04EJI4F z%zi0vP$V^}Y2gTWE1kZPegD`F>Vh){t_sDNEW~v}n7+|SL@IT5VRcaCFg$C#OY^%T zM+HB8t6&|oo1tkd4(la6t_sJP z?4V3gmVwa-5I^OkH@K8yuW!mT%XkEdt0F%pd%0haxtwBeVCt@MAco9QxSEoxCyqBX zIFJw=E6qZdIGh(-)Qx2{uP00UC+u6h0ursS!}QUrOl5<20rZ;enT{y7O!rK?g*Ip3vj-|X%I z`oQaNeD!qC@fo$?#AY9(a@zfJRE24w0rz%z!k)&$%5$oPru;1l;1R>#4T=?hlv~d` z?K)mqCX;Rst?+xIMKS2t@s4>yCq8Nvp%A($hKe&)!+k591f-^zi^a909kvjOWWbX* zi=1V}&XN1|vc7-R%m}-_ZArltjfw-}eHt{v*uX5Nb~FF>59>8uw(o&Y_rw0)X#)TL zJ+H3T{dV_6LB41oPHi`E_q-$dSN4}mbnxZ@PpoTynzp14xUg*Esd~%JvhlG<-Bcq| zRi!>ah|v*J%Zu?H+gtNN$98qTyDvLn>VeDI{fAx318M)hiruisN5kdv4frASQZlUS zFt^0xW8r(-XeVEG@pB3MuTJK%=En?$?;(OFCNIp_+_FV(qVNDm`x5mM`9so}F*GENWAEy2s~`?%ua>a$%O-HXe!R6(Hn4=M_eKNnYGjuZ%+%Ic0qe z49#v!0O0aHb1!ST|-tHkA ztPg^Ozs616y(JuI+fnBf;KsYqX2g+^jyn%#Te6JB(ZE00IBnWAfYOpYfw~*0eVeSm z;3ygui2*6UYE4aG02vi0mI9ICvqZ{d0CAWE|5MAhtwUHjjU~(2RFCfwe>Q*Mm3OuC z_>Xb{H(MhY=XXd90en=cuQ3u&eg5X-z8=`OtY?T)r{#`6gL*!cv!fF##8Cf6RJ-vo z5EK=auLbf%pseaw@1K<(4FhdWKufkUH*|dxSZ1uP57366lM1snMnP*h*x}m+R1-i9 ziIZE8=o>lg!33?{RdM_ip4wlEo^6HU-sK>bin}*_x^eI31DJ(K)WK@i?|oQ&4xtN- z@Cd*v8%MsPi^wYY6oZogpl77s!P4huAKCB$V|5}8zytHcohqCnrUEO+2R4~_3rJPJ zYVks9>IGuV*1N*w_4u>lwDO-l>)JQCYm{oLRHJk*c06KWbe&e@BdHG>+bEEZUFDLK zuFZTGPPMgus4Go^#8&H)*%sans*<##asipz@Fe~_FCzHZSlJo&^ecA`ZuQp{V?yUK ztX*!~idrLBoklf(4q<6FSZaF@3lG9HyzQ)IlQ53eTnROH^^-@n)yz%-zKtD-$VpN; zC!Ko!%el~gbp7qaN#m3hl19!o<%qH1E^S*pkqxk4HTN+-??T?N9YohYyb>Zp9=md} z7_e8E+;x2G-#oF)C-UqK62hQ>2fDH}kiN(77pKHRemhA!=g(X9V#Yzo3>$N_e(Mfu z>_Ovcu;3IDE_NkA$t`>DW#LtmfDI4nApiKB&@Y-@icoqj4+;{^*BA?T7$V`%?s$`< zn2?VHKfhGI`-cdB*}NLhAu1#Cmm@EN450QwuG59#35zEaByta0(9$VkF5r<^x5SfRh9rJ0vEmsvU9062|mPaL<5qMftt)6rEB z7heLo69`NhXKw_A?+KbmYs3&}m{ zs6j{yUvS@|NL(-8`|6wJrODZ3(yx#x<#Z@lcXM0^xpaaHbABK7^XvKiq1C{QiDQT1 z1#>)&C>zV?d%0U2_-eVUhdRlAC~Qu@>;;zgY3hoXrC#ml`s0o-hd=A;r}oO$TGjVs zf$deWbK!e-74hMoH}0k9zRmjQ_8RViFEgahP1^9O*e=tu&Yg=7-7pQq3rH|8%2x%4 zyMDfuMt-2(0@+lfNl~c@M+vNjduy=GfsMN*qgi^Qh@qg6coRBwTbzi@$2!JpJdeGwk|{6O@$h z3bsm*o0L8;`U9t&L>qhF(`_zdG-_A+iI3Zu)pi#Y27-r z=3g7Woym85jHYw0m6gOsTO1NeHdH&#*Y=&{1L`orbWg}mvA^>`F;7*j2p&MtXXaxa zJaPW5NlluNhI3Q_%lc6S7$t&iePM-(C zaJUSPEtv4a{hGzGO}Evp?FCspGkRe;w#SET#tE|1E&t|(_0R9zTEJ4buor~$>35F5 zQz!zM&ZfHLOS#Tsjd$k0oJ+Gy37mT6G1JuV#V6bfokfEjhxBv7*%2pv$ksu4LbQ5& z-C*B|P8Vzs+C?XxN29-yS*S}m2mLihG7-RQt6mZD!T0?YXO@*5J5c{Vro zGWbM0c?^CmLL=@U^C%naw#vIU!=84oHreY^@~!Unw8jGm1q54KG{p-b)>Q$Uv{}?d zWx&CA*WJs96Bs3En=lQ+M=Hqre~_N)qrHVMat8L`4Gdx$nz&OEJk1k0Kx8PvzM1-} zjtm#nBvW7?)nW%(!!%~ThmM)=*oZ%3X?rhW)lmfiS%FLT5G=j<4 zvGX5`(zr2GKh9G5x;zBQJMqJmsAdTdpI7bAtrVz&pA0M7Z#9BT0bpch)GQKEKmq(U zUi(CeBP`Tvc}hG`RFC-lb|A_nc=5_6IDl*YCekX|VB$m>R_ezYDqWC-pdW8v-4?3~ z;6C{m7qWQYDnyQvAqVmgeE!9{*4r6bxdbgf4l#1ZZ8(~F%HAncR9uF(okOxOHT*b6Gt~oH}5^&(+zz-P%3V`pTtJ`SmD@tt-wTJBk0|dSqq1*ksB;0Mz2qG(?FoKn2WD zRX`??NS+_Vm8d3ssuM?OxS5}G=h0OfnF1igCS%!s{SVj3k0>z=NDQF#9g3=gj~)E# zrM&V@rwPv>4nwh|tU%DVK`mqk&~{>$EI?Em6(>FrV@d zA?f`Z4m9Nwig;ylAh~U&N&`x^7@3Ao*LebyQA&g2kpldcmFZxU0qgqvJ2@1sKpCHc zTo4DKdGXMb3u`Y0z=4xVe8XwkqyV7kFV;-VEXv2o%mdwJnhGCG5Mm(f6|Q7wvOqOa z;*d)-iwM61Pq_r=LplJeeiT=-0eXS58UPW>WpOQ;0^*K)F;b-w{O-#;U%8 zw?NtClb*^y244XvN>_!Wb)GcG_%Y%=(*5DJtjDFCHxGQA+=!5z0yLNpC# zAKZfLB{H`fB)tMTjpl8&M4bua(m&(!;?LCHy&Tj$7cs$B`)?f#3v^P}und2b_%hBTkehf9()3 z!T^6le<)9xQiyuxHllii{W~zMW~rQ@!cngU=)RWBg?(_VjQC6U?Nu9DiX7cP?)`_% z_ju(Nk$)>@r@GbGDqQh#f{`e$^V7M0)797U86Ub+mablXjeq5pB8T{hJ7sDi3yt1A zHKIg6peijAWWZ!>>sB)%OVD}>b|mvvAr)(By?>DvC{uk13UoW0<9S31A)4u1jQ4!i zREZM29JYLm@ZlJl_RqAF2$gX1?KD7FGJyFbOiDor@M+0?btQ+e5=vj$?Vo|l^Vdj; zR&(+l-xdCh^mR~~eWT;9y(@yxu*?P}AMbdnZfWEFbRA5gVUJuyD~9udkC?te_f+wC z%Aqe%)}R1{vB_+7aW)W=*NW08eI}?5|3BbbBi&i#2mJ-Aff9#M5As+#cz1NZpCOIn z3RvmeObr*Mxdpn3SpE&HqPy)o7iYnY5)QdT13*9r6_NwpgJ;EMGEV`46BOX(JY(yf zpvZQ&J-n@9-HDL~m6uPbkg?bvycXz!C`K!o#rM1)sRMpauz>0cdZo;e$s7rl;|t*S#ZnGr>{Uv>p?2R}WJd!Net9e(o=a zf3%^*C5yg&rRx=oO+msS@x(6A-PKHf9;Q#p z3#~T>WUl-Umr<_-npmcjOPM)Zd*+cmv+u7f1&?m`ZEg`ztSumPy&dC6CN*8#Ggq;l z51StJ>P55@tJkreuIg{f4!Q!0n`&GrJO1aG(rv?hK3^zm`;C zUicPvtj27@l`(;*=$UqCgL|!u`cF@3Hw8lYbAO%IpF3Ujz=wsrmJNc;UuenMrNbIi z!N7@-SE{S3*5@+p`pU=39ShML%B{@+aN~>aCG^ z#&Ju*Pke)1Wzgtz+s7zoIHr;38{Jc;dEsFKuljrY_uIz^WYPI$s5Vd#FS0hDmoeie zUbV7n;J1{r9Jsv#^4WmkblcM>7pGs>#yw;vHJ(1@`5N&0-eQGHqltS7k|oL;dEAqW zQlO_dUW3hGwRDt9Urdi?Um#tC_P7bCbFOQiFk8S_Cf}q$`J){@M(|_YbbZjK_!(G9l4haMnLMcM(GD(9B-$9${UCBJ!84QxAWqvJwp_y__vxf}iBYBi;zbsQvcz3z4qnX!)d| z-_m^|f$$A#Xt@|c#gb$`r8o+o8~KYc#Iz+Gw(|a()?hbL&HaT?kg9XsCIUE$Y&eOr zY`TDDP%o2keXrQS&E83s1d?nyX4lf8h59oE_N-;+A>ap;XQ(IgIuBLX8Ha13WkB!d z*YNocF!VhAY{bVfgu`0x>nf^Iyg>~|&mvgScXX4~riZX7J&)4&&OEwjX>SB3trO0o zKJ@YXT};|eXO&h=cle^F?;K1ZvE|l()V6c?p($;@h;PPFrqw zJ2tlK=L%g9UlS<)b~*))i&lSWp)nE6BDvshH$R zgf_z)wHjK%y1w9X@RW{>Q_Qc)#Q1dpn%!rQ%qtH$65#o1^cNZ$e@re^N0;J_>dxwrDzLSUjH!2ZR+3+gr-D|G57PYV# z{BOMTtzG+eFTWSjh*|Nx=N!ejdB_4zxbWJaS$d|0MOl&89)H<~lFN z@2Zv9$!(-j56RtPVFXWtV5xOpNV-RW5I2uh1(IM@1{3-RLj!*gX|r?j@C#0U(I(sB zf3OWViBc8@1LP(Z%q%hL3d8xV%Wz=L;~ev&uV#C@V0Ps{4$W-C+rCWVYn~MI)T~<@ z0C_bn-WFg$as>{M47d8z`IXnD+B5iRmSLdxO5bM!#7oT$Us069y$A9lFF0!p9q$we zx0S~VCfTKF!|(J*ywu36HI+^JnCLxD!V&h@arcE^{t?EC>w=FME-(Bop}jfYM+q*9a?YANP*Gx(RF0e zaz*Jd_lu&rDMN`;IP!RdTOh5X`JR#wrnTxT#&eDzy&Kv;ljn@?#jpKId@vWa6)j92 zRGrv~Ce$NPl}7Q@Fj=gnK8^_U?N^r>N)QFlcBQUNo!DwZ&9B^oJlCm85Mq|KhA%pL zK$O);F#@jJc=4Co_U6C;V6%ZJwjv2+83EukbGhGfVOO8;NN+FitMLUD0WNy z!=LA8@FGFO^>vx^w{Be6B!s%GNjv+Mse*rQT%0wdm-&6Xam@#GYVVw z^ONne1)>$YPbvL*Enu$Flpu`Dcr14mK84Fi*c<6GlR!rr=WZN#`Rvjk%djLI=|{5d21)+_DHiHkaeDoE5AL(m@p{({Z}m)3@*M*|R_@*WYb3|6 zedj=Q@?%2(C8Hqr@EqS^RudyMUby2Sr-vjMm5c1K z3q^ol?vn}*0#ah>8DY)7J*H3bxv7TP5OKN854Kl0KOCT?SSFbv*DbJLRBI=y!Zj6lr8{y|3YE%KP}=8^ zD%8k2vC_!o$`CRCwAFi)nB1_w#U>CxK1WX-lSRkx2pW7Br+A01kg)YxN)|qh6?ei2 zUh$FCTzHW(5^LOmPL<5K$t~>s>mGx4ob6Ub-wbY)@Dg!??q{=#CHp?U;BUOq*YIfF zMT%uq&^Om_^pCHHkK!%5Fn!SS2h@_^Hzo<`Waed_Jcr@;!2s5}$>~6jP1Fdx+5*Lm zjIY5H@4X4}J33jl5Xt%xaUS8{$Ne$_Q^#Hf)9f+M(`H8?`TUqA)Xjd^5Tr`J2~uQG zW@CERePSEWph|ObIo}ay#{;5+h&lctM=;kO$4Lrj{E;jn{njTubFOv|+xQP9Ldo7~ zH(Fb>DcXui)*e_0JwoB>$1E>!v+n!rpn6{e_iMb2buTi7=BADxvy37C!ji$7NUbju z@WbTa)lX=Te_>+xpI~D!U*(STHQvTew20W5w(sF)noY4*M2aj;nGIc^z@G%`$o>wL z!;n1%=RfC>wSPemP}xx_L9QX3ybsqqa1p_*5dd4|@qZ*LdbnKTfeckP5;_fId{Ev= zm(N?F^m%lT-C{?ElD?d6nL4F_-Uo`cb&9M3a;z8k;1Aq5AY}o|(2YeN>};$Ky!{Hr zJ7#HeqiOt8Pxfux@wn%gbih5#&$Rx=pR&&!-^K${6wE>eWW$=(4}YQxbYoHOURkN? zuZf~~r?lO?CN6%GAAlSGqOIez=b3ciPu6iSyaa<5N6?K$N_%G=_|g%0p$+4h$*W3`V4NALBXFv8Ga>GP!~FzX&lu1S)T%%YI8w^-K{G{WhQ&`-y% z=eaelAA%tY4IXZm`^ND1gMR4Gyuyqn8lq4rnU=&qXUjnpx*yi~B&RywZCLnKq$!1eSYgwWkxK#RhkFBlpiWtHEPX>Xv~un;0=-g9U1e zQeRQCgw6nGPeU;F{Gp(-NpnB`Mt%?s^{(F{)g>NQokb!J4axR`1%s-yFbXrJ6P5xVH7{=Tu{q>PRq^!{~m6bMW0P8VT-ZD2P_ z>{5)5HgC#BCQ~JlLADS9Djim8Uk(T(qbq}^_Fw#ieK+7S@$skwODai0eFMe|aofQ_ z#n_M8*-@^dz+nag`-Ik70(b{wfyU@NTkUO`+a;;+!zD9H9yRL>JZi zf`W^BE_k6^Ry#+^LE+3SPEex{z0)xuGd?hj?%o$fRyu$41e|OrM0~QJo%$P} zq3G6n!L<|yoiCoP^V_v=Wfv(2nX{7U8uqU~E>eu^8N8C1Ze-x*L8^JXcZSPp>Xz&T zOFvK541~nFV6)K!$z2o__lUjaE7O6Y2q_T22}jX3gPDqct#O1-b2bAf+|kiy zXm;NIm9~i~L&K_nq?Ph(cz+=}ZK|FuU`HV1Pp>f##68p0PrqFl-GMY)C|5$utbLUC zFOxj{ogxROs^8^;Z_2fwr+BJ1Xiq9w<1qnjIDEK>K~ z|N8sgX)r@(Y`YA4CbaUxvIDg>o3{S#-CNxMSNVLSD0c#spZ;2H4R<^gxPD32@v1!q z5@hET?UA$KVlty5kB(pDzr8q>^L-H#@SxeID(n=8P$8qF*Y}ZY z+E0`BB6}1No&XlWUj-74qmJL`|6o#n&3gOGAMO`T*+H5-22v&aEu`MJ#Rn3pC<-qM zc;lo760s@@j~j-=Pl&{D7ZdG2vSXSM4}DRc>hmt-18zMbFV0cUCOoY5NXCAT%lsw` z9eu4jdZtTJngfu}Vz2W^4y7@lD^$S+0zzpH{sv9!ezvGg)#4?V+eaf{Svq5fzl21~ zqn0OOq4C&oEnvaD)Hu}A9R)q((|SP`y+&Xd8x;le#5eG!4h%{Hrf`)_9R#%?-J7J8 zC$-)86!f&xtu0$&8>Pq#su`jutx%1Bk5~z(l`3PqK6dm_Ysi4H|7;P<-=<${W6Z2? z8~<&I_*S?gAno3h7?~nV;qJu)_?~qHUZq?Yb8UxMl6mZtvW{_}${#$v7zfnO_7_Yo!#B6G zr2W;tIea)0TF15n-U~ArxkI8$)4p7M?xe3GdF{(_INY4b0evupP+U8!eRR_GOG$-O zyZbBRuFoXv8wxM>tVeg$Z&wQshz?&DuUE{j$kRQhM$vzUsuxWi`#Zvfa_qvo4CE1> zIH%J8H3jUTM_QIvr5pGT=oLM1#7N)B2u=jknZbPl?z|GrNwcHYqEmyW-1SB@L67*x zEbt9UainE$9q`c>1h%(54D<#7nnKdj=6t=g&1m1Ks9pBut*mvz&=V5U5hXa78TxT# zMmj?q1hJFjpkzkY7ZNvck`2MgP0YlW%?1EFfpN8`S~cL?%2&eD+zmOBO54g-xdnJ0 zt2H@x)Q`ZH#bMTBGa4@yH8X5_3U0EYt*4(OkDSajoC??BySLffWaB;}`5oT!pOfrv zKKQynrz6cSQ9+_|%3dTMxZQzFTdKe>gJ2TIHSgLWqzlrBD_wit7UG6OLw@hI{{kLl zRZs9vzu>$VqK5`|C`_>E2Rw*bRq{EoS}mljlhvPT?9y=&3!v(nGI~MaofM*nX$kA5 zF&hLvsE*OFnL}7brK?lPd(u55Kv4FXZ5h>iu=g0C9pxb7k=^l(we>ChY0NI{qWH#AU<>t^%t8qr7!wq zue?IdlmCDQzFm=wE7NT>03GByGW zN6HSn8(sw088-F+89vrj>S~Z}l#CgC9?KWjusrBD>_xB2lGP*@Kp&||79qy8ItVnZ zIInm3lypIuQ^LP;X$n^SB!i|QwOYiJM#ufkB~Yh67c-2j;glUC11URHZ}NMb0Cf(+ zPM2`cusg<~!y(m3GT=2 z`Z|hlb*$-MNWo=4l+(e=gOvYF#?}h0mBqtAd%ad&Wc@JdoNM8`hz1=3EX-|xSk|XH zkn_KxblifD_8SP>X*p+Is1enoZ>b>jZ`gBMcXQFvLE0Wvn)bq8wqQ+N5nWhR%^51? zd4v9|^CjBV)Ci~u7=rwW4pcwrdm6BmCe6+%mQH@S}ccI zR|Am0xDlteZyJt3XuBLK$)`!xJrA=%TD?RW(d>&hl39SV7PVPkNe$K!X#h7Bz^j~ zFn?QsCB(qZ#ifRk!{$>jGrxO8lp7P0<2=QvL>H|aEAr9AOx{57x}#!+x)C_2CTwaT2_aCJPc2ha~3 z_x<-*BnJ9{o1shXuJlUB-MyHY1KyTI2rvndo6c5i=?6$SI=0||zCZbYpnv#m@&MD3 zx)aL&bW)0_- z2?08+I0SgSEtd`@hNg&7NTn;KkjcOt!J60YAW{T-8Q4zC7HzJL|Ys^&i!>xmV1C-ghi+SIQ;MSA9>Z z-S$W;d0P zr@5AlZ^AeGK$d+liYeCX-!|9V(<^iJvnEG(^21yUEb|b00c53XzahK z>|AZn3Rgm3ZQ2mD%N+HpI1SrQG|`F8H4!S}B9 zN|#mYH@ETKqpE-MC>?ZegU#u9KYN~&Qy_(XVh7nkioSAD#(!QAZNt?5SZMQSoojz| zgiBk30a@MuO1kc7xSlo~HCUqe7Ob*5LHrP%5G_g~TClq4y+#);(M1=m7KvU~@4d4^ zwCFWzbQ0gazW?UDXYSkQdFIUAz5Bz75rO-nqm2JB1X>~$&q|EW5!H}+r&>owZ6*Ie zp@_F}X()wx4TbY%+bfym_@@?B>^bG)Zp&Z_Go+F{i`HOYxsFUP>U+Nl&3b$!vv#vD zk5|{WIxz3LrSMvA;;})LeA`ToKYE6hO8oZ4T&VlXq#cap?_0<|@lFnpLhqsQpG}*n zC8(Vz|FvUaGukFZYF&^6xzZX zHxl^R?8h{gECY4_skDnrfG<;Qg1HZr+VEoF?V#9%iK%8G@TYMYuccqEH1Q^#-cnRp z3cY#rEqgxV^bMm9U>ZmCP8r!xJ0H@2y_gXe-CX%B+%Q^$ZQm06M-nz9n)Pr#z78pO zh8`UKuV?52QN~9=K@~mB`}~P1HYPQF6vL<7@BaZJ?Y3~>qyd7%+p3d76pn^=Gxdch zgpyk|l~_RpBREEI?{jY#@8)<+Z&pa`X7vg-CT#}YXQJ#gjM*Y=2!YxeX6KVHU@-X_ zjE9sp!lgijrmi<^_hH9&bEy>X>4sp1=O>p^l3Gf~33xlf&}O=RGQ5}zFUB+^vhsy0 zAKClIRgDS*!O@FbiGmSNym%H?5y@frhlZQG_smvyR-SAI@Gza#H@oOt2mu8}zC!?S zM<6+oPIJkd794vuyNZh9#JjpAEFdpPm;K+av(t1@9dAwxyE)m010Sy$c4^7hm09^b zPW#0p-M@6E8Vil*5_y&PEgw+kJcaPNFWjnX(c`)FITOYBjCy)*{78jQR5YPy#P^Y-CE3g1mKYQa#$wullBk_rxw+EaIHc@l7+JnmN# z(|6-HLX(i-r}^s)#{ktGI|1Kvt$6nzS%{{9i3o!)pWx;K5DnV@;PRqXbxQN{K-S%>r3ne~U5)=p=^dd5L;4!lMgz-E#_@X# z+>6-mvoNH_E;>a%*@e+nG`lkeL^vtcBe}_M0edFFAJzCX)}#|3N00*lt3ga%Dgy%z zXD#x{N8-{!fc|X!2IV=^htoh4l1*_ojwjEmxI-|a@wiT{s4LKe@p2$$ z^c>N9xu%tV8uqwgsReqhVAgHkQI(>rJ6LlL*>FzVzHdfKdOCyz(Ybrgj@8Rpn3Vo4 zVmPU)W`0!8um7=`2!1?vHeZT=!_?KHN(fza{0}GX+R_JlwRUVYnTd;<2zv7dnr)zz zF4$3n9vpQ!P#e0}1nekD$c6Qhy(%%W%;F=Ac12vdAHsW%NZiBuO|US@jShslRrDkP zqk4Tgm{p+B+FglEIRgNMED&vS$1*C;$eMEH+q(R&m zX;J)+;7<5}9sb(?8y}dF3Ah~GX70qx+9W28k-i<&Xvnb&UG@>3ms9T0=)<~4gPnch zJ3J#B2_f!I+8UldaE)Ica;Y?~kpEtn=H^}~D^~WD&G!K?HF+TSi$4$C|Kp65!X{)1}Cvn{p;o^;fr6^Qx=VE}}Lz?*^H@eoC2YE6H&? zcxLqkGC76Lwh$Vt8PE_?##dt~KjU%w1N6C66G#JBaV67D3t z1s|f74CeVepC~)i2brxa4KJFau+E*BDh=z89+0NAYp!k+w}#Z6GVns5mHMPtE#uI! zwA68UYEx`20y`gIEy0`$mZGR_$8;$cSOr*YW49z&Y}#_)F3y^n=t}EbR1E zgMG{e^&3dc6Rdqm{TH7757GTsFA|<|i=y}S^UPcr7eoH7$Z8FGgs_7~wO9)=R;cZq zVA6(4;ottMdg0TSu{gyf-8Ipg+M@9jML;6$3b0H)(0-DLzBs8lbPjH{d(6w$hZyRu zT*0MdXXWx|T9Ct}BJ1v1x7>Ra>Khry6O`ZWR?o6xL+w-|W2mIB*hi9`8ng$7^UlLY z-YAfWl7xM%XDqb>(VtMbdiM4TclO5*%tkMP&JekC)lM^AS$H z7c?74xLYadUp{-7oT2rK2++-9oLcRspxEEA~`GJQiGdDdCar|WtawR)|fGe8Q>C` zCpxOD(~(cc>$g24ai~?2fBrysB^DW*$K4b`5c#Lq^LZLzJE1rv#CU?-7&w)G-y-*f zf-cAmF|vpqx~SNG5XbxApZuZ1_km#`F>PY$N(;`-Lla;0KIJ|8 z?}yqcrro{Wx9gcbs&CMrN0tR}TNC)5WwqM~jRF>S(#5#M$7GS2c-y1)9J7|8aj7{Kc9Lp3jHTF~k#ms9H#ds9U_)u>mz z7QNx@PMN147Z(DnB!H`h(fUNv`yhsHE`)P=#v?u|ch~yy%^qQXN_?m(j^lfc&PG!y z;Ti^a6WhQPM{An|1>6){fFK|TgkNZXBBU! zQRpj6piL`#-4$%;DR8gUk>`jHHn-sDfQ+1OHxTh_;eri4C(KX8<*~_K97CdTIj!QC zPQGm1LHX;iX7^WX!ssdmN`&D-jI`P9&RSbb5ok#pfM_DC_*@)R<$_vCOe!k|?ntyt zeJM=AwVp}gzb~uz1JJ&-R>unm9h>_=RwAvt0El1-W^$mxc*)6&QMV0xSEtF+FUN`T z2SwEUm=#ljY10IQouB$6kP-`P_kHE{i?CEh_Xja`d=LBlCkwEVIWM_+rS-;%sfoj^ ztQ^5Mf$G1LnPsQt-H5;+ShiWj$tYnl0222-Eevz`;T>Mc$jLMb+Ehndr?sp9*=4hv z1zkKbOr+X@4}=bAz=HaUkJ(=mxse@DicaCaYgWZ{A1glNBCcN%G5t+_8(a5)l{Mr| z)Ov*ldCJTpIJJqf;%jX9e3`1Uf(B;D$PsjY%`E4FL}{+1heg`p(yJ`t46{gWo1#@{H|CC=qUDrZW83((GKB|8`*G8b?cE~d6_0=j2bJwV=So2Ps!t2;1 zrb!Vedl~N^a>ULd3nSsY46f|YLe#D9CbD}qe^JxO8Is0P{5nIviz?8#(01~a(Z#uz5NKh}dht$b)@-oxPv9VQx zWQKOQitD+7j;-a)q{k@r*=bJ%1RFX}zMo0pM5u_vR$&D=ak9F}{9YUEX10n0BfOnB z;RwND!^Kts>LVY`g6{ta+XeZ=j4q!e8nMbu(kgOJ3{G^(Q3KDsi$*B%@r1IG7xxN}!z!%(7omK|0@Pg;s=E}S1CCE7754BYW zLILM)C&oR*{k?SzRZ)xA1g2f2qUuW*;xD`s$W0!TQzjZ+cZRa-F#$_ZxyQ=bb0S5l zC#?HoRm2rstLuLfZ7cpBY=1D%>A{%|_dL}WoNe?YoFf4YB*C5u5rQ^X_o?qftCWDS zdMuJZ3xNBD4~b1Q)+#_yaOXZd#(T$1b+INNjl7Hpf_fxx#JWEoV*^ZtaPJc2fpzG= z>LZ*1QoPKN(ElY1g{2km8UH&%Q@|8ho{I-$P>Gm_yzx;G<=l0d&@JH=F`!`6`R0DUk0FGh4Lt z_iMJ^{w#EhG-!jelBvYtJNgQmLO+PI9&S6BbLcU%slx_T$Rq*)CnB5pzmQo_17yTZ zI`c0UT#hzW{`^U0Q~(xtw;DemI_qZp-ux*6*(u z@9-t|tVSs+zAu#voFIE2k`;2#M0^|w@f0_^=T5n2BwXVZb^w7lJD|@ z^??qY|67YUN$%Vo-hI8h&2k^_&5?G%%M?&#=_AJeG`KFMI;h?GyMxcQGgi(vntsLSbnSE zes+-$N4#8@>D(*;TjgZzln|9=6bTtfeZ_#va*P^MLZi%Bxu?7x+v^LN`=(y6qg}bd zMOJeG(3D9ge@OwrLHny8Vh5cY!2w}Gy^T|bD_Q_wicWo>$k<5=qm3s{Gq}EpzDG5> z5Kn&l0RZ!xEx&&s1o6OH95caVBM?u7Sx!r(?n8|$*^t2|+rBboWLXaqFr?;MZf_r8OjG5v9VMt2-BSO6QB2&{xs8tEyT%cBMbRH*m? zx-%nWZ$DShXNadRv@#Jd(i~Irs>DDnl7D$r-mQS}G!mbrpi2cH^5n!!3u?gxJX%NY zFp~vp8*r?>CAz0PG0A<4F)-3@T^`ZEss}8B&>jf=_5CdG_ak)=jjjMDt7|^^`~eN9 zX+yfPXJpk8z<}zGoJse)B8J})WM#2S<^1DFMIYQ5dBS&@0z0XeN&{|k!6?oGnLO?O zHgEX&w)sB^@#fY|*f0NW!EE~EY4Rrr&729>MbK8QG_8I1crN!}sPs1vOE6IBCc<%F ziHmH7m|t4N?=r?9#-^dPO3;El3>F)!1$stw<&PEI{ z{li!Oj@Z^WYF{aZo2Ok`&LyDtG#o z8fO-*&1g0rB_)=!mjH}=(XBgDMhs?g6)TVRBBZ050vtqMb41Bt4q^Kla#Hm?j-rJc z>qE(U9tU42JmWbh?I?7UjCOBv*dHx{Cs|cqHN1CqwG$Yv_A+X(qcey(=Kd$>ohHj4 zC3DaumTqgC%YjY2;PZLky@LZ=vzguUbK6TYtI8KnPZKRjrLu(NSBnxpz3dMVq`g}| zc0Sx@ozDJ4TR^|Ra%oiCpd57P=lkLc@?y9qcd4hE2xDuQs-?-D0zTZ{t;#Vm!3z=C zG=WoCpVn3pnW3?&m~o;S{)+|~+3@WPz#x~FpIs%l4P;^rmj<>)%! z0`I7@#Xx5SmaIbc>`4_hsyB@5Ak`?gnCyt?hx3q{QMQ=JTh0XzI5qviXv+0&lmZ?$ zA|$D%zJd>VPSe2b-Vn=fX6}_L3D<+(r4sHL?Dwa{3`-V}4^|9run?S1SVMzoT6AD* zyHz|CyX<3XNpn~~omBJ0^w z65#jV5WW1o_`Q~KC*-4YsgY1iojMJ-=C05J8S}=L*Yqb*wXaR~D1*1PDQD<;IOQmD z(oCHfQUM{*v&s=Qn;Ja1-r$`2ng#feOKs=(llAM!AE8)#2y(aKic4z5)K{_u@HGsv z?V;Jf1Wz$rGe@{ym5*12nHQ*X7A3nV@g+Az8}lC63-8`kL*`bJbqsH}~^Ph%--}iN54!`3msBdww2@ ztGP4bvYtn((UCO@PuKK+e|*>Qxv)T|k)lPW2xZ&A=N_b0o1^|jl}Xd)R`A7@ZCl&0 zU1`Z}i_Ls~6RXuFO_oNfP6^p7V^HT3f2HYh}rFB~M;|P{P+@BqwYuS23f zYqTfKm)@3aY=m#4&$HG`SFmXy22FMeR_OIv@NeLmB-$ZaH}&Z(~putuXoO+K$)Y`2sm&G1mkLeHu>0WsFDmL%KaF{!6&P-B9&MaCWP zNh-QKjK2I#C_=nY+DJKBxl|)k0vJWDqspdMKfzw=y0B`%M{GWom}WoWZD z5#PpPKbHa~Fe*p%|Jjabj)j?2m!tEc?hUO9D*341`^dJSOF(h&4cZ-j5Njht2{cJ zxcd>^HfGeaE7$up;@3aXs}%8$(BWq_rv=VT(w~Mo>y|5>nR+j1aoNn%JvNQjWUFRV ww$cZ}%^P%hbc>NWTgh$Lx!*o;B8l$KWLldP0i1IFYQdz6w|uv z3b^NU^2#M#ILYT`JvI5lz3t2K@}{UvUKfY?tnMf9*S0Dq{EmKIm0jMLnVmr1)y;G% zzN((UqqBTw%%;T;%8!c*7w6V^4}5UB&L>SdJq%Z`u@7n6d{cBd^7Zj|Ska=&+f8x2 zwo5|3Yf{2I#H`8ts&ZxWX|bMF_{X8)TXkH+f-2iUZW%vjRu-#zJCZNLpK}N`m<;m!|L6NCWO%R8_`7fJ@kY zoENx_-_eBuK70iK+2%`p;JAc+o504GrsZ^Nzh_$X)vU1xf@~&!2|Q^xgOJF;JBLHW zXx$WtxHRzErsE9Le%Yoe+f~uPk0SwnPLEl+?(iVMyTgW)0}hk)EB1S`sBj#BsJ9)T zYvO~CVR{{>CBG0W(-eHiM*=Nji9B(Vqqa8qcE{w!$0*-=Ejo#gNWRcsNRQK}{Md}F zj%;QzEv98LZp?MMu%3T;uKfqPXK=dUipnVQt6I(mj*A8f+!DV%1&)h+sqM(P6{$s? z)1}wbQ=BpFW}dum@_iuOL&N^fNm&&~UdrX*;f9 z0cmu-IQ&0>M5+jJenbarm;CHiW(xdrt(=-=*kH1_Ze6LAA#*970}m-UYn=2Gdv+yP z`$nN*LZGqE1uEaNSX%+v6)+@R{g9I8g#WPWXPoqa^&i4psyY8ss~!x;;fDND!mY_i z2~MAb#%C1=BF-0$0i8@!XPL~O<&@8;ywj5y(|O4l=GmrhRo&_fdcfcSMT*7E=q2QH z35Va5>qGTZ2$`>H(0Rka->N*rtrd9%InLtq$@dMCH|Sf-lg+XatkWyx{`KT2c$q)2 zPO^!hZZ(x@r)CXjyQvpdxmb3JRxlVqe%54kuXzunle5qj&BAn&glBS}e@j0Uu7Ijp z;t9x$W(6x&!O1A?&|fGk;x{dlGy_Sv6`%Cggr9l)Z%ZSDm3b*6Ss4{JeFOg+C;t^C zFi(BNG-~L&NkWo=i$5%+!?OB`O3PJ&^vO?7bBpxpM_mqLdw&s?# z;;3twBapH+(5xClSn?6*OI_l0MZ`;P3f6@atw@C1EDg(X{B^>a=aELXs4Jwd$}ASJ zY63r$ z_rOM|;o)>6)EI`#*Wv;1-xK`7JvMNxFQe&g(Y72pMOOW1F%7hEGB^dq6U7)}B8|$Fpwd+RYRx%FFhJ;PZ}WMq>Lc8W z@DlPB)2aaCjr=Ge7TUbufKrcEF3=CG!;DMo;UG!syF=%RhP1ZER3JIO08&8iDAEzu8RMO?7jV))4!>sX)qrK0rH(B&7?6HA6KR_k@-~S$O!Qf zj$<^G7dUJvB5IMCV&#^6D4{sv4Un9c@*&6hfn8Rdm|z<~zIJ3AB!y3a3pcQDf^3{@ z-^NTDq^Na12bE}fF}n!5;s*A6-L+gwfyQ%?eKZALTvW%!Nt+|VpUFO)f}okz$LqW+ zS0;mh&ePvcHt^ep>>td04(0|7nPmrOvJ0>_RXHzVGvJ?d+54#BPtA{i`T1|~G<+#A zxJ?K8&wrg|9|Fb0-~PCS{V>g$?EI_;r5kmlEc^0I_Fg%Izg#(=on3>Sl)dk6gNa>O z_i!Hkjb~w^z%TpOz||!XsX|kbGodL ztB7-ar?4u;BIEQ6Y7wY%_DoD*yh?cee<~Mkfrj+&H+|2G`0K2c z4&j$;ViuZ#+P9l>TGoAw+kSjX4t4Ni97?c@_a(bPw)JxC;>Vl+{y$W9adrZS5?)MH z`&ID_X$7qaAR)&pcW@NuE+P(>NXstQbJKPr-a)Q=?JF5;8kCE{pY0U&n{@i#ULNTE zV@j5$uv`8IaR!{tVq<`+nMNfQMkT?*05P}Xy5kn^LZ0_}O<-$e?G!ae(efYP&r)9b zNj*E+;+3BQV*>(qHdMx9CbQziEy2umAR>Vem6k}%jC0;$b;-B=o_;~X**osW4Z%X} ziHi+tuK1Hh@){9UU1T6b=ij#}$FK?-56e*t4fGr^GMAZ$1Uf zGMojC#F ze#IAt_*eK`xNj_(+d$nb3$6mIa3D6QJ)(l>OD z5zz$^b)Bt_k7GoppBo_gl~ynE1|#JVPS1<m?=+19^XfDxTZB6a>e<9EMv%e zh}@~i2LC{YB_~9_6vWr85YBbF5Dh9`d?CJ2HjddKEY4a~hrmm%6+@QF%cp2&G^XG+ zfY_@+;{zqk8g$WOy#{*S6rB%~eI4J@^k|Y}Voz=s3ssJ40(LlIe0FYOc6ip7@Yo5c zz!Npo!Hz(5q?`O0ff7o*f8+$N`Y1v{XumD1BaC(_GrOVCe$ei3NjhYSQ`EcRyc9&U zp^|gbn&wF6lG#%ok7OfOY=XC=v&4A;5$3`Rl}^j*QA@GTt>Ih0q-)tCw=UahUZd=W z^%lyuP3w~Ki=4G3i0qRsXN@o%g9#4RY^!tn5hvcrB_E#s-PS{x+`U1_4s3%?0#x6; zUAn_cUDza{j~@?`%) z>^S!HJ!oP#1s$`MC9H5=H!I8>o-zIwiIySE zvOO`-&xvpgu3>TI%6VD%{Ue9+>%>`v!qWzEHYR@2xM4jZDVg+wbuooq{7U<*vTn*w z&ni6j&XU!p088DH7Ws&f%AuwBiIf6rA~LN_Y_lf6wde@&qm5a&l8;hrj)s~Yt6aj- zCPAtj%Dch|rqXfDlj3u|qddROHEmO6@)o8og~F!pV0Deu-A8L|TT(=bkMNIUmda5v z#*~R5WX2f(Hcj;;^sg|JVUCFm6)WjAb`qi-A^((-zVWVnT$*wf*LH3bNn96J5T}x!T2ibM>yJ32FQ=jB0SsF}gJu(MpvMb^5 zpzL*KQ8y>(Iy;|zH2eJ_p#6XDcmXp4?nwxDZzmB!chUn13qw& zotqk`y{{iGtxnItJ@*jEMm_<-d>-EL# zd}p4ah1Z_X{$Xb3HgDjHTQuUyLTBJ5c+^tY#+ccUf5rKI)Gk5EN3yUR1oOIVUx*}d zlFtgzSI%S~TCd}}Tm?TL{B}vW{xGC+!EQ=dEL!^mH}BnqF!i!=4$L#zC+mKdAC=~K zSWbNKl3=oI4Gyw%c6ART#^1muiwev30Ywf{>>_dAMi4K48Q@>$A7h<~`uLXTjC&RAZ+ne2j{zyGv9XELg8XXdmj*sN9AodKZ(Ptc$IgLje- z2FLE1>@)vMxn`e@;U>EwR0|3TKtemR(n*Qv1#g!kCeTEjS3|0W=Jk-VB_hKbr*KAU z{B3^As8hsBNqg8=aWv_xh&^p%?3=Pf6)a?v@LGut+RphBn4-`{Y{uj0M5SqqsOPQw zh0DAmtazIP;uHE-O%Xf>_Kin4#Fb>ODOyx{@LLhZ<2hJoCASNS42kPZGz##>?4Vw; zl$tTu)_#%GiDPFzPnywugIbadDaYU!Tudo%*EUs_e37Z}_XQ|zsD!1Y4~wL4i4R7S zoA4)J2T-p;baV+v=)(t`)SOcKUJRgC0Fi{GJ&9 z5mF3Eivar`e{R6^hebwHWNMbR)mOK8%pW~W{C+R-d*UoSOw9h_lf>@{dVhFd|NVjF zxIP#VB}t%ZI1_}djVIWa&z@tJ;y02;ViPMdcw*}xJmQCW%!~{1Rkfa-e=Do@6>&T5 zorw*8@IbK;y!SlKWgjHS;=x@mg6)^75oB^?Mdm>`da*&XB^Ts=;&j{>pNGD-byw^# zsP5nAROU>aEw8s)40(-`M5sM=m)qp4ld18!5>yMJA%Ls#C{Hsbr$J4JQXYPy|14J1 zX_LOdhH&>WeTS!G4pIM1gjQ;|Do$#r#j96_J z{Nt%7!+FS<`RW<1Le1K`21k%n*R;y7$j-q$NKXNr1^WEP9d%XSZ#6_psRmy(N9fNT z2LCi(i6|3=8bj82Rlx#CNI&)LLoOZ{J=)a~EvKFqijzwZ>8o-h4oFC0U{1<79ao1Zd!oVEs^rY~)I>H*(JuZ-;|$38Y4u+tiLJU#GzalFgLkmJhP!nCexn_Y%U zF8M^H*7B=vdR&$Bt46%*pU0$}C^{u)Jdc4RhQsL(G^+?b7UngazExo(k}FP#;`=a= z$gAhsb<26hH?SKTo3l+!JFD>deN*+m@5H^X)>XTcxDYiXp2wnT-hD9P6}{#j-%puD z9|uHWsFf`}_K|TpeS5sr=~MUlm>uBNIK3nwUAZk+*mF4b-tWb|iYrh_PwEcRBeljx-CW{brFjhY`ja)*TEP)^El&xa35#fXbu05C4 z!D|aJ@Y>I_qW1G(to{6}eVW&PUiGz~je*TIP9;uMfL;ta0+;B`nRwrs?eDwd-&ai9 zy4l2UdlugIS$NyU@mBe+?8V-NVMYuWP7Qn4!5_lAJ{aCL-g?S+ed^zZxdZ=g*ZVGo z_g%I%&itissHpbGv~6_2l0A94%&F?*EV;wscBx6S@^ocppxNbPf}VvD(zALujhU7k zmqzuSdWU~S?tXIdcdaNXlvKGq4RYi>s#ii9Jjx>P-hff($GWa?>$nKUqdG;81l=sS z(`k{53mz?-(j3YFm(LpX<54}SXu29Sy3O)rV3Jg2Ggc4yCjMHf0toj(y;zu8%6+f^ z9Yb^XduihCr!^lgWnKf^@%!YwI}}3;8LPz5;+1BK-gP%qBP3dxuus`@pjLFHr^S2@ zjMfQXnxr~vSIp&4Ha8%s1#|`iUztw(PFL&=?mOY^hC>ckxL;;M3tkG@8o!VlMAJl? zdf|4IE3ytADN{;nc$&)%N&v;0VJ2v~vf8Q-QHi@^>P}^2?~`Rr$*Zo!m0cxjWzb7f zDLbA5byLbKwW+a{TscmFxD~nmZ->mm(0LS1S;KIP5h{od66wQXBrOspg5#V=Qoa@n zW_|S;E!MIgt6f??1K!G%YNvEnG7XZTc2%z!7a|*1Z-8XXi2=^twwA#V)m7D6m6;BZ zvuUUMllxrT-DVVBk51{}fV-a9`fO{?R#fo+MRZ8TU<|c_q|r%KS5asB`4*bu%+}!v z6tdpIQwBQ<6blV$Ep{(y>wS-Zrf$wrXM<*vX>pJz+140tj5|F%DSTxK^NM)~n#PFy zP7#1oV5EGsXat$~O78a7Tf7H*>nP6S9NiKHf?{%WVJ7(?t(LWJ+vpA=)hA}wIEN;6 zJX(L$wi#K9YmaN`Tqp6e;8~ zwbT^(s1wr~!O=E4cC~;q9eGq-`H&7ZF?`pTbk3_C=NK-|`m*C9H%h=vfGn-hVFUcn z9m?rdm z$nQ3F(;<3}S4Nm|xDa6z0feKa$f1g`E%!H4o367HV~e8G=+e?U_NS*1!cEe$)6WUF`Qdj9<-wwh$~ymdx4GG>!$#r3|(8I8)1L{iA$1gAC5!T$7*(39%xb^R6cP$&Tney$kv z*ye!BFc|Hd!AX-9h{o^p@Nw#(qGUuX*U|Vls{xWRCOz?86ribP`yumCCZyYVj78Aq z4^#+y4N7h!z_w+6wu1h`5S-k1P|VH}v_w7$n>8?0dy$RyHg5>W-8az(}K_TLndo{r~>?IYzVgwrCErCo#AiEK_{rVRmo`p^;~skRuZKBFT_ArXC@ zXtpC7Fu(`Vp^u1&TrY_|J)Ch+m2(6I=SkO0CE;cd%SN*3D)j1qq6!NP-kbunQBH!o zYRhWon2FF?h{o<}Ls;Fm1&`oguW_}foHuyFaNahL1x97Bz^36>U78?U6Rnh-K23{1 z2&hwX*Bm;#2M_Q()(F3In8xXm7+)^`?&4cWS$sW6jKuCh@!QBy3z|k7kP0&W>n-Mg zo_RNt8MnCjM8Qo{97XjPtb++sYLw073os*>|6+;VToNY;#LP)v&vOvp2q{n*kqsCk zKnk*kjicc63H$CoAIbNHF@?_sARnXxl>kROfi=KtM$Qs~Q&?|RGQ#Tf`d2DemZhm? z+@=f8q8(UIq5aJ;VOEmGksuu!9dpUla1!uJ`KBPJ1?3;S75wK{3suV%BnaaX4D#T) zdLp$gVHx>H<=CX*n7e@$FOEO6G*zE!N1FmgeH{piMDN&*B>9kQ9BB+eC%Y&x3q?vw z8Z^{eeIgDMg*}Y$FiG@$iTI*x8X$Zp#q{qVSb@)pjKy?cTmWQvN9y%!@>kEWmlgH(F& zBn&m`D)Trqq}32%AT;HvT!7BM$@J8Zd-q<5z2BECmnBaNBXIOf530*|l(&fnYU${d z>}V5r9T+TPW|~DV|FjWB)@&#iq}RWr2SMcLB+W5`446bwOD8DA?IQeVk-4g3iQ7Pc*r+bNd?K`uJQseP-Eo*^)rxYE=xWmCo06+f|IqG%0vmFI(^;s9Y@7IzS?q;H>%x6kP&J2cR z$U>pKH72z@5^+KbMo178D9$Va=uaOECFLlkc!?H95p#&~faAI-(&5-)rYWDDWXUMUu+I530V|sMZK}8 zMU(s`a-wZ0e@D=dRer{0oGu$6T%hRjm|ufuHQNpIAD8&M6cxX8EVmeQnEdZFOML7Z znv#~JJL7G+*6CUtzk2?`uhey6OGhTf{4Hbk7R!o@(V(|fhIyP|szTLu2v-wvc~yL$ zpS`T#((=@l_AkLTS{yTG4^vzuvb?f`bd=G)DkQfP(8Wq4ya)`}xe=ySZ(FO#lE1VDsx>={QT8~v z28|(Mx}>%->`=Ih_f2_Pc(j*uBc6XZ9Ox7|ACE*Tj<&3$IGVO8O26N5DYhBLjw+;V zS92<%8os-|XzaHp)etEN#es;GY`0`hEH${9IuLR)1+(o3HzNr~D~vgq(9R0bk=+$t zh4l6oJKC&@SqTggVcKMuE{^ve+k@$>G?q7yjNUdDu(7Ii zvdRV&hN*@XV;EiQ{9^fK3X~vtB9-jFL>(u&W6Yyt{Wi1+4d#Ec-aZH}{%Z?NyaLXq z0<11jHV^1MbD&YUHrtA)nMHV-@AaEgA)PqgYmZ2NdPMR|9st~~;2w;84@S-tO*Ro0 zT#U#A)Zt(%qtxjHOayh^(!f*XA+>%9>C7hF#aePM>4paHw;4D^W6{L7V3HGMKZSc9 z%nNp%$i}#A{>|{A_uhRz^ye4kL+9+8y;cTv5Hp~!83S_CiugpzHb;TIJC1ePq5a{M zLj>yugg!5cPmAV@s%Yj#-!T0P+1YJh!OtpNoZ&mpxK{MyzA%>-!&Y#%t>!aK@P9ve zaE>qRk3awXw=9#opwB58m2{h%PO<*18!&ZHHLnZT&58P}Te`t9DwG=ci=w1tECoM} z6%x!=W@mWSJ`_h%wcizy`>Y%-CtF~Ax_mmXag2Y$+*#~(xYj>h6j8?kE-VUao{A8aMTXEnM&S%Elt zM@aHb_ynePfeEY#2`mn<%S}#px==!gv|xl}S<#Rc3abS?V7m6{d#t>Pp1=EJGYhxgs0?G1;$;gBwF@$8bQDC7B~T65FLb}_7dSq@?VC9$h* zx=fGtW+VJeBqK8L0rGku!xj#$IFp_PIvZ1!Q=Ml&!on94O`X)$&Okc}FL?=hq#?*h z$;#UbaGs)X>MmWplX+>2aCS_F<@uTj2f@4hS;h;Qn0Lzu|0Ipk2jT4sMd*i-CPwLx1cwbqLBFwxfQ4LJ}+?%bp*W@cLzl!L1}G8VY@pBNLG?OIs2 zIBSC9&8q98ZeEcdDHGxY^CHuk_3NGICB~=JsvIn11ASpJWLS~fsZqP`H^47p>9iNo zL><)Xs!~>@@~Tg*7w}NW%J7&d@CP5!SYS5LIb~fet4iX+5DfKoce1q=os2j6$Te$3 zTsPQNuIW!*g`+_rUW<6l2ks5_1-lURyk_y17QkV27e(WIw!y5|?FiP6YDd2VhYO`ANGZkdgZGrONmm0TlF_I(jBS@j z*3m6aD_cMEl)b)h#(oyO6R`08}@wH&! zdjoiH05fH;@AQ;n9kxn^Fa1=qRwZNBa~hCF1(>RFnnw(-9lY0sg9W^h7=A)akzMsO zZrI<7_&jBNG+C%zq|93@$wzibk@cPVDVQC(fn-P9_qq~775YkGM-XkaiBK;;jTSjh z^0D$Kc2nhu;pYOX_SL}r-gmdXqm71W{fkS!Vx$pQiTVpn_;~e7>BTt1&xmzj z&4APlzA^sj&SQ)TJ3Jnjjf9|8Qi4g{&=#P=0b%i_6kI)Dr{LrlE2(y!1%xZ5aLEl+ zC956wCG&7oYmVV9V1r90?#{EZ>?6tp(N7>Hd@5zpv;)Wg(_eo6+qvvIxzbLpf6iY1 zm;d#DFP_Uj2Hg$Kq%kSvueKT*bSh-vVytW7XFAZ2UQAUsAaIO$O5~v}0_ott|Idum z)=-eSbnOCSSRy$8i4@<*Plb;N{)$S)&?=7ldIDD#+Npl{iw=pw1-IXVkmGL+I_)w6zv zo{bV59gr~33M6|J#_$1ck1ne~phRY=fHRB{9<9ksa)lV!y0@VyB&!PmOGl)Xd3%4} z>HT>hc=s7D;NFwB_vA6MIW!e&t|TAMDVn+(X)6)T3-=U5^O(}DBw6I}r($rR z5v4@WNNUmV$HfK`iWPd>ki?@LRVrh=*FqFjl67Y7tu!Rl$H_(Y0eix-kdO^^`_il< z=|C4|T!T0pHash0TMQY*+m^qNE^q5nK$N|HeF!S0h^~=uTA`h{EcrG7Ww_SAx8ydk z4gy~=qIY3jhNycp+8DBH)W7X!AEVxVzhyboMV~gn3-;g!T~!HN;VJ*(W%wqoWbz)L zZA6NT#xYFkdq{^aXy0JWBVtT}_Ol-9kD@|+Vw0nILSZcE57QAlo^zP+^b@pa=s18+wIbM8`4My5!aweOCTS#FQ7R5 z#fg(qp%WKBW(J)PmF)EaE6h{5DF}wEyG{}@@&~Iec?QA!DAT&gC#G17Y;vU$gx#HB z++)fI=a&OrjTPKtdjp#IHmPiTf1rR{8tFQB-eQLa$YUpbT)sGC)r1~ws|ql9@~U{= zSEUdDjXB<)apJP%zz!&?lU9O!j-(!=@xyk)LZNK@wUtY6!ExPY$9|lpxKYHYHMfJn z-3mOwqnc5Cnk`*Ih#Hj!N2Oe^viGnibPWE^_lIK5)+q)U6>yq*a5te^Dj?^vPn}{v z6c`JM%To!Z29q7k-o6tN&*8noad#vB*1bV}dV~5SZ^imG+}qT9n_47-_ylsyBg`01 zA)M)EXDVQfmlT}AgqKlN6%7;*^DiAmsupE3FBG@eLU7q;BQ(v`De3M({3S~z-3>lS ztq^ZpFIo~&i@OJgnn^tjOVGb!SS+9Jqv%mCeSC;V&=TLJsk-eUdicp~QaLHUpJ;-6 zkD?|nDh*j6&S)e=JqI1b_U1|^`47oU!u?{Mu_OXZ4N4!Rp%bM|Fs+hpw?xkr@BxEV zSjKv7V|X|_p*WNgd!D^M{#mqVN>YUjtXejopb{@MtvkgfNs)oO(uQwB@SR#QyrIIwhZE^kG;`qZ{g_seLSJwyrkWWXB%66j3m} zkL9ZI+2Hyn8k+*iPE70*qeZ~pI;2RBc5R~Uu_0U=QnpF{N4FNqNnWEuWhC-3&CsD} zlI`TJ7Z9c}6P0?N%er&*sUU?R%Oq~b=(0Qcx{#uY1pGm3im?2wc)|KL5;ON!hC?X( za{B?4BAqm#6_k$4!9WeJM{gXyUbIq(<7zUqD?*cAZ>C9^UXu)pKc<`=Nq-u}Ef;+^ zh&!0N(vFP*kq`3(8{w^(y>BOcyQ|RembL^Z7I$70`+@y1z;B#$V#e1DaW`(N2j&Ce z?&1{12ewB@I%X-OXpC)L5F##A(T6`aap5DKHLxX6sglh+CynbDprs@FSQI&*7PKki z*OYnI)n+M#8@U`I=yTRiv6;#0IS(}LSJ(mg3mw(Z{>2(kG*JqF7{Oq|#zi*kj> z#9ZP1W=M(6A#B#$aeTfbt=ouqALfRMU;IoCnf%_4rM|u3RS!idF&6T4Tjlw?DNf&5 zH?Y_P!Kj@M?Lun$>08&G(Yg~1DdEgkiMxkJMTo4CYVZ|P{d8R)PofUI;lz5gmPE1# z=91LUMDO3d=l1lT+l$^;itRl2UfaFbR>4?S%WcZ)NU=sqNI&YI!b7)Ouc*7CI)%5$ zOMqu=@09`qy=XA@n~WNT*2@E$UczZ|k6-dSn#&q^E28QB*9buK3UZ!?h9y{E>??~i z$OiPH`@&l|$emC$L(3?DSIt~K;T^I6YUw*i?2eLqeN6ytj)*~$&P@Se;bTURhf!At zt3tb$zO;9PkuA3yW895zK|Y=R1?AJh>&1z^I4;~TBA<34!(Ox2+&IYq56i4kx;%q) z5~^G%JAnfagnVdD{?6&-HRB0iiPrC(B$v-ExkPhyR3q4HXuC2M+tkzuMBBruy;aNg9f4FMOTO&e!(G*>KRn{c+ppZpqFVjxG=W!GH;I9^F+RJS zVXS(5Cw8-qb;YR0Ap{&%QQG)ZTw95ORQD^%!;F;z9VCKAwo-k}8nZD)C(-bDqcN>U zIMvTBMw`-*<6j0_Y-efNd!n@LU<5Ast9_c6mRc=z@C6rxopXuaoTY&h zwD(={?<*#4-E88wJqvI9EWGXFcxwc;{#G>QS99V})*JaXz7#|rhuJC2l?&CmgFusk zqx?S^8*A1sNXOmNqeCXurp{JZxc9N{Ol}Ee%?tUMU~37_DNS>oE*q00NPs<`{EI5v z+*u${)3O^6G5#V|o{t|&SGer|kP746%i@e-3{vP777}wk?nfFSpA+#zoT_u@$>&>P z{d;k5J52lVMt9$etfb;u4O(24N-{x{9=FC>G+HUZ58Tp;f+f?YZd=#8j;5Y_%z#0e zA)hueW(gXo__4qss;GRR;7N>WGw2&mgPiY%BqZNJi=!i8l$k;qk>`YKnvyJUdXn+V z>9X31B}y&KV&(u0u8Td^kum*w^4T7=CB%@fMubVMArdr(FBFPSLI2EwP^a!IQ2WnB z;nb!vQ`bYx5jvS5-C}BFG?gTc`&|G z_H_%6pax6EO$w!1+PthVdb6^KAFP{@D__*X!JL(eZ3+sx3;x~{euPn2J7C?xkIH%j$K51FOm47~$YUj(__nmgT4vTnrCuRt zA(bmRD1g(-;@EK#>53WHVP-@Yrq_)MYm6q~^;&;{SrKq3Z1bb0C`BTH%K_e>(w9G_b_MsRbbm@&AU|pt#^pWL_ebpG zV4e{A^I7#Y4^=BGh?}fM?lJ0YSQZT?dGlAHco*IaTP4&uL<*+gtfa+6?2yJ*dcz4* zJ^pP`t^Te&{;%fIRC4_k_{(sKDR;Dpx9GbP^tu7ck0yq$Hrx zzU>?OwqzyLEyfyrLdlTJ!MtjtOZOokJ_RAfd5rHONmj{gEyBA(aio@r`+1tS7k#td z+QDd8)n6F%7<5G#BGiNIFE~FON>A% zMbn3fTFzat#Z~RgcbfZzu2M0ZxRNP~?l!c|;0iNQLx#*+xgdKh_T@o6E6d6DM(o~* zrR^sP_u|J?hKmE3faqp|`8rY{Q>!s9-*ZL>Ge)ZUpgfbbp`B&ObnIo(cAza5LT{K9 zE_?6I^+6GW?A*H%Qe{=Ez=r>We*3&CrM~FXN*W+7Zu$5U$i0Yp`6#Me!o85$3mGk^ zGgP>0MK6!JWO2mh+=F9dyWBhynUYf6G1moj`19(4-NrJmC;N|76c0UY}r4vm%Y9AT>OCun> zhRjn2RKcrgr=j~NODrYmNYvW3!I}NZfB94oyMp_pzd!nkG|>AZ1G2FFf2g^ro9|tNxsL{Un?S`|Ces_6GQ zEyHKuT&BcV%IZ$LV5}~frL@|mqa%1EBwChdWnOMu)oL2lP?7wIZjcY?_@(@ddAB1A}xFnJ> z;Hl`*53(4LY|p!1o_FB_?s?aqcS#GnpCZL*S;Mj&kmBo{7G27{3U8h+eS@UayIF4O z1K}-i(BndJp9lNf!#~m5y$~Nprc*5XN;pVtRi1Rm3~f}h6htGpz{x5fnP(|vZ&HpN z)eT#_*~ObFI2e4l{V&&1hQ^NG9?th`6lGh?+g(I_$6iJDiZOP?wyE%*ORb{yzdo4fp z4`3yL10a19NK|IxUV=}4>VK$y>b3e4`Um<0?C1WR?uavGM4UJPFDFqh)N)Bz$mtAm zVvZe`i{f(e@v>T(Nn6hP|1xjO6H~))Uk$%?eOs;9W;$Ayt9jd;m{Hx#;g+w5TPAI_ z?3&eR-I}i3v}Uxf%ii!;*X49-TKMfM`K_6nu5VU`zYa#R9nI_V8GN)iZCg&7zAxdP z&&ekjaA7-|nbpLM=I(1>4wu(uZAMLbJeoF}G5p!p<(S{GS=CiPYR%LRVAMD5WFcnN zjN#GgXll&5!wHQ}%NiHwv*8|?aJ3qZ+iG$gu3lpk>AKOn?D5SvhTmaD%X-wU%hQ!z z5;CuG1>+E-Cf}>8r5R1i)wITc9UHz?rwuHqvJK>xVKURITsEuesA&x!-DFX91}ENc zW;6czX@!KcG5)oRv^HtQ+8 z;2vDLGF+K$+EqEh@%~Er?6@u`$8I~vn;!nn2anScD|>s_;N_}Vf`cVp9d~f)W=j6E z&1$~EYyVkJw|QDkDue#4I7usanp>ta$UnRb2@!v)$rGd}3J!hD9X6v@(J3D097*oJ2<{$s}4#PT9+p!CuDb&y+CaQo3t1G)}A_2{)jAe4R7U} zjhg}s*U_z^ymQ@dCMH?5Ym$y|FZj2*$)?7iKB4?@1Jr0gL54iuOzo?IhWo{~>$!pj zq|tB0;r}y8q?!;XIcc?-0C!v|XJiq!lPslMMJiLsm&<3sF-pz|$D6S|sgj3%t57T< z$5g`t1#eNVtT^mTcqLr@fD+=E|FGOlIiUfgKZdW=Gd@$R3`F9;DQ8M}Gntg&>m$(b zteQZC_q;WrfoaMslkn3KSY5OZg}x`Du-}uMLt0eub4iWUzD>!yIO45i&8L3uYUW_6TE4I>l)6?s_%fE;E+M~Hn7w^=m1B>hn9tn8X)rpDltdJ z4N#XxXmGX7c7$ZnqMCVPj?NWV!r!1v^=96*+oCAWT`WF~zr#{&*6>^L?zCK%a|73l zi^a$Obe2x1Zu;W<3WfwPq!&&~U}yNiQE`6OR5iTiy;)nLCDUwHJzh8}KEP3q%)f2y zuEVV#))o8*?0@ZkIahqRET7^M;zGhP#OE%4Z^dmbpcpOJy=T`kW zI9Tum{mDQ0P4>ZMGp%NoY0nj(`d@(3>9pYu=L_NXF!i>#S|L%IH0hKW)>Qx6wD7;- zgnvc3uxHW4ThMV9fl7>Nt#A**^vi^s0k18q$?yrO&3I|=`4!o$WmTg#52TEo=?ALQ z;0jr71K*0f4*cHwEUacuw3HKjkaQ80aCmZ4Fu0wziJW?u@omMjzfMl}EHP5%O^qzL zHuE{GTE-xc4EmNjWnkkqKReO%aXR~Lpje zaL%kCYpE^n%mq}UvzXK+5O3mI1l_3{ZPsAtqT>hX2i9TARoZxvB=vnm$BDirZH=kc=3He+1s!am zqsPdZnIv2zQ?T40{d#tmbXELMWbf@?&LpM2*Jx2k0rI^h&8&YUJ+92IAmgdci4o!> z9LMBUUf{4-5$gkqDYXE}Lc|^T=7mdg+HzYHSJw@8!BY04V(vm<9Qe;@Jp-TTBY>=!T*6a=zp`C^99s* z{}uib{{KR8wJUr7wWxgk%U^MjS2mqB3!zd)nF|ZcNo%mg zssAaZS^p%B$x!`Vp7vu9!q@2f>1?9bJ_E77;~8+m%A zdx1!(FX%wqReGwZNp9*v?ooGS^cf4)1~k`NI@5_J+^rf|WMHlh4yjs>I#9$H)*)k- ziY6?>2fxkFlkh3U4J^=UJ$=~xlE^I5MLqzhL7)6kW%rQuuZH^wOk7v`Zt4Y;tIwXqIGxD#a=i^_3 znY=5R2{N@8VOCI$KpcRe9r_ z`O>X6suSmv$NV%7=QOZ<>ZNl|7rqTtc^%?fbc=O&$Nh|vefJK(-~xh*6>X`}I!(E{ z-eE=vAA)5W&H`oJB4Y`~LzC)CJ#MYF`wQ;ew=zw*fBa$in4Y)kGwWfw$v(T8pnG<7 z(p1wzb=$1qv~QMtVTgZ)kA?fjvgtn5y+V%NH}vby*E`r*eN|hcAZv?e?{?%egM`gT zVry2;Z-pW44VHh*E~9cdsEkLC6&F3kBA&y_D5|Kl>;1BNHufFeAnJDGYe!ygtc$|E z{6k8sHsJa#9b-atK}6kPtK)-xiRtHFke#K~i@d=|IfT>m5`FL{-w|fYbB8(IM{>9( z4IFaC`GG7WCIThyROfd_h9w6?W(wkKT8ao1U5FM{E4~n4C>zIY5SDsS*2ln0trbI- z%FCx@WYnSVHGtHsL4#-&j2d)Nj1;t(;pSt>U#GXU8#Ku=qB1h`xhh9B0UJ*kADvs6 zJ)X4%JhlZTcdRD=*f$Uz=@vgmpk@;9A31@`O%lcn)URo9QU!7ddMS5ZT)uXN@o%gE>F-bfZ0M&QYE!<(HE^LzH%8v)hZ{vi3&M~i)WGq0J&?b#aB>KcRYrjqiE{(5i4o@yr z{8A9AT%hkZbsT&8j+)plLC0)m2`gMR?Glq|IjbT<4&~~=%s9QK-`6rRXs%^YbEx6c zOc{SmL`w{_?9P?-VG2ZMMr=it<9>Fe3V*q zgz`MAT)@#LL8_a|x55dg(s4)~G{Z6;GI|afJzS*}{csCxo5LA-6*Jw_Sg42p<9nZittGH1}1V~zUrR|uvWTS4y z@@aJ-5gbg! zHsL3}E~Z%ltM4;j&}o^?p`V`qLtZ?%Kjpb6qaL%{NCPGIRwHbJP8sJxDae4toEjzl@oIWYZ9;G55;+0 zp8XMhEfZ1{pCnGvU0N#noY&ZFl>O(4Yd+7mUn)Lq%@VDDpW<=OYEMOR^^EBg?>m>O z&nnm*`TTg-zWk;T+WsQs%}{R(DXVba>@VrzM>g0_M6{H-x*VT_^So$;MP)at>!n#a zghhVLU3iqc@Gy7bLGHqRu69hyx*DTl3Dn7LD#~A*dY0qg;~4VEeDTiX+=U16&=|`CfJ$#b+J@fvD zNHHWQ0_<`8xdl@l78#9-iCHvOU)|v@fA}Et``yg%nX~X9Gx`UQGrwo({lPu`_xqCL z`ba>MA%RBTR1mWEKEbwp>KwEAzLhkRnpl~aXSV+SLw=Ztw73vlRq-A8Tv_$I2-<1i z%xv)e`@*Fb%E9Tvkp&{I`)t{Y_q^ws^!*G~-M_;nxLp`E`bbRLaAT^`FIRI%U)s=MdpAr2g3-zvTmh0mxW>ym&8~!~|asL3Jl#S#lsFRc#7bgg<)$+Q#_nb4Mi*`L}-y{gBv z6|GMT=!6fAWz%Y-;FG705a(cH)~{!@3N>r%8vIC3+taGX5;F;NC_M#m7U**$_ta#0 zw^xj^QUjoDPtdU&Uw%DZi6o1K+EUi`Rk1fnIzaB(2V6YPH)xh8w48fd=v^+gWK&mb zaX>=)1M_jF5w@85N2Ek+Y;Mc+QQnw*lE1X$sr!5%y)LKiBh9fj;|<~hRZ=xRH9hdz z2FuH{U%V&IeKn7Xvz%W{n!2$mYnVAR8jE~ge$|betB8I%iP!$~l*TiatmN>IQh=6t zYW+!PWu~XXY=>jHENx_RDbz+jKMW{x+zNKxvScv_c0+3u!HLOeH9^0jszUg^xYrfX zYIhRXq~;vp0dAViA6x^9!gH7Jr_9Vx11<2XrLAZ7;dVKEd+yZXQ@8w(a^Uqlz0M$C z4lY;Nvp)6SAH=(+@O?vbg{PQuH(GDr5V5Hy>bt5eea zNC`=~Dcvr9ZUSsiayFw!YY-&z-D=+4cjBfH|H`kXD>9h2<-Fkmt6zy%1<||jlNzn? zwWS$@D^ai>@>WCk9D}4AM`WNc(z6DdpvyUzl`aB}jxaJTFXrFqHmjI>&g?rFEJ8WL zyEYTk$|bZ(6F6C?g_Y~I2vZz!wYsX0URmIR7nGhC1*JzrLFr%Y)4ZVcsxK&Q4Q#G) zx^kwh^m51*c%R-}i0_@-{k<#xd*!%m+I9N1r{Qa#hOb>7Zk69Ep6^>2zQi!(#ISE2 z{Vsg#z4)!+)-!(Vy8jmDtpB%t-+MoN?;~3{%|H5@Ds_L#8%;+ngOsPu>@TZh$_$V@ z9B!AQB+FTsW(q3bXvq4rsa|@j@VYgVYVFdgzE|(?pBTBHTwGtPbP9D^E|Y^CIgg%~ zkp>To#A_I_ApN^;DBL=&tnqki$s<8G3vPN^Ipcy-i?%YyGH~Wo5&iq9o>aDd0~+0W zv5icUs%*yU0pF&xl`4SnD>U=DndbZobI>ugFoKsR?tWTG;_~pn$nTSL`%v^cWULas zj+d_~dKY@YXtL^{5HasR*^dA&7*HArvTHv5J72jMU2($i4d)Up>ePMe1&6Km3;8}Y zW29}o=auW84oj?1N^5w=%T2mx<%(e^XgRZ5hGq;<+z~@}S{D1BEN9AIb)~NCD$xXk zDv`_H@kFCrKVA>by<16hFz*)cbgQyQIVH!@pa>`dQE3je=I4ykwMGLK`!1iRZh=w%fM!K$F^$K?&TwuD$sV5+ey@y0#0>hj zH6m$K1d|jvDIfV6z$U(uyS?)k?~UI17Uw-qZb^bVDY?0jk$jL=%~}_1at9H_V>4}? zbCNm{tqW<_h%Ci*r;T)!lh{}A5>g@6ls)4e6!G7>*pqL5oosP>4>6%-#BR8W=qo-n zMXKUrg{-Op&?H`#pE|J7wwODZexmFmUfxvzEc5|h zC9WmMd!IZNM+`ckEd}%ib1RF4D7)QCD!o9A;oQpdpgnzZTp=^ThmthK>zw(hT#fZ4 zY1hF9bW5Q@R0*t#Oru?V5Hxn2`~J`dLulL1-Jum{xaz~OlA4_=f-h2ZdzF!)wGTah zvgN}JM6J-BPCn7eTW4e=Q#P4bTHLZ0H_NRY@o>X7A>#vZ9LMdo) zO{Lh!HXT%m1}k|EMUH1La(piqIW7-?<%jadLZl+u1E+v7v+C>g5PNLKcv86wgOy}y z<#3u(#A@thtW_^85e1nlCPv8Yl?5L*E@^u>@CRyaya1MCE80EC`UfQomBFLAc~C&y z3QR9P2%A<2oF801mI87*Wm?hH-Yh(ih7^z5E7J zcV7)&DY4m6b!}y25#0)FVE!@<@(t!epL;u!dUv?TMcrCko+M>VtW_3LYLdL?>zI>p z*?fUb!ZIfaq{$XE+!07=R5EKLvSLHPQ$eS(F-Uwq;~Tlh-{jkknbN1ekO`?^Kft)Q zum)HZ%vnMRsOUYhhSn6kjGRi3MP=$K_lE>W>mIzc&{ZuCm{m4%G*yS_$lRF?oCLg5 zy)M{xLHS2-1VjDhGBe}~5|sV~N<4afA4{c5coqDk^4+A5rMrRE01rR2bZwtmO1sY` zy-^5>B;VMN#`>VpP^ALplCtNqO*H?3p1~YsrxSA&2Z;icW&H-!DfC#JJBl)zz|WGr zk{#ZR6nKgEc;(~L65*XOkQwovLd5l45<#}fE_J2sN+x(BN|6H_NL1R>3iFB z3;4!AKCgR$tlZDyUSJU~$5SsB7vo~n@m(15<$fxpDU6tH*bW8_`itJdP+WJ&CBrLz z?-Kb~twP)U*-X$VykOr~EjLSSHrQK{VNvSbmR=d|906@r{g%~u4j&l}H~#Y0giACvV}*+j{}|L8eV8u={Uf{L!inV310ytBmPJJ<=YDiFEcN41^{F zR&y{91kTZb?r`_HB*8t&a#>k{Fbjw9Pm~qkQ{E<;dgg<`vVm0E5l;iIBC4Bj%W6AZH5$J!XWfDWX>YPv{Jx_1Twayo#`PK6eex&Jim|?6O3hNE>G%>?bsuWKJ%KE)A zz8yYg+K>ThA{RkgG)V+W+mzJ15-+8?@zAJ;6mDzI6I3_%cdbobxdb~q2hJIZSV^AD zRz*|8lF7r6;w!k#Cc4u|7*^p-!BlkGuHG;}5!TKK2x6D+XuB+@6>tOu0F=$&cpHJ# z?#U-PvN8-E0jTLkn4jo1dz3cLRMbp@G64G zO-UxrRPvHLhO|u9#zhNSU^+2t8HJejzcgR{OCY!^c>Myu^@!dx0~&>E)164IMS|4& z&Srfk5FDp`9MZPVp0@SA&y}((c*xs2#owkI#SX0L3q0o5tE)BnIE#mwR+wWMc}P@Ot>rJ8wT9`r~u* zp$m4+UMT}QN*U1Ci~%`mMVtavH$s8EKVI{Z1A@dMhe*wH2z?HUkQdFDRng4L{$co+ ziu1c>!OtpNoH&njrtH)E!dN;C&%oKXnok_y|Gt0!0$wua zP>f6M0*5o;Gg-7600A%Sa?rd^&Vt~^tcRQ!YWFh?o?Gd-Fa^uG(rEKMUc8m>R=QcK zU9zap0Bv;59p!s#_#uCuPKItS4N6rubk!ZhBK1gL*pGydYH$)VKmziOkpG%-8cdoJ zb2|}@TAXl~wwrC2qQuZ>ffC6~qOlv4W^5>0ez8blU=cjc`Z=k$EI@QnbFRzQZ#)aAYJztXg?nWR6%7}IO5k1!HwQxj{EW*GC z$me}9S9rYA>~j(dY)xHFbe{bXYeGo8aop55dt1AgmgUH4sn};pD;=Zxl z8jY#kS}WegOmDc?emrPO z`gD!ypS#h|9n}tg2aYI9PmlurunF!}$|haij7Zm_+Awzf8jxbB!&so?e35Vg@m3)UeJR1ZAfX_)DHgC2da0K?s`9XUQVc$atb0 zBWoSpGT%_*eJYAMlyE6hu9!cwhe$1qQkG2!;9&K>Y{zGd7k@B-4+bz(_BOqqTdTvQ zsR*~%C2Li3NMOJi;PLgJ5TCx(;Sa|KFOO$eN;Xc=(F7ZdwJYt#wf|iU^5R_0yj0-adYt=O? zY@7KyGiNxK{81hQ-Il%!4D=5?2D%$6$92CDu+Ogs{`0Q8?JeySMQbr!42+RR+91@O z>Ow1K^v^KS;FTYxN#_)2m};h)d8e6UL-^30mJ}g4KAu)Lgpg9=RHAUt6)5i%3tKBUEX1|*)+(9_xP9MM@CZ@8&gVO9u%U`0oeGmtZG_~mY|g~5};<34sr|} z6Z3X;`)EM)-qD2eNg>kVw@RxMwUcdxz4x7QSf&X7$vfV@Phyd8ghb@ zf~UF5QFkf>$Dx&B>8EJcP0Om=7FxJC?dTSYXS`WW`$L*(HK}AO{J3dpZ&=|Tu)s(P z%A(J+ccP6|{<_u>!qHK>+ytsHH4X`t`Jbt+#SjFr^C^>r7umb)BgzBGPar^iCS=)m zQ5gK;Pk;T}h2jRe(oL-Q%wGPN|Mh<_Uno8X%?gc}A*bUnwrCeL9%SI+UDv|T^hG~; z307No{V5^@k%xB3s-yq@KMNs@LfzoP^?OKxcHsPHGISq35gr`)6ZMVQ1dw`S0#_Eg zqkj0)PU+pMLdTRpd^9aML}6Gp?i#Gi8QRM=SW*JH+P5A_;K4B(Y!z>cWQqcVeynI^8<#J3hrqnOQrC4>9 z_~S?^c~b9dnUYeaY1hVjnmAV^wu}$Xc6_;OXXcVk{!lxxu=cXf+@L1FIUFQc*1AA2y@bkCGYalCZsrT9mAAvi+oIi_Kd~^A;uJFlk-@4 z6c}(^d}7X{ctT+;=nwM@d!9_Fd30E!84DMm<|?Kn+zMgjS&x*cTBgnI*ipTnoG$;7 z&yzFEJb;9}Em-xkT;;rdF_*hrIQK>#X)xh6G=~YKiZ%n<2BO?L$rc*H@xy7*+fmC_ zGq5i_vzwwuxH|1D=_6OMI+kZE%yBbm%F)=AE0J8T^dYgk6TAtS0m8X#q{gA*TWoK@ z-QFe3ZR;5niwjr2L)#l!_q0RF<8d7RUcNX(5rrOmt4lDGMs@jYQ&&Q4wB~eo0*XtT z1M8uzx1Gev97sKF1_9d%3xyK(msVcA0}6GQ;rwxt<6#MV*Zdp;k1Fs0kM>0YU3N67 zA<9=CG?enn+TO!@ATn4u-=3^=R#tf|#0nFOno@BmqmU{%;i(g!Vnvh~d`Zh$38e-@ zAB^QqFQTvUyTVg=Go99<{={UA>G+3@+_H8EPQ)z%9YMN`at! z`i`OqxpeRt!Jsw03sd*IgZl83S?558d_UR@`yFIOS|l1$N}R1osB#8Mg6-{=P1=vi zNW$x4eXt~EOYKM>*2$C=J1yC6iJn{FLkzhlmtETs#v5?gbA zIVA&Sa9v0cBR4I6J1#(`b1l%+*rdKHT6OE~FA6Sb2cP7M#RzQ3ktQh}{YvB;d7TBt zed7C>qNC4Db526*or57vMQNO8gO2d8i@l18BWbrymrlvog;XifQP9j6QwAJNRxM!t z4VjU9J>Fr4eYyLDNTEqO40A6FkxUwHUf!%vyeYTD8Z&5u8_6`{l8Vj%X%J0jfF*90 z)q0X!GqBf(OpD*=CX;oZQ5bH~af0}Mso(6>7+6jiC#d#s#9DnbVcvb^dY@@au(dGi zqGS)${s^LR!JUEg7FOXsim~wSx|u#hxoB%=+y-7;ZYRciRnLJahh5%Gr`wT8J^TnC zAE7$res~>lV??<4${+5t&2>(_FT7NoqWFvL5t3e5%6(d6k@!P+h3e<{B|?C5<0t|bf%n>Dw$+vft?NJeEB%Qf@E=g0jdz<(et|?R{Ef!Z*7;GQbh!&k43XVmH zDiHmayWO6doL<(zX%o^Y0RvV%Q^E{LG)@W!ZM%M_aPGDe-E2j%lTYSdS99r@rirf} zWQeJAgxGvO9}{xKut_bkB&T>t;1y1528h@m!)Co1f8|@!w2V0AVQiQnCLDI57{FVv z)Fl^`iBL+C_A{F7svmzl#p!G7gT)GL1_b(20#rK%m7N6oSr=Q=Pv)xWjfT)4)5@9k zlD5uF$_!Z-(cu223h}Br9Vb1V!)f-0E(vKz#*@|EL^tV!GxqGx*vsDiiA^pKj@W}E zR)JYoOLnUIL@|Gf)tR&k;n7&FEz%uP;ljJYW#BP(_X`1GU$z*|%?5%(-{FXtmvN)q z<(Itv=F$h=h}e4f6++CsgbqNXy%LTu^`51vWD%d}p70+=xf2R+XekEps;Mhi{6_4* zTHMY-yOS(mUsnK|Bf^TLNl}1M_?Xe-u~mLq-wIT(U87sto6*RA)2%V?#y23J&i{<^ z>FCu$#a|>@{>=iIR!61|reMjJ}O3v5!2_M$I-yHNnyLfN5{ks(nj9)5wx9 zHr?^Q3fCVW79s=CuH4e%R{i2MftOe}iE_3!KFyn(^2<4RDC%k$DYt?4U zEZp~4gC@5GviOBeCfHiSt4cFhCyUmM5NN=jPadD6M^;2a$-HaZ?N}gD(<&VgHU2#H zq>mm*XS!?@kqZRfEz(qB40GrO84`OvZP1w@pc5fQ+_Ezl3*Z}Oy?|a5ps0$l`Q9|& zbi%P%K5anPt8>XIX!zp>I=e@!9QcvjLrJ)0(l%Y^I@r^kbB|6CRUR_6u`vtKQ^k)Z zhG9ja1O-uItexTSa7yLeIV3OnPFoy@0prvZ(uh1KoZFPJd3O{(3;R!52pZNx92Q#I zUDbP`NU8Z+P94I+4M7?@u%dCxMIjLOBRI*rFNG_r%QtO^ZU5 zXRTW^aXrdhj=w){g-qz_F*N*3%O!UZMK9g_j}|T^nfBgG@7T8}#FDrGqNJjh=jkhD zUw6O|YN%%1UQwE{&8iy1MoSAx!nzf?B1s)v%$cy*<-k>K(AvFrh&H5sSPwCTv&RrV z@Sc0OgoiM~A&j8=_wPRDM;K+i!{v=mTGo6w?N%uQe$fack5zEuyUG@KnQ5DphJ|E_ zTxw?28z;5J;Nv9Hms2qE%m7SGuboz1kCQoppG0;Z>3^K0^ICt*f$h9_H$%IE2d;DA zI$Bw5ido?GT7Q8VA8;sa@1(Y@M3#cfXg-|M7eA$T1rMk6a7tO0K+MtS7@k+AJ+*uU+XtLbt2mVPZ;ll2+n8a|OF=8!Ol+u$UAz+a!kFyidT_opPQc%eX6 z3&bovOGAxKyV}_sDX!|z4C$?flHN8*@0SU&ey$Ryr5*u2j9u?+{nXHuh?AvFJm5+( zO;&JXS1CIH)vUEamrk(1p#ttr5W%^##V>ae!9VXJf-?}>$Nn3zr?X9$ge*S_pScu2 z1M;UcrTol0&o>nfDfFY~MGAd0fF~~}4W7f#f3HXjHzSv&Rofhy$T#oed7)14boD7g zqr6TT_0kNW)G0*hY1!_GFDUhM`YV!dcb9BMSoh*h?mnT*SWd^T&WqyD#f}|Z*(Q2Q zO!*~`$%t8~!xmj7FtI^$5UK&EHBe50Vv1Egi_-o#>8?IcJ-~cXc0GuC&8D zUq1?Q@-Dm`;UQM~2W0Udbndgdk^;_0)>|MguB!PuvV+s-#Uny)2@hVMgV#q(ehX!S zT6Mr9E?HcbwGogw(`I#Sc)OBMsho=%^F#eBcSS0vw+crny?2!M@9AY5UIIgYjlFCc zZM03Z3<*$^hdkTeTcG<%?sIAKC^U_@v{L8lQT? z2c^Pd@Em1a+`5`9j6hn6c{;3Sm=`onLZ3I5hC@Iss6K7|Rfm)R;(;-{f`_AjIQp5S znR_B%q_mwOLjnr*%5LUmi!KXsbR7zSue@jN895M!{Ekg@4X`Y%EVyZ{`rsj8S*L9U z7dilc(+*Zmb6mY9=kd=H2RTcq!$&pX$6w2p6IsG>=_I7kQ1mR~A)8e!5lH$*u!h`2 zDFIF5b-_-K3}-WGK`Y(7nWC^)8I8>O49<_6Q{?zrKIS~8&%dR3xSB>ZaUgIXW(d$|xm z+Nd&x{$8UF*DOa%vmCbxNT^4|w8fbGg-cU6FJjtXOJ4DM4SR|y+)O?H7`jA%BWP=1 z!_QOf8}I_t{z$yyHO`oTHg;8Q?UFsQYC1osr}&WON0j&~S%hO3jFoS(6e8IOJ-}Mf zG6*X*N49sNrh~=8v4?!CJf^FXk|e8WV@@+WN3H0}OHnWaT7A+pjs(=aFIy!R3VbGU z`hwt6+7UnE3XGkhZ9SuXgLLL;tMR_=)%pPbSmaQ#X0lhGOQ{aIRiBCd=k@Y5|I^pJ z#eLQC)BOqSVMoJh#rq5b1)M&_Jg2j_dfw&KR z%X;fki#YweIbpBHpkmFMyA@%Y;?W3Pz=KQj;F9DDvN5y&NNBed z7pQ4NLr@1KLzO0vml6FS6k@>zx`&XdqDS8^QgoRE?|O0Gg$sD#T?gL90}gKvr}GtZ zuN@Z6KKIQPIrr{0IdJx}QReZSs-@F|55Ply(4veGV{Y4J* zi+cfxT6tog=DH^VSKUmI?L=?51fwO*LllSkr0jD(dLZA)!nad7yRYsu{Y7}oYjoSu z4IGN|k}5ju)o}R)6k!KmX#dMKb|6a;%rm+_w4uNOAM-` Date: Thu, 20 Mar 2025 16:26:10 +0100 Subject: [PATCH 13/86] Initial 1.21.5 changes --- .../kotlin/geyser.base-conventions.gradle.kts | 1 + .../geyser/entity/EntityDefinitions.java | 9 +- .../entity/type/AreaEffectCloudEntity.java | 4 +- .../geyser/entity/type/LivingEntity.java | 30 +- .../geyser/entity/type/ThrowableEntity.java | 4 +- .../entity/type/living/animal/PigEntity.java | 8 + .../type/living/animal/StriderEntity.java | 4 - .../animal/horse/AbstractHorseEntity.java | 12 +- .../geyser/inventory/MerchantContainer.java | 10 +- .../inventory/item/GeyserInstrument.java | 24 +- .../java/org/geysermc/geyser/item/Items.java | 15 +- .../geysermc/geyser/item/type/BannerItem.java | 3 +- .../geyser/item/type/GoatHornItem.java | 18 +- .../org/geysermc/geyser/item/type/Item.java | 24 +- .../geysermc/geyser/level/block/Blocks.java | 44 +- .../level/block/property/Properties.java | 5 +- .../geyser/network/netty/LocalSession.java | 3 +- .../populator/BlockRegistryPopulator.java | 5 +- .../DataComponentRegistryPopulator.java | 2 + .../{ => conversion}/Conversion766_748.java | 23 +- .../{ => conversion}/Conversion776_766.java | 6 +- .../conversion/Conversion786_776.java | 60 + .../conversion/ConversionHelper.java | 47 + .../geyser/session/cache/tags/BlockTag.java | 7 +- .../geyser/session/cache/tags/ItemTag.java | 3 + .../translator/item/ItemTranslator.java | 14 +- ...BedrockInventoryTransactionTranslator.java | 53 +- .../{spawn => }/JavaAddEntityTranslator.java | 6 +- .../spawn/JavaAddExperienceOrbTranslator.java | 49 - .../JavaMerchantOffersTranslator.java | 35 +- .../java/level/JavaGameEventTranslator.java | 24 +- .../geyser/util/StructureBlockUtils.java | 3 +- .../resources/java/item_data_components.json | 14620 ++++++++++------ .../network/ScoreboardIssueTests.java | 174 +- gradle/libs.versions.toml | 2 +- 35 files changed, 9197 insertions(+), 6154 deletions(-) rename core/src/main/java/org/geysermc/geyser/registry/populator/{ => conversion}/Conversion766_748.java (87%) rename core/src/main/java/org/geysermc/geyser/registry/populator/{ => conversion}/Conversion776_766.java (94%) create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion786_776.java create mode 100644 core/src/main/java/org/geysermc/geyser/registry/populator/conversion/ConversionHelper.java rename core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/{spawn => }/JavaAddEntityTranslator.java (97%) delete mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddExperienceOrbTranslator.java 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 09440ac6a..93b4d8c13 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -70,4 +70,5 @@ repositories { content { includeGroupByRegex("com\\.github\\..*") } } + mavenLocal() } 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 757c126b9..baf6a9990 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -473,8 +473,9 @@ public final class EntityDefinitions { .heightAndWidth(0.25f) .identifier("minecraft:xp_bottle") .build(); + // TODO 1.21.5 lingering potion POTION = EntityDefinition.inherited(ThrownPotionEntity::new, throwableItemBase) - .type(EntityType.POTION) + .type(EntityType.SPLASH_POTION) .heightAndWidth(0.25f) .identifier("minecraft:splash_potion") .build(); @@ -960,6 +961,7 @@ public final class EntityDefinitions { .type(EntityType.CHICKEN) .height(0.7f).width(0.4f) .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant) .build(); COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) .type(EntityType.COW) @@ -1016,8 +1018,8 @@ public final class EntityDefinitions { .type(EntityType.PIG) .heightAndWidth(0.9f) .properties(VanillaEntityProperties.CLIMATE_VARIANT) - .addTranslator(MetadataTypes.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataTypes.INT, PigEntity::setBoost) + .addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant) .build(); POLAR_BEAR = EntityDefinition.inherited(PolarBearEntity::new, ageableEntityBase) .type(EntityType.POLAR_BEAR) @@ -1045,7 +1047,6 @@ public final class EntityDefinitions { .height(1.7f).width(0.9f) .addTranslator(MetadataTypes.INT, StriderEntity::setBoost) .addTranslator(MetadataTypes.BOOLEAN, StriderEntity::setCold) - .addTranslator(MetadataTypes.BOOLEAN, StriderEntity::setSaddled) .build(); TURTLE = EntityDefinition.inherited(TurtleEntity::new, ageableEntityBase) .type(EntityType.TURTLE) @@ -1144,7 +1145,7 @@ public final class EntityDefinitions { EntityDefinition tameableEntityBase = EntityDefinition.inherited(null, ageableEntityBase) // No factory, is abstract .addTranslator(MetadataTypes.BYTE, TameableEntity::setTameableFlags) - .addTranslator(MetadataTypes.OPTIONAL_UUID, TameableEntity::setOwner) + .addTranslator(MetadataTypes.OPTIONAL_LIVING_ENTITY_REFERENCE, TameableEntity::setOwner) .build(); CAT = EntityDefinition.inherited(CatEntity::new, tameableEntityBase) .type(EntityType.CAT) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java index 165495506..546f66700 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/AreaEffectCloudEntity.java @@ -35,7 +35,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.level.particle.EntityEffectParticleData; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import java.util.UUID; @@ -72,7 +72,7 @@ public class AreaEffectCloudEntity extends Entity { Registries.PARTICLES.map(particle.getType(), p -> p.levelEventType() instanceof ParticleType particleType ? particleType : null).ifPresent(type -> dirtyMetadata.put(EntityDataTypes.AREA_EFFECT_CLOUD_PARTICLE, type)); - if (particle.getData() instanceof EntityEffectParticleData effectParticleData) { + if (particle.getData() instanceof ColorParticleData effectParticleData) { dirtyMetadata.put(EntityDataTypes.EFFECT_COLOR, effectParticleData.getColor()); } } 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 8c1ab80f0..70ef2300e 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 @@ -62,7 +62,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.Object import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; -import org.geysermc.mcprotocollib.protocol.data.game.level.particle.EntityEffectParticleData; +import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; @@ -80,6 +80,7 @@ public class LivingEntity extends Entity { protected ItemData leggings = ItemData.AIR; protected ItemData boots = ItemData.AIR; protected ItemData body = ItemData.AIR; + protected ItemData saddle = ItemData.AIR; protected ItemData hand = ItemData.AIR; protected ItemData offhand = ItemData.AIR; @@ -118,10 +119,6 @@ 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); } @@ -130,6 +127,15 @@ public class LivingEntity extends Entity { this.boots = ItemTranslator.translateToBedrock(session, stack); } + public void setBody(ItemStack stack) { + this.body = ItemTranslator.translateToBedrock(session, stack); + } + + public void setSaddle(ItemStack stack) { + this.saddle = ItemTranslator.translateToBedrock(session, stack); + updateSaddled(stack.getId() == Items.SADDLE.javaId()); + } + public void setHand(ItemStack stack) { this.hand = ItemTranslator.translateToBedrock(session, stack); } @@ -138,6 +144,18 @@ public class LivingEntity extends Entity { this.offhand = ItemTranslator.translateToBedrock(session, stack); } + protected void updateSaddled(boolean saddled) { + setFlag(EntityFlag.SADDLED, saddled); + updateBedrockMetadata(); + + // Update the interactive tag, if necessary + // TODO 1.21.5 retest + Entity mouseoverEntity = session.getMouseoverEntity(); + if (mouseoverEntity != null && mouseoverEntity.getEntityId() == entityId) { + mouseoverEntity.updateInteractiveTag(); + } + } + public void switchHands() { ItemData offhand = this.offhand; this.offhand = this.hand; @@ -202,7 +220,7 @@ public class LivingEntity extends Entity { continue; } - int color = ((EntityEffectParticleData) particle.getData()).getColor(); + int color = ((ColorParticleData) particle.getData()).getColor(); r += ((color >> 16) & 0xFF) / 255f; g += ((color >> 8) & 0xFF) / 255f; b += ((color) & 0xFF) / 255f; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java index 25bbdbd3c..85abc1c40 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java @@ -120,7 +120,7 @@ public class ThrowableEntity extends Entity implements Tickable { protected float getGravity() { if (getFlag(EntityFlag.HAS_GRAVITY)) { switch (definition.entityType()) { - case POTION: + case LINGERING_POTION, SPLASH_POTION: return 0.05f; case EXPERIENCE_BOTTLE: return 0.07f; @@ -146,7 +146,7 @@ public class ThrowableEntity extends Entity implements Tickable { return 0.8f; } else { switch (definition.entityType()) { - case POTION: + case LINGERING_POTION, SPLASH_POTION: case EXPERIENCE_BOTTLE: case SNOWBALL: case EGG: 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 d4227cfd9..fb7ef3357 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 @@ -48,6 +48,10 @@ import org.geysermc.geyser.session.cache.tags.Tag; 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.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.PigVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -155,4 +159,8 @@ public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { public boolean isClientControlled() { return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK); } + + public void setVariant(EntityMetadata,? extends MetadataType>> holderEntityMetadata) { + // TODO + } } 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 62318e255..236b22c51 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 @@ -69,10 +69,6 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic isCold = entityMetadata.getPrimitiveValue(); } - public void setSaddled(BooleanEntityMetadata entityMetadata) { - setFlag(EntityFlag.SADDLED, entityMetadata.getPrimitiveValue()); - } - @Override public void updateBedrockMetadata() { // Make sure they are not shaking when riding another entity diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java index 100a29299..80995be1c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/AbstractHorseEntity.java @@ -79,12 +79,19 @@ public class AbstractHorseEntity extends AnimalEntity { session.sendUpstreamPacket(attributesPacket); } + @Override + public void updateSaddled(boolean saddled) { + // Shows the jump meter + setFlag(EntityFlag.CAN_POWER_JUMP, saddled); + super.updateSaddled(saddled); + } + + // TODO 1.21.5 saddled flag doesnt exist anymore public void setHorseFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); boolean tamed = (xd & 0x02) == 0x02; boolean saddled = (xd & 0x04) == 0x04; setFlag(EntityFlag.TAMED, tamed); - setFlag(EntityFlag.SADDLED, saddled); setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10); setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20); @@ -114,9 +121,6 @@ public class AbstractHorseEntity extends AnimalEntity { // Set container type if tamed dirtyMetadata.put(EntityDataTypes.CONTAINER_TYPE, tamed ? (byte) ContainerType.HORSE.getId() : (byte) 0); - - // Shows the jump meter - setFlag(EntityFlag.CAN_POWER_JUMP, saddled); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java index 0bfa6d1a7..631b73936 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/MerchantContainer.java @@ -34,11 +34,13 @@ import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.VillagerTrade; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundMerchantOffersPacket; +import java.util.List; + public class MerchantContainer extends Container { @Getter @Setter private Entity villager; @Setter - private VillagerTrade[] villagerTrades; + private List villagerTrades; @Getter @Setter private ClientboundMerchantOffersPacket pendingOffersPacket; @Getter @Setter @@ -49,9 +51,9 @@ public class MerchantContainer extends Container { } public void onTradeSelected(GeyserSession session, int slot) { - if (villagerTrades != null && slot >= 0 && slot < villagerTrades.length) { - VillagerTrade trade = villagerTrades[slot]; - setItem(2, GeyserItemStack.from(trade.getOutput()), session); + if (villagerTrades != null && slot >= 0 && slot < villagerTrades.size()) { + VillagerTrade trade = villagerTrades.get(slot); + setItem(2, GeyserItemStack.from(trade.getResult()), session); tradeExperience += trade.getXp(); villager.getDirtyMetadata().put(EntityDataTypes.TRADE_EXPERIENCE, tradeExperience); diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java index 9983a8e90..38d4f2cd5 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java @@ -34,8 +34,7 @@ import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.geyser.util.SoundUtils; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.Instrument; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import java.util.Locale; @@ -90,34 +89,35 @@ public interface GeyserInstrument { return -1; } - static GeyserInstrument fromHolder(GeyserSession session, Holder holder) { - if (holder.isId()) { - return session.getRegistryCache().instruments().byId(holder.id()); + // TODO 1.21.5 + static GeyserInstrument fromComponent(GeyserSession session, InstrumentComponent component) { + if (component.instrumentHolder().isId()) { + return session.getRegistryCache().instruments().byId(component.instrumentHolder().id()); } - Instrument custom = holder.custom(); + InstrumentComponent.Instrument custom = component.instrumentHolder().custom(); return new Wrapper(custom, session.locale()); } - record Wrapper(Instrument instrument, String locale) implements GeyserInstrument { + record Wrapper(InstrumentComponent.Instrument instrument, String locale) implements GeyserInstrument { @Override public String soundEvent() { - return instrument.getSoundEvent().getName(); + return instrument.soundEvent().getName(); } @Override public float range() { - return instrument.getRange(); + return instrument.range(); } @Override public String description() { - return MessageTranslator.convertMessageForTooltip(instrument.getDescription(), locale); + return MessageTranslator.convertMessageForTooltip(instrument.description(), locale); } @Override public BedrockInstrument bedrockInstrument() { - if (instrument.getSoundEvent() instanceof BuiltinSound) { - return BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.getSoundEvent().getName())); + if (instrument.soundEvent() instanceof BuiltinSound) { + return BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.soundEvent().getName())); } // Probably custom return null; diff --git a/core/src/main/java/org/geysermc/geyser/item/Items.java b/core/src/main/java/org/geysermc/geyser/item/Items.java index 664c956c3..5fd64c4d4 100644 --- a/core/src/main/java/org/geysermc/geyser/item/Items.java +++ b/core/src/main/java/org/geysermc/geyser/item/Items.java @@ -270,9 +270,13 @@ public final class Items { public static final Item COBWEB = register(new BlockItem(builder(), Blocks.COBWEB)); public static final Item SHORT_GRASS = register(new BlockItem(builder(), Blocks.SHORT_GRASS)); public static final Item FERN = register(new BlockItem(builder(), Blocks.FERN)); + public static final Item BUSH = register(new BlockItem(builder(), Blocks.BUSH)); public static final Item AZALEA = register(new BlockItem(builder(), Blocks.AZALEA)); public static final Item FLOWERING_AZALEA = register(new BlockItem(builder(), Blocks.FLOWERING_AZALEA)); public static final Item DEAD_BUSH = register(new BlockItem(builder(), Blocks.DEAD_BUSH)); + public static final Item FIREFLY_BUSH = register(new BlockItem(builder(), Blocks.FIREFLY_BUSH)); + public static final Item SHORT_DRY_GRASS = register(new BlockItem(builder(), Blocks.SHORT_DRY_GRASS)); + public static final Item TALL_DRY_GRASS = register(new BlockItem(builder(), Blocks.TALL_DRY_GRASS)); public static final Item SEAGRASS = register(new BlockItem(builder(), Blocks.SEAGRASS)); public static final Item SEA_PICKLE = register(new BlockItem(builder(), Blocks.SEA_PICKLE)); public static final Item WHITE_WOOL = register(new BlockItem(builder(), Blocks.WHITE_WOOL)); @@ -321,6 +325,8 @@ public final class Items { public static final Item SUGAR_CANE = register(new BlockItem(builder(), Blocks.SUGAR_CANE)); public static final Item KELP = register(new BlockItem(builder(), Blocks.KELP)); public static final Item PINK_PETALS = register(new BlockItem(builder(), Blocks.PINK_PETALS)); + public static final Item WILDFLOWERS = register(new BlockItem(builder(), Blocks.WILDFLOWERS)); + public static final Item LEAF_LITTER = register(new BlockItem(builder(), Blocks.LEAF_LITTER)); public static final Item MOSS_CARPET = register(new BlockItem(builder(), Blocks.MOSS_CARPET)); public static final Item MOSS_BLOCK = register(new BlockItem(builder(), Blocks.MOSS_BLOCK)); public static final Item PALE_MOSS_CARPET = register(new BlockItem(builder(), Blocks.PALE_MOSS_CARPET)); @@ -389,6 +395,7 @@ public final class Items { public static final Item ICE = register(new BlockItem(builder(), Blocks.ICE)); public static final Item SNOW_BLOCK = register(new BlockItem(builder(), Blocks.SNOW_BLOCK)); public static final Item CACTUS = register(new BlockItem(builder(), Blocks.CACTUS)); + public static final Item CACTUS_FLOWER = register(new BlockItem(builder(), Blocks.CACTUS_FLOWER)); public static final Item CLAY = register(new BlockItem(builder(), Blocks.CLAY)); public static final Item JUKEBOX = register(new BlockItem(builder(), Blocks.JUKEBOX)); public static final Item OAK_FENCE = register(new BlockItem(builder(), Blocks.OAK_FENCE)); @@ -891,6 +898,8 @@ public final class Items { public static final Item BAMBOO_CHEST_RAFT = register(new BoatItem("bamboo_chest_raft", builder())); public static final Item STRUCTURE_BLOCK = register(new BlockItem(builder(), Blocks.STRUCTURE_BLOCK)); public static final Item JIGSAW = register(new BlockItem(builder(), Blocks.JIGSAW)); + public static final Item TEST_BLOCK = register(new BlockItem(builder(), Blocks.TEST_BLOCK)); + public static final Item TEST_INSTANCE_BLOCK = register(new BlockItem(builder(), Blocks.TEST_INSTANCE_BLOCK)); public static final Item TURTLE_HELMET = register(new ArmorItem("turtle_helmet", builder())); public static final Item TURTLE_SCUTE = register(new Item("turtle_scute", builder())); public static final Item ARMADILLO_SCUTE = register(new Item("armadillo_scute", builder())); @@ -1027,6 +1036,8 @@ public final class Items { public static final Item BOOK = register(new Item("book", builder())); public static final Item SLIME_BALL = register(new Item("slime_ball", builder())); public static final Item EGG = register(new Item("egg", builder())); + public static final Item BLUE_EGG = register(new Item("blue_egg", builder())); + public static final Item BROWN_EGG = register(new Item("brown_egg", builder())); public static final Item COMPASS = register(new CompassItem("compass", builder())); public static final Item RECOVERY_COMPASS = register(new Item("recovery_compass", builder())); public static final Item BUNDLE = register(new Item("bundle", builder())); @@ -1120,7 +1131,7 @@ public final class Items { public static final Item BLAZE_POWDER = register(new Item("blaze_powder", builder())); public static final Item MAGMA_CREAM = register(new Item("magma_cream", builder())); public static final Item BREWING_STAND = register(new BlockItem(builder(), Blocks.BREWING_STAND)); - public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.LAVA_CAULDRON, Blocks.WATER_CAULDRON)); + public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.WATER_CAULDRON, Blocks.LAVA_CAULDRON)); public static final Item ENDER_EYE = register(new Item("ender_eye", builder())); public static final Item GLISTERING_MELON_SLICE = register(new Item("glistering_melon_slice", builder())); public static final Item ARMADILLO_SPAWN_EGG = register(new SpawnEggItem("armadillo_spawn_egg", builder())); @@ -1210,7 +1221,7 @@ public final class Items { public static final Item WRITABLE_BOOK = register(new WritableBookItem("writable_book", builder())); public static final Item WRITTEN_BOOK = register(new WrittenBookItem("written_book", builder())); public static final Item BREEZE_ROD = register(new Item("breeze_rod", builder())); - public static final Item MACE = register(new Item("mace", builder())); + public static final Item MACE = register(new Item("mace", builder().attackDamage(6.0))); public static final Item ITEM_FRAME = register(new Item("item_frame", builder())); public static final Item GLOW_ITEM_FRAME = register(new Item("glow_item_frame", builder())); public static final Item FLOWER_POT = register(new BlockItem(builder(), Blocks.FLOWER_POT)); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java index 2a5f76c33..f7229e202 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java @@ -46,7 +46,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; import java.util.ArrayList; import java.util.List; @@ -226,7 +225,7 @@ public class BannerItem extends BlockItem { } components.put(DataComponentTypes.BANNER_PATTERNS, patternLayers); - components.put(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP, Unit.INSTANCE); + // TODO 1.21.5 hide components??? components.put(DataComponentTypes.ITEM_NAME, Component .translatable("block.minecraft.ominous_banner") .style(Style.style(TextColor.color(16755200))) diff --git a/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java b/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java index bd1ac0724..e4173d2bb 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java @@ -36,7 +36,7 @@ import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.Instrument; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; public class GoatHornItem extends Item { public GoatHornItem(String javaIdentifier, Builder builder) { @@ -50,9 +50,9 @@ public class GoatHornItem extends Item { return builder; } - Holder holder = components.get(DataComponentTypes.INSTRUMENT); - if (holder != null) { - GeyserInstrument instrument = GeyserInstrument.fromHolder(session, holder); + InstrumentComponent instrumentComponent = components.get(DataComponentTypes.INSTRUMENT); + if (instrumentComponent != null) { + GeyserInstrument instrument = GeyserInstrument.fromComponent(session, instrumentComponent); int bedrockId = instrument.bedrockId(); if (bedrockId >= 0) { builder.damage(bedrockId); @@ -66,10 +66,10 @@ public class GoatHornItem extends Item { public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { super.translateComponentsToBedrock(session, components, builder); - Holder holder = components.get(DataComponentTypes.INSTRUMENT); - if (holder != null && components.get(DataComponentTypes.HIDE_TOOLTIP) == null - && components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) == null) { - GeyserInstrument instrument = GeyserInstrument.fromHolder(session, holder); + InstrumentComponent component = components.get(DataComponentTypes.INSTRUMENT); + // TODO 1.21.5 hiding???? + if (component != null) { + GeyserInstrument instrument = GeyserInstrument.fromComponent(session, component); if (instrument.bedrockInstrument() == null) { builder.getOrCreateLore().add(instrument.description()); } @@ -82,7 +82,7 @@ public class GoatHornItem extends Item { int damage = itemData.getDamage(); // This could cause an issue since -1 is returned for non-vanilla goat horns - itemStack.getOrCreateComponents().put(DataComponentTypes.INSTRUMENT, Holder.ofId(GeyserInstrument.bedrockIdToJava(session, damage))); + itemStack.getOrCreateComponents().put(DataComponentTypes.INSTRUMENT, new InstrumentComponent(Holder.ofId(GeyserInstrument.bedrockIdToJava(session, damage)), null)); return itemStack; } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index f0ae57018..2c3303689 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -46,12 +46,10 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.item.BedrockItemBuilder; -import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DyedItemColor; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; import org.jetbrains.annotations.UnmodifiableView; @@ -159,12 +157,13 @@ public class Item { */ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { List loreComponents = components.get(DataComponentTypes.LORE); - if (loreComponents != null && components.get(DataComponentTypes.HIDE_TOOLTIP) == null) { - List lore = builder.getOrCreateLore(); - for (Component loreComponent : loreComponents) { - lore.add(MessageTranslator.convertMessage(loreComponent, session.locale())); - } - } + // TODO 1.21.5 +// if (loreComponents != null && components.get(DataComponentTypes.HIDE_TOOLTIP) == null) { +// List lore = builder.getOrCreateLore(); +// for (Component loreComponent : loreComponents) { +// lore.add(MessageTranslator.convertMessage(loreComponent, session.locale())); +// } +// } Integer damage = components.get(DataComponentTypes.DAMAGE); if (damage != null) { @@ -266,10 +265,11 @@ public class Item { } protected final void translateDyedColor(DataComponents components, BedrockItemBuilder builder) { - DyedItemColor dyedItemColor = components.get(DataComponentTypes.DYED_COLOR); - if (dyedItemColor != null) { - builder.putInt("customColor", dyedItemColor.getRgb()); - } + // TODO 1.21.5 +// DyedItemColor dyedItemColor = components.get(DataComponentTypes.DYED_COLOR); +// if (dyedItemColor != null) { +// builder.putInt("customColor", dyedItemColor.getRgb()); +// } } /** diff --git a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java index 527e49b14..b90bacdd1 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/Blocks.java @@ -325,6 +325,9 @@ public final class Blocks { public static final Block SHORT_GRASS = register(new Block("short_grass", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block FERN = register(new Block("fern", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block DEAD_BUSH = register(new Block("dead_bush", builder().pushReaction(PistonBehavior.DESTROY))); + public static final Block BUSH = register(new Block("bush", builder().pushReaction(PistonBehavior.DESTROY))); + public static final Block SHORT_DRY_GRASS = register(new Block("short_dry_grass", builder().pushReaction(PistonBehavior.DESTROY))); + public static final Block TALL_DRY_GRASS = register(new Block("tall_dry_grass", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block SEAGRASS = register(new Block("seagrass", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block TALL_SEAGRASS = register(new Block("tall_seagrass", builder().pushReaction(PistonBehavior.DESTROY).pickItem(() -> Items.SEAGRASS) .enumState(DOUBLE_BLOCK_HALF))); @@ -399,8 +402,8 @@ public final class Blocks { public static final Block SOUL_FIRE = register(new Block("soul_fire", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block SPAWNER = register(new SpawnerBlock("spawner", builder().setBlockEntity(BlockEntityType.MOB_SPAWNER).requiresCorrectToolForDrops().destroyTime(5.0f))); public static final Block CREAKING_HEART = register(new Block("creaking_heart", builder().setBlockEntity(BlockEntityType.CREAKING_HEART).destroyTime(10.0f) - .booleanState(ACTIVE) .enumState(AXIS, Axis.VALUES) + .enumState(CREAKING_HEART_STATE) .booleanState(NATURAL))); public static final Block OAK_STAIRS = register(new Block("oak_stairs", builder().destroyTime(2.0f) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) @@ -630,7 +633,7 @@ public final class Blocks { public static final Block REDSTONE_WALL_TORCH = register(new Block("redstone_wall_torch", builder().pushReaction(PistonBehavior.DESTROY) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(LIT))); - public static final Block STONE_BUTTON = register(new ButtonBlock("stone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block STONE_BUTTON = register(new Block("stone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -640,6 +643,7 @@ public final class Blocks { public static final Block SNOW_BLOCK = register(new Block("snow_block", builder().requiresCorrectToolForDrops().destroyTime(0.2f))); public static final Block CACTUS = register(new Block("cactus", builder().destroyTime(0.4f).pushReaction(PistonBehavior.DESTROY) .intState(AGE_15))); + public static final Block CACTUS_FLOWER = register(new Block("cactus_flower", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block CLAY = register(new Block("clay", builder().destroyTime(0.6f))); public static final Block SUGAR_CANE = register(new Block("sugar_cane", builder().pushReaction(PistonBehavior.DESTROY) .intState(AGE_15))); @@ -997,43 +1001,43 @@ public final class Blocks { .intState(AGE_7))); public static final Block POTATOES = register(new Block("potatoes", builder().pushReaction(PistonBehavior.DESTROY) .intState(AGE_7))); - public static final Block OAK_BUTTON = register(new ButtonBlock("oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block OAK_BUTTON = register(new Block("oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block SPRUCE_BUTTON = register(new ButtonBlock("spruce_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block SPRUCE_BUTTON = register(new Block("spruce_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block BIRCH_BUTTON = register(new ButtonBlock("birch_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block BIRCH_BUTTON = register(new Block("birch_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block JUNGLE_BUTTON = register(new ButtonBlock("jungle_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block JUNGLE_BUTTON = register(new Block("jungle_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block ACACIA_BUTTON = register(new ButtonBlock("acacia_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block ACACIA_BUTTON = register(new Block("acacia_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block CHERRY_BUTTON = register(new ButtonBlock("cherry_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block CHERRY_BUTTON = register(new Block("cherry_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block DARK_OAK_BUTTON = register(new ButtonBlock("dark_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block DARK_OAK_BUTTON = register(new Block("dark_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block PALE_OAK_BUTTON = register(new ButtonBlock("pale_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block PALE_OAK_BUTTON = register(new Block("pale_oak_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block MANGROVE_BUTTON = register(new ButtonBlock("mangrove_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block MANGROVE_BUTTON = register(new Block("mangrove_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block BAMBOO_BUTTON = register(new ButtonBlock("bamboo_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block BAMBOO_BUTTON = register(new Block("bamboo_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -2232,11 +2236,11 @@ public final class Blocks { .enumState(HALF) .enumState(STAIRS_SHAPE) .booleanState(WATERLOGGED))); - public static final Block CRIMSON_BUTTON = register(new ButtonBlock("crimson_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block CRIMSON_BUTTON = register(new Block("crimson_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); - public static final Block WARPED_BUTTON = register(new ButtonBlock("warped_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block WARPED_BUTTON = register(new Block("warped_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -2268,6 +2272,9 @@ public final class Blocks { .enumState(STRUCTUREBLOCK_MODE))); public static final Block JIGSAW = register(new Block("jigsaw", builder().setBlockEntity(BlockEntityType.JIGSAW).requiresCorrectToolForDrops().destroyTime(-1.0f) .enumState(ORIENTATION, FrontAndTop.VALUES))); + public static final Block TEST_BLOCK = register(new Block("test_block", builder().setBlockEntity(BlockEntityType.TEST_BLOCK).destroyTime(-1.0f) + .enumState(TEST_BLOCK_MODE))); + public static final Block TEST_INSTANCE_BLOCK = register(new Block("test_instance_block", builder().setBlockEntity(BlockEntityType.TEST_INSTANCE_BLOCK).destroyTime(-1.0f))); public static final Block COMPOSTER = register(new Block("composter", builder().destroyTime(0.6f) .intState(LEVEL_COMPOSTER))); public static final Block TARGET = register(new Block("target", builder().destroyTime(0.5f) @@ -2336,7 +2343,7 @@ public final class Blocks { .booleanState(WATERLOGGED))); public static final Block POLISHED_BLACKSTONE_PRESSURE_PLATE = register(new Block("polished_blackstone_pressure_plate", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .booleanState(POWERED))); - public static final Block POLISHED_BLACKSTONE_BUTTON = register(new ButtonBlock("polished_blackstone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) + public static final Block POLISHED_BLACKSTONE_BUTTON = register(new Block("polished_blackstone_button", builder().destroyTime(0.5f).pushReaction(PistonBehavior.DESTROY) .enumState(ATTACH_FACE) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .booleanState(POWERED))); @@ -2790,6 +2797,12 @@ public final class Blocks { public static final Block PINK_PETALS = register(new Block("pink_petals", builder().pushReaction(PistonBehavior.DESTROY) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) .intState(FLOWER_AMOUNT))); + public static final Block WILDFLOWERS = register(new Block("wildflowers", builder().pushReaction(PistonBehavior.DESTROY) + .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) + .intState(FLOWER_AMOUNT))); + public static final Block LEAF_LITTER = register(new Block("leaf_litter", builder().pushReaction(PistonBehavior.DESTROY) + .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) + .intState(SEGMENT_AMOUNT))); public static final Block MOSS_BLOCK = register(new Block("moss_block", builder().destroyTime(0.1f).pushReaction(PistonBehavior.DESTROY))); public static final Block BIG_DRIPLEAF = register(new Block("big_dripleaf", builder().destroyTime(0.1f).pushReaction(PistonBehavior.DESTROY) .enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST) @@ -2921,6 +2934,7 @@ public final class Blocks { public static final Block CLOSED_EYEBLOSSOM = register(new Block("closed_eyeblossom", builder().pushReaction(PistonBehavior.DESTROY))); public static final Block POTTED_OPEN_EYEBLOSSOM = register(new FlowerPotBlock("potted_open_eyeblossom", OPEN_EYEBLOSSOM, builder().pushReaction(PistonBehavior.DESTROY))); public static final Block POTTED_CLOSED_EYEBLOSSOM = register(new FlowerPotBlock("potted_closed_eyeblossom", CLOSED_EYEBLOSSOM, builder().pushReaction(PistonBehavior.DESTROY))); + public static final Block FIREFLY_BUSH = register(new Block("firefly_bush", builder().pushReaction(PistonBehavior.DESTROY))); private static T register(T block) { block.setJavaId(BlockRegistries.JAVA_BLOCKS.get().size()); diff --git a/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java b/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java index f295c4f51..a837bd532 100644 --- a/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java +++ b/core/src/main/java/org/geysermc/geyser/level/block/property/Properties.java @@ -29,7 +29,6 @@ import org.geysermc.geyser.level.physics.Axis; import org.geysermc.geyser.level.physics.Direction; public final class Properties { - public static final BooleanProperty ACTIVE = BooleanProperty.create("active"); public static final BooleanProperty ATTACHED = BooleanProperty.create("attached"); public static final BooleanProperty BERRIES = BooleanProperty.create("berries"); public static final BooleanProperty BLOOM = BooleanProperty.create("bloom"); @@ -77,6 +76,7 @@ public final class Properties { public static final EnumProperty FACING_HOPPER = EnumProperty.create("facing", Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST); public static final EnumProperty HORIZONTAL_FACING = EnumProperty.create("facing", Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST); public static final IntegerProperty FLOWER_AMOUNT = IntegerProperty.create("flower_amount", 1, 4); + public static final IntegerProperty SEGMENT_AMOUNT = IntegerProperty.create("segment_amount", 1, 4); public static final EnumProperty ORIENTATION = EnumProperty.create("orientation", FrontAndTop.VALUES); public static final BasicEnumProperty ATTACH_FACE = BasicEnumProperty.create("face", "floor", "wall", "ceiling"); public static final BasicEnumProperty BELL_ATTACHMENT = BasicEnumProperty.create("attachment", "floor", "ceiling", "single_wall", "double_wall"); @@ -145,5 +145,8 @@ public final class Properties { public static final BooleanProperty CRAFTING = BooleanProperty.create("crafting"); public static final BasicEnumProperty TRIAL_SPAWNER_STATE = BasicEnumProperty.create("trial_spawner_state", "inactive", "waiting_for_players", "active", "waiting_for_reward_ejection", "ejecting_reward", "cooldown"); public static final BasicEnumProperty VAULT_STATE = BasicEnumProperty.create("vault_state", "inactive", "active", "unlocking", "ejecting"); + public static final BasicEnumProperty CREAKING_HEART_STATE = BasicEnumProperty.create("creaking_heart_state", "uprooted", "dormant", "awake"); public static final BooleanProperty OMINOUS = BooleanProperty.create("ominous"); + public static final BasicEnumProperty TEST_BLOCK_MODE = BasicEnumProperty.create("mode", "start", "log", "fail", "accept"); + public static final BooleanProperty MAP = BooleanProperty.create("map"); } diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java b/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java index 0f6e6b5bc..620a693a2 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java @@ -41,6 +41,7 @@ import org.geysermc.mcprotocollib.network.helper.NettyHelper; import org.geysermc.mcprotocollib.network.netty.MinecraftChannelInitializer; import org.geysermc.mcprotocollib.network.packet.PacketProtocol; import org.geysermc.mcprotocollib.network.session.ClientNetworkSession; +import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -56,7 +57,7 @@ public final class LocalSession extends ClientNetworkSession { private final SocketAddress spoofedRemoteAddress; - public LocalSession(SocketAddress targetAddress, String clientIp, PacketProtocol protocol, Executor packetHandlerExecutor) { + public LocalSession(SocketAddress targetAddress, String clientIp, MinecraftProtocol protocol, Executor packetHandlerExecutor) { super(targetAddress, protocol, packetHandlerExecutor, null, null); this.spoofedRemoteAddress = new InetSocketAddress(clientIp, 0); } 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 81f1fec46..d8e8a68ce 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 @@ -58,6 +58,9 @@ import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.level.block.type.BlockState; import org.geysermc.geyser.level.block.type.FlowerPotBlock; import org.geysermc.geyser.registry.BlockRegistries; +import org.geysermc.geyser.registry.populator.conversion.Conversion766_748; +import org.geysermc.geyser.registry.populator.conversion.Conversion776_766; +import org.geysermc.geyser.registry.populator.conversion.Conversion786_776; import org.geysermc.geyser.registry.type.BlockMappings; import org.geysermc.geyser.registry.type.GeyserBedrockBlock; @@ -119,7 +122,7 @@ public final class BlockRegistryPopulator { var blockMappers = ImmutableMap., Remapper>builder() .put(ObjectIntPair.of("1_21_40", Bedrock_v748.CODEC.getProtocolVersion()), Conversion766_748::remapBlock) .put(ObjectIntPair.of("1_21_50", Bedrock_v766.CODEC.getProtocolVersion()), Conversion776_766::remapBlock) - .put(ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()), tag -> tag) + .put(ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()), Conversion786_776::remapBlock) .put(ObjectIntPair.of("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()), tag -> tag) .build(); diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java index ef3168f41..29d85cdd2 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java @@ -75,6 +75,7 @@ public final class DataComponentRegistryPopulator { byte[] bytes = Base64.getDecoder().decode(encodedValue); ByteBuf buf = Unpooled.wrappedBuffer(bytes); int varInt = MinecraftTypes.readVarInt(buf); + System.out.println("int: " + varInt + " " + componentEntry.getKey()); DataComponentType dataComponentType = DataComponentTypes.from(varInt); DataComponent dataComponent = dataComponentType.readDataComponent(buf); @@ -84,6 +85,7 @@ public final class DataComponentRegistryPopulator { defaultComponents.add(new DataComponents(ImmutableMap.copyOf(map))); } } catch (Exception e) { + // TODO 1.21.5 enchantment reading is broken throw new AssertionError("Unable to load or parse components", e); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion766_748.java similarity index 87% rename from core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java rename to core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion766_748.java index 6f2bc61e2..b63797d74 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion766_748.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion766_748.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 GeyserMC. http://geysermc.org + * Copyright (c) 2024-2025 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 @@ -23,16 +23,18 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.registry.populator; +package org.geysermc.geyser.registry.populator.conversion; import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtMapBuilder; import org.geysermc.geyser.level.block.Blocks; import java.util.ArrayList; import java.util.List; import java.util.Set; +import static org.geysermc.geyser.registry.populator.conversion.ConversionHelper.withName; +import static org.geysermc.geyser.registry.populator.conversion.ConversionHelper.withoutStates; + public class Conversion766_748 { static List PALE_WOODEN_BLOCKS = new ArrayList<>(); static List OTHER_NEW_BLOCKS = new ArrayList<>(); @@ -84,7 +86,7 @@ public class Conversion766_748 { OTHER_NEW_BLOCKS.add("resin_brick_double_slab"); } - static NbtMap remapBlock(NbtMap tag) { + public static NbtMap remapBlock(NbtMap tag) { // First: Downgrade from 1.21.60 -> 1.21.50 tag = Conversion776_766.remapBlock(tag); @@ -116,17 +118,4 @@ public class Conversion766_748 { return tag; } - - static NbtMap withName(NbtMap tag, String name) { - NbtMapBuilder builder = tag.toBuilder(); - builder.replace("name", "minecraft:" + name); - return builder.build(); - } - - static NbtMap withoutStates(String name) { - NbtMapBuilder tagBuilder = NbtMap.builder(); - tagBuilder.putString("name", "minecraft:" + name); - tagBuilder.putCompound("states", NbtMap.builder().build()); - return tagBuilder.build(); - } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion776_766.java similarity index 94% rename from core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java rename to core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion776_766.java index edc2543ae..325ad5ca9 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/Conversion776_766.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion776_766.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.registry.populator; +package org.geysermc.geyser.registry.populator.conversion; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; @@ -31,6 +31,10 @@ import org.cloudburstmc.nbt.NbtMapBuilder; public class Conversion776_766 { public static NbtMap remapBlock(NbtMap tag) { + + // First: Downgrade from 1.21.70 + tag = Conversion786_776.remapBlock(tag); + final String name = tag.getString("name"); if (name.equals("minecraft:creaking_heart")) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion786_776.java b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion786_776.java new file mode 100644 index 000000000..ee4518a95 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/Conversion786_776.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 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.registry.populator.conversion; + +import org.cloudburstmc.nbt.NbtMap; + +import static org.geysermc.geyser.registry.populator.conversion.ConversionHelper.withName; +import static org.geysermc.geyser.registry.populator.conversion.ConversionHelper.withoutStates; + +public class Conversion786_776 { + + public static NbtMap remapBlock(NbtMap nbtMap) { + + final String name = nbtMap.getString("name"); + if (name.equals("minecraft:bush")) { + return withName(nbtMap, "fern"); + } + + if (name.equals("minecraft:firefly_bush")) { + return withName(nbtMap, "deadbush"); + } + + if (name.equals("minecraft:tall_dry_grass") || name.equals("minecraft:short_dry_grass")) { + return withName(nbtMap, "short_grass"); + } + + if (name.equals("minecraft:cactus_flower")) { + return withName(nbtMap, "unknown"); + } + + if (name.equals("minecraft:leaf_litter") || name.equals("minecraft:wildflowers")) { + return withoutStates("unknown"); + } + + return nbtMap; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/ConversionHelper.java b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/ConversionHelper.java new file mode 100644 index 000000000..0bc9708ac --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/conversion/ConversionHelper.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 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.registry.populator.conversion; + +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; + +// A variety of methods to help with re-mapping blocks and items to older versions. +public class ConversionHelper { + + static NbtMap withName(NbtMap tag, String name) { + NbtMapBuilder builder = tag.toBuilder(); + builder.replace("name", "minecraft:" + name); + return builder.build(); + } + + static NbtMap withoutStates(String name) { + NbtMapBuilder tagBuilder = NbtMap.builder(); + tagBuilder.putString("name", "minecraft:" + name); + tagBuilder.putCompound("states", NbtMap.EMPTY); + return tagBuilder.build(); + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/tags/BlockTag.java b/core/src/main/java/org/geysermc/geyser/session/cache/tags/BlockTag.java index 59d301a89..924b168a4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/tags/BlockTag.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/tags/BlockTag.java @@ -141,6 +141,7 @@ public final class BlockTag { public static final Tag FENCE_GATES = create("fence_gates"); public static final Tag UNSTABLE_BOTTOM_CENTER = create("unstable_bottom_center"); public static final Tag MUSHROOM_GROW_BLOCK = create("mushroom_grow_block"); + public static final Tag EDIBLE_FOR_SHEEP = create("edible_for_sheep"); public static final Tag INFINIBURN_OVERWORLD = create("infiniburn_overworld"); public static final Tag INFINIBURN_NETHER = create("infiniburn_nether"); public static final Tag INFINIBURN_END = create("infiniburn_end"); @@ -171,6 +172,7 @@ public final class BlockTag { public static final Tag MINEABLE_PICKAXE = create("mineable/pickaxe"); public static final Tag MINEABLE_SHOVEL = create("mineable/shovel"); public static final Tag SWORD_EFFICIENT = create("sword_efficient"); + public static final Tag SWORD_INSTANTLY_MINES = create("sword_instantly_mines"); public static final Tag NEEDS_DIAMOND_TOOL = create("needs_diamond_tool"); public static final Tag NEEDS_IRON_TOOL = create("needs_iron_tool"); public static final Tag NEEDS_STONE_TOOL = create("needs_stone_tool"); @@ -200,13 +202,15 @@ public final class BlockTag { public static final Tag WOLVES_SPAWNABLE_ON = create("wolves_spawnable_on"); public static final Tag FROGS_SPAWNABLE_ON = create("frogs_spawnable_on"); public static final Tag BATS_SPAWNABLE_ON = create("bats_spawnable_on"); + public static final Tag CAMELS_SPAWNABLE_ON = create("camels_spawnable_on"); public static final Tag AZALEA_GROWS_ON = create("azalea_grows_on"); public static final Tag CONVERTABLE_TO_MUD = create("convertable_to_mud"); public static final Tag MANGROVE_LOGS_CAN_GROW_THROUGH = create("mangrove_logs_can_grow_through"); public static final Tag MANGROVE_ROOTS_CAN_GROW_THROUGH = create("mangrove_roots_can_grow_through"); - public static final Tag DEAD_BUSH_MAY_PLACE_ON = create("dead_bush_may_place_on"); + public static final Tag DRY_VEGETATION_MAY_PLACE_ON = create("dry_vegetation_may_place_on"); public static final Tag SNAPS_GOAT_HORN = create("snaps_goat_horn"); public static final Tag REPLACEABLE_BY_TREES = create("replaceable_by_trees"); + public static final Tag REPLACEABLE_BY_MUSHROOMS = create("replaceable_by_mushrooms"); public static final Tag SNOW_LAYER_CANNOT_SURVIVE_ON = create("snow_layer_cannot_survive_on"); public static final Tag SNOW_LAYER_CAN_SURVIVE_ON = create("snow_layer_can_survive_on"); public static final Tag INVALID_SPAWN_INSIDE = create("invalid_spawn_inside"); @@ -219,6 +223,7 @@ public final class BlockTag { public static final Tag MAINTAINS_FARMLAND = create("maintains_farmland"); public static final Tag BLOCKS_WIND_CHARGE_EXPLOSIONS = create("blocks_wind_charge_explosions"); public static final Tag DOES_NOT_BLOCK_HOPPERS = create("does_not_block_hoppers"); + public static final Tag PLAYS_AMBIENT_DESERT_BLOCK_SOUNDS = create("plays_ambient_desert_block_sounds"); public static final Tag AIR = create("air"); private BlockTag() {} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java b/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java index e2f4f2db3..50398a765 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/tags/ItemTag.java @@ -76,6 +76,7 @@ public final class ItemTag { public static final Tag LEAVES = create("leaves"); public static final Tag TRAPDOORS = create("trapdoors"); public static final Tag SMALL_FLOWERS = create("small_flowers"); + public static final Tag FLOWERS = create("flowers"); public static final Tag BEDS = create("beds"); public static final Tag FENCES = create("fences"); public static final Tag PIGLIN_REPELLENTS = create("piglin_repellents"); @@ -85,6 +86,7 @@ public final class ItemTag { public static final Tag DUPLICATES_ALLAYS = create("duplicates_allays"); public static final Tag BREWING_FUEL = create("brewing_fuel"); public static final Tag SHULKER_BOXES = create("shulker_boxes"); + public static final Tag EGGS = create("eggs"); public static final Tag MEAT = create("meat"); public static final Tag SNIFFER_FOOD = create("sniffer_food"); public static final Tag PIGLIN_FOOD = create("piglin_food"); @@ -181,6 +183,7 @@ public final class ItemTag { public static final Tag DYEABLE = create("dyeable"); public static final Tag FURNACE_MINECART_FUEL = create("furnace_minecart_fuel"); public static final Tag BUNDLES = create("bundles"); + public static final Tag BOOK_CLONING_TARGET = create("book_cloning_target"); public static final Tag SKELETON_PREFERRED_WEAPONS = create("skeleton_preferred_weapons"); public static final Tag DROWNED_PREFERRED_WEAPONS = create("drowned_preferred_weapons"); public static final Tag PIGLIN_PREFERRED_WEAPONS = create("piglin_preferred_weapons"); 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 0c8c63f00..9d01f0d8c 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 @@ -168,6 +168,9 @@ public final class ItemTranslator { public static ItemData.@NonNull Builder translateToBedrock(GeyserSession session, Item javaItem, ItemMapping bedrockItem, int count, @Nullable DataComponents customComponents) { BedrockItemBuilder nbtBuilder = new BedrockItemBuilder(); + // TODO 1.21.5: + // - Hiding components + // Populates default components that aren't sent over the network DataComponents components = javaItem.gatherComponents(customComponents); @@ -180,22 +183,22 @@ public final class ItemTranslator { PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); // Make custom effect information visible // Ignore when item have "hide_additional_tooltip" component - if (potionContents != null && components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) == null) { + if (potionContents != null) { // && components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) == null) { customName += getPotionEffectInfo(potionContents, session.locale()); } nbtBuilder.setCustomName(customName); } - boolean hideTooltips = components.get(DataComponentTypes.HIDE_TOOLTIP) != null; + //boolean hideTooltips = components.get(DataComponentTypes.HIDE_TOOLTIP) != null; ItemAttributeModifiers attributeModifiers = components.get(DataComponentTypes.ATTRIBUTE_MODIFIERS); - if (attributeModifiers != null && attributeModifiers.isShowInTooltip() && !hideTooltips) { + if (attributeModifiers != null) { //&& attributeModifiers.isShowInTooltip() && !hideTooltips) { // only add if attribute modifiers do not indicate to hide them addAttributeLore(session, attributeModifiers, nbtBuilder, session.locale()); } - if (session.isAdvancedTooltips() && !hideTooltips) { + if (session.isAdvancedTooltips()) { //&& !hideTooltips) { addAdvancedTooltips(components, nbtBuilder, javaItem, session.locale()); } @@ -545,7 +548,8 @@ public final class ItemTranslator { if (!customNameOnly) { PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); if (potionContents != null) { - String potionName = getPotionName(potionContents, mapping, components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) != null, session.locale()); + // TODO 1.21.5 + String potionName = getPotionName(potionContents, mapping, false /*components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) != null */, session.locale()); if (potionName != null) { return ChatColor.RESET + ChatColor.ESCAPE + translationColor + potionName; } 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 bfc71089a..c44f21213 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 @@ -30,7 +30,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; -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; @@ -41,8 +40,6 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventoryTra import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetItemSlotData; import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.Entity; @@ -52,7 +49,6 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.click.Click; -import org.geysermc.geyser.inventory.item.GeyserInstrument; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.item.type.BoatItem; @@ -78,16 +74,12 @@ 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.geyser.util.SoundUtils; -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.DataComponentTypes; -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; @@ -388,29 +380,30 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator holder = session.getPlayerInventory() - .getItemInHand() - .getComponent(DataComponentTypes.INSTRUMENT); - if (holder != null) { - GeyserInstrument instrument = GeyserInstrument.fromHolder(session, holder); - if (instrument.bedrockInstrument() != null) { - // 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.bedrockInstrument().ordinal())); - soundPacket.setPosition(session.getPlayerEntity().getPosition()); - soundPacket.setIdentifier("minecraft:player"); - soundPacket.setExtraData(-1); - session.sendUpstreamPacket(soundPacket); - } else { - PlaySoundPacket playSoundPacket = new PlaySoundPacket(); - playSoundPacket.setPosition(session.getPlayerEntity().position()); - playSoundPacket.setSound(SoundUtils.translatePlaySound(instrument.soundEvent())); - playSoundPacket.setPitch(1.0F); - playSoundPacket.setVolume(instrument.range() / 16.0F); - session.sendUpstreamPacket(playSoundPacket); - } - } +// Holder holder = session.getPlayerInventory() +// .getItemInHand() +// .getComponent(DataComponentTypes.INSTRUMENT); +// if (holder != null) { +// GeyserInstrument instrument = GeyserInstrument.fromComponent(session, holder); +// if (instrument.bedrockInstrument() != null) { +// // 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.bedrockInstrument().ordinal())); +// soundPacket.setPosition(session.getPlayerEntity().getPosition()); +// soundPacket.setIdentifier("minecraft:player"); +// soundPacket.setExtraData(-1); +// session.sendUpstreamPacket(soundPacket); +// } else { +// PlaySoundPacket playSoundPacket = new PlaySoundPacket(); +// playSoundPacket.setPosition(session.getPlayerEntity().position()); +// playSoundPacket.setSound(SoundUtils.translatePlaySound(instrument.soundEvent())); +// playSoundPacket.setPitch(1.0F); +// playSoundPacket.setVolume(instrument.range() / 16.0F); +// session.sendUpstreamPacket(playSoundPacket); +// } +// } } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java similarity index 97% rename from core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java rename to core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java index ed1951243..738d3c07e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.translator.protocol.java.entity.spawn; +package org.geysermc.geyser.translator.protocol.java.entity; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.GeyserImpl; @@ -47,7 +47,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.object.FallingBlockD import org.geysermc.mcprotocollib.protocol.data.game.entity.object.ProjectileData; import org.geysermc.mcprotocollib.protocol.data.game.entity.object.WardenData; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundAddEntityPacket; @Translator(packet = ClientboundAddEntityPacket.class) public class JavaAddEntityTranslator extends PacketTranslator { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddExperienceOrbTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddExperienceOrbTranslator.java deleted file mode 100644 index 8f37eb4d4..000000000 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddExperienceOrbTranslator.java +++ /dev/null @@ -1,49 +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.translator.protocol.java.entity.spawn; - -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddExperienceOrbPacket; -import org.cloudburstmc.math.vector.Vector3f; -import org.geysermc.geyser.entity.type.Entity; -import org.geysermc.geyser.entity.type.ExpOrbEntity; -import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.translator.protocol.PacketTranslator; -import org.geysermc.geyser.translator.protocol.Translator; - -@Translator(packet = ClientboundAddExperienceOrbPacket.class) -public class JavaAddExperienceOrbTranslator extends PacketTranslator { - - @Override - public void translate(GeyserSession session, ClientboundAddExperienceOrbPacket packet) { - Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ()); - - Entity entity = new ExpOrbEntity( - session, packet.getExp(), packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(), position - ); - - session.getEntityCache().spawnEntity(entity); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java index e4ff0539f..8a06698c0 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaMerchantOffersTranslator.java @@ -73,11 +73,11 @@ public class JavaMerchantOffersTranslator extends PacketTranslator tags = new ArrayList<>(addExtraTrade ? packet.getTrades().length + 1 : packet.getTrades().length); - for (int i = 0; i < packet.getTrades().length; i++) { - VillagerTrade trade = packet.getTrades()[i]; + boolean addExtraTrade = packet.isShowProgress() && packet.getVillagerLevel() < 5; + List tags = new ArrayList<>(addExtraTrade ? packet.getOffers().size() + 1 : packet.getOffers().size()); + for (int i = 0; i < packet.getOffers().size(); i++) { + VillagerTrade trade = packet.getOffers().get(i); NbtMapBuilder recipe = NbtMap.builder(); recipe.putInt("netId", i + 1); - recipe.putInt("maxUses", trade.isTradeDisabled() ? 0 : trade.getMaxUses()); + recipe.putInt("maxUses", trade.isOutOfStock() ? 0 : trade.getMaxUses()); recipe.putInt("traderExp", trade.getXp()); recipe.putFloat("priceMultiplierA", trade.getPriceMultiplier()); recipe.putFloat("priceMultiplierB", 0.0f); - recipe.put("sell", getItemTag(session, trade.getOutput())); + recipe.put("sell", getItemTag(session, trade.getResult())); // The buy count before demand and special price adjustments // The first input CAN be null as of Java 1.19.0/Bedrock 1.19.10 // Replicable item: https://gist.github.com/Camotoy/3f3f23d1f80981d1b4472bdb23bba698 from https://github.com/GeyserMC/Geyser/issues/3171 - recipe.putInt("buyCountA", trade.getFirstInput() != null ? Math.max(trade.getFirstInput().getAmount(), 0) : 0); - recipe.putInt("buyCountB", trade.getSecondInput() != null ? Math.max(trade.getSecondInput().getAmount(), 0) : 0); + recipe.putInt("buyCountA", trade.getItemCostA() != null ? Math.max(trade.getItemCostA().count(), 0) : 0); + recipe.putInt("buyCountB", trade.getItemCostB() != null ? Math.max(trade.getItemCostB().count(), 0) : 0); recipe.putInt("demand", trade.getDemand()); // Seems to have no effect recipe.putInt("tier", packet.getVillagerLevel() > 0 ? packet.getVillagerLevel() - 1 : 0); // -1 crashes client - recipe.put("buyA", getItemTag(session, trade.getFirstInput(), trade.getSpecialPrice(), trade.getDemand(), trade.getPriceMultiplier())); - recipe.put("buyB", getItemTag(session, trade.getSecondInput())); - recipe.putInt("uses", trade.getNumUses()); + recipe.put("buyA", getItemTag(session, toItemStack(trade.getItemCostA()), trade.getSpecialPriceDiff(), trade.getDemand(), trade.getPriceMultiplier())); + recipe.put("buyB", getItemTag(session, toItemStack(trade.getItemCostB()))); + recipe.putInt("uses", trade.getUses()); recipe.putByte("rewardExp", (byte) 1); tags.add(recipe.build()); } @@ -158,6 +158,11 @@ public class JavaMerchantOffersTranslator extends PacketTranslator { ServerboundClientCommandPacket javaRespawnPacket = new ServerboundClientCommandPacket(ClientCommand.RESPAWN); @@ -114,7 +114,7 @@ public class JavaGameEventTranslator extends PacketTranslator("doimmediaterespawn", packet.getValue() == RespawnScreenValue.IMMEDIATE_RESPAWN)); session.sendUpstreamPacket(gamerulePacket); break; - case INVALID_BED: + case NO_RESPAWN_BLOCK_AVAILABLE: // Not sent as a proper message? Odd. session.sendMessage(MinecraftLocale.getLocaleString("block.minecraft.spawn.not_valid", session.locale())); break; - case ARROW_HIT_PLAYER: + case PLAY_ARROW_HIT_SOUND: PlaySoundPacket arrowSoundPacket = new PlaySoundPacket(); arrowSoundPacket.setSound("random.orb"); arrowSoundPacket.setPitch(0.5f); @@ -143,9 +143,9 @@ public class JavaGameEventTranslator extends PacketTranslator#5075 - */ - @Test - void entityWithoutUuid() { - // experience orbs are the only known entities without an uuid, see Entity#teamIdentifier for more info - mockContextScoreboard(context -> { - var addExperienceOrbTranslator = new JavaAddExperienceOrbTranslator(); - var removeEntitiesTranslator = new JavaRemoveEntitiesTranslator(); - - // Entity#teamIdentifier used to throw because it returned uuid.toString where uuid could be null. - // this would result in both EntityCache#spawnEntity and EntityCache#removeEntity throwing an exception, - // because the entity would be registered and deregistered to the scoreboard. - assertDoesNotThrow(() -> { - context.translate(addExperienceOrbTranslator, new ClientboundAddExperienceOrbPacket(2, 0, 0, 0, 1)); - - String displayName = context.mockOrSpy(EntityCache.class).getEntityByJavaId(2).getDisplayName(); - assertEquals("entity.minecraft.experience_orb", displayName); - - context.translate(removeEntitiesTranslator, new ClientboundRemoveEntitiesPacket(new int[]{2})); - }); - - // we know that spawning and removing the entity should be fine - assertNextPacketType(context, AddEntityPacket.class); - assertNextPacketType(context, RemoveEntityPacket.class); - }); - } + // TODO 1.21.5 +// /** +// * Test for #5075 +// */ +// @Test +// void entityWithoutUuid() { +// // experience orbs are the only known entities without an uuid, see Entity#teamIdentifier for more info +// mockContextScoreboard(context -> { +// var addExperienceOrbTranslator = new JavaAddExperienceOrbTranslator(); +// var removeEntitiesTranslator = new JavaRemoveEntitiesTranslator(); +// +// // Entity#teamIdentifier used to throw because it returned uuid.toString where uuid could be null. +// // this would result in both EntityCache#spawnEntity and EntityCache#removeEntity throwing an exception, +// // because the entity would be registered and deregistered to the scoreboard. +// assertDoesNotThrow(() -> { +// context.translate(addExperienceOrbTranslator, new ClientboundAddExperienceOrbPacket(2, 0, 0, 0, 1)); +// +// String displayName = context.mockOrSpy(EntityCache.class).getEntityByJavaId(2).getDisplayName(); +// assertEquals("entity.minecraft.experience_orb", displayName); +// +// context.translate(removeEntitiesTranslator, new ClientboundRemoveEntitiesPacket(new int[] { 2 })); +// }); +// +// // we know that spawning and removing the entity should be fine +// assertNextPacketType(context, AddEntityPacket.class); +// assertNextPacketType(context, RemoveEntityPacket.class); +// }); +// } /** * Test for #5078 @@ -168,7 +148,7 @@ public class ScoreboardIssueTests { playerInfoUpdateTranslator, new ClientboundPlayerInfoUpdatePacket( EnumSet.of(PlayerListEntryAction.ADD_PLAYER, PlayerListEntryAction.UPDATE_LISTED), - new PlayerListEntry[]{ + new PlayerListEntry[] { new PlayerListEntry(npcUuid, new GameProfile(npcUuid, "1297"), false, 0, GameMode.SURVIVAL, null, false, 0, null, 0, null, null) })); @@ -198,7 +178,7 @@ public class ScoreboardIssueTests { ); context.translate( setPlayerTeamTranslator, - new ClientboundSetPlayerTeamPacket("npc_team_1297", TeamAction.ADD_PLAYER, new String[]{"1297"})); + new ClientboundSetPlayerTeamPacket("npc_team_1297", TeamAction.ADD_PLAYER, new String[]{ "1297" })); context.translate(addEntityTranslator, new ClientboundAddEntityPacket(1297, npcUuid, EntityType.PLAYER, 1, 2, 3, 4, 5, 6)); // then it updates the displayed skin parts, which isn't relevant for us @@ -260,7 +240,7 @@ public class ScoreboardIssueTests { ); context.translate( setPlayerTeamTranslator, - new ClientboundSetPlayerTeamPacket("npc_team_1298", TeamAction.ADD_PLAYER, new String[]{hologramUuid.toString()})); + new ClientboundSetPlayerTeamPacket("npc_team_1298", TeamAction.ADD_PLAYER, new String[]{ hologramUuid.toString() })); assertNextPacket(context, () -> { var packet = new SetEntityDataPacket(); @@ -270,76 +250,4 @@ public class ScoreboardIssueTests { }); }); } - - /** - * Test for #5353. - * It follows a code snippet provided in the PR description. - */ - @Test - void prefixNotShowing() { - mockContextScoreboard(context -> { - var setObjectiveTranslator = new JavaSetObjectiveTranslator(); - var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator(); - var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator(); - var setScoreTranslator = new JavaSetScoreTranslator(); - - context.translate( - setObjectiveTranslator, - new ClientboundSetObjectivePacket( - "sb-0", - ObjectiveAction.ADD, - Component.text("Test Scoreboard"), - ScoreType.INTEGER, - null - ) - ); - assertNoNextPacket(context); - - context.translate( - setDisplayObjectiveTranslator, - new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "sb-0") - ); - assertNextPacket(context, () -> { - var packet = new SetDisplayObjectivePacket(); - packet.setObjectiveId("0"); - packet.setDisplayName("Test Scoreboard"); - packet.setCriteria("dummy"); - packet.setDisplaySlot("sidebar"); - packet.setSortOrder(1); - return packet; - }); - - context.translate( - setPlayerTeamTranslator, - new ClientboundSetPlayerTeamPacket( - "sbt-1", - Component.text("displaynametest"), - Component.text("§aScore: 10"), - Component.empty(), - false, - false, - NameTagVisibility.NEVER, - CollisionRule.NEVER, - TeamColor.DARK_GREEN, - new String[]{"§0"}) - ); - assertNoNextPacket(context); - - context.translate( - setScoreTranslator, - new ClientboundSetScorePacket( - "§0", - "sb-0", - 10 - ).withDisplay(Component.empty()) - ); - assertNextPacket(context, () -> { - var packet = new SetScorePacket(); - packet.setAction(SetScorePacket.Action.SET); - packet.setInfos(List.of(new ScoreInfo(1, "0", 10, "§2§aScore: 10§r§2§r§2"))); - return packet; - }); - assertNoNextPacket(context); - }); - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f19ed7678..0f6d1faf5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ protocol-common = "3.0.0.Beta6-20250324.162731-5" protocol-codec = "3.0.0.Beta6-20250324.162731-5" raknet = "1.0.0.CR3-20250218.160705-18" minecraftauth = "4.1.1" -mcprotocollib = "1.21.4-20250311.232133-24" +mcprotocollib = "1.21.5-SNAPSHOT" adventure = "4.14.0" adventure-platform = "4.3.0" junit = "5.9.2" From 8035a26198ec09efbd54416a74c646b0d7ce56bb Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 08:02:46 +0000 Subject: [PATCH 14/86] Support new tooltip display component and other 1.21.5 things (#5417) * Work on supporting new tooltip display component * Fix some stuff and allow any item to function as saddle with the right components * Some fixes and TODOs * Re-implement tropical fish variant tooltip * Fix hiding advanced tooltips * Fix ominous banner tooltip, custom name and some TODOs * Implement RegistryEntryData to allow getting an object from registry cache by its key * Fix goat horns (I think) * We prefer checkers for the nullable/nonnull annotations * Remove unused NotNull import Co-authored-by: chris --------- Co-authored-by: chris --- .../kotlin/geyser.base-conventions.gradle.kts | 4 +- .../geyser/entity/type/FireworkEntity.java | 4 +- .../geyser/entity/type/LivingEntity.java | 16 ++++- .../living/animal/TropicalFishEntity.java | 4 ++ .../inventory/item/GeyserInstrument.java | 15 +++-- .../geysermc/geyser/item/TooltipOptions.java | 59 +++++++++++++++++ .../geysermc/geyser/item/type/ArmorItem.java | 6 +- .../geyser/item/type/AxolotlBucketItem.java | 5 +- .../geysermc/geyser/item/type/BannerItem.java | 9 ++- .../geyser/item/type/CompassItem.java | 5 +- .../geyser/item/type/CrossbowItem.java | 5 +- .../geyser/item/type/DecoratedPotItem.java | 5 +- .../geyser/item/type/DyeableArmorItem.java | 5 +- .../geyser/item/type/EnchantedBookItem.java | 5 +- .../geyser/item/type/FireworkRocketItem.java | 5 +- .../geyser/item/type/FireworkStarItem.java | 5 +- .../geyser/item/type/FishingRodItem.java | 5 +- .../geyser/item/type/GoatHornItem.java | 8 +-- .../org/geysermc/geyser/item/type/Item.java | 17 ++--- .../geysermc/geyser/item/type/MapItem.java | 5 +- .../geyser/item/type/PlayerHeadItem.java | 5 +- .../geysermc/geyser/item/type/ShieldItem.java | 5 +- .../geyser/item/type/ShulkerBoxItem.java | 5 +- .../item/type/TropicalFishBucketItem.java | 55 ++++++++++------ .../geyser/item/type/WolfArmorItem.java | 5 +- .../geyser/item/type/WritableBookItem.java | 5 +- .../geyser/item/type/WrittenBookItem.java | 5 +- .../geyser/session/cache/RegistryCache.java | 5 +- .../session/cache/registry/JavaRegistry.java | 18 ++++- .../cache/registry/JavaRegistryKey.java | 3 +- .../cache/registry/RegistryEntryData.java | 31 +++++++++ .../cache/registry/SimpleJavaRegistry.java | 42 ++++++++++-- .../translator/item/ItemTranslator.java | 65 +++++++++---------- ...BedrockInventoryTransactionTranslator.java | 52 ++++++++------- 34 files changed, 348 insertions(+), 145 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java create mode 100644 core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryData.java 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 93b4d8c13..3f7b48a2f 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -26,7 +26,7 @@ dependencies { } repositories { - // mavenLocal() + mavenLocal() mavenCentral() @@ -69,6 +69,4 @@ repositories { maven("https://jitpack.io") { content { includeGroupByRegex("com\\.github\\..*") } } - - mavenLocal() } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java index ebe35320e..d7a9990fe 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/FireworkEntity.java @@ -31,6 +31,7 @@ import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; @@ -59,7 +60,8 @@ public class FireworkEntity extends Entity { // TODO this looked the same, so I'm going to assume it is and (keep below comment if true) // Translate using item methods to get firework NBT for Bedrock BedrockItemBuilder builder = new BedrockItemBuilder(); - Items.FIREWORK_ROCKET.translateComponentsToBedrock(session, components, builder); + TooltipOptions tooltip = TooltipOptions.fromComponents(components); + Items.FIREWORK_ROCKET.translateComponentsToBedrock(session, components, tooltip, builder); dirtyMetadata.put(EntityDataTypes.DISPLAY_FIREWORK, builder.build()); } 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 70ef2300e..6ef6ba0c9 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 @@ -44,6 +44,8 @@ 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.item.type.Item; +import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; @@ -51,6 +53,7 @@ import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; @@ -62,6 +65,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.Object import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ColorParticleData; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; @@ -133,7 +138,16 @@ public class LivingEntity extends Entity { public void setSaddle(ItemStack stack) { this.saddle = ItemTranslator.translateToBedrock(session, stack); - updateSaddled(stack.getId() == Items.SADDLE.javaId()); + + boolean saddled = false; + Item item = Registries.JAVA_ITEMS.get(stack.getId()); + if (item != null) { + DataComponents components = item.gatherComponents(stack.getDataComponentsPatch()); + Equippable equippable = components.get(DataComponentTypes.EQUIPPABLE); + saddled = equippable != null && equippable.slot() == EquipmentSlot.SADDLE; + } + + updateSaddled(saddled); } public void setHand(ItemStack stack) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java index b6751bc3f..182bb176f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/TropicalFishEntity.java @@ -61,6 +61,10 @@ public class TropicalFishEntity extends AbstractFishEntity { dirtyMetadata.put(EntityDataTypes.COLOR_2, getPatternColor(varNumber)); // Pattern color 0-15 } + public static int getPackedVariant(int pattern, int baseColor, int patternColor) { + return pattern & 65535 | (baseColor & 0xFF) << 16 | (patternColor & 0xFF) << 24; + } + public static int getShape(int variant) { return Math.min(variant & 0xFF, 1); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java index 38d4f2cd5..71f43a23e 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/GeyserInstrument.java @@ -89,13 +89,18 @@ public interface GeyserInstrument { return -1; } - // TODO 1.21.5 + // TODO test in 1.21.5 static GeyserInstrument fromComponent(GeyserSession session, InstrumentComponent component) { - if (component.instrumentHolder().isId()) { - return session.getRegistryCache().instruments().byId(component.instrumentHolder().id()); + if (component.instrumentLocation() != null) { + return session.getRegistryCache().instruments().byKey(component.instrumentLocation()); + } else if (component.instrumentHolder() != null) { + if (component.instrumentHolder().isId()) { + return session.getRegistryCache().instruments().byId(component.instrumentHolder().id()); + } + InstrumentComponent.Instrument custom = component.instrumentHolder().custom(); + return new Wrapper(custom, session.locale()); } - InstrumentComponent.Instrument custom = component.instrumentHolder().custom(); - return new Wrapper(custom, session.locale()); + throw new IllegalStateException("InstrumentComponent must have either a location or a holder"); } record Wrapper(InstrumentComponent.Instrument instrument, String locale) implements GeyserInstrument { diff --git a/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java b/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java new file mode 100644 index 000000000..2fa9af789 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/TooltipOptions.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item; + +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay; + +@FunctionalInterface +public interface TooltipOptions { + + TooltipOptions ALL_SHOWN = component -> true; + + TooltipOptions ALL_HIDDEN = component -> false; + + boolean showInTooltip(DataComponentType component); + + static TooltipOptions fromComponents(DataComponents components) { + TooltipDisplay display = components.get(DataComponentTypes.TOOLTIP_DISPLAY); + if (display == null) { + return ALL_SHOWN; + } else if (display.hideTooltip()) { + return ALL_HIDDEN; + } else if (display.hiddenComponents().isEmpty()) { + return ALL_SHOWN; + } + + return component -> !display.hiddenComponents().contains(component); + } + + static boolean hideTooltip(DataComponents components) { + TooltipDisplay display = components.get(DataComponentTypes.TOOLTIP_DISPLAY); + return display != null && display.hideTooltip(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ArmorItem.java b/core/src/main/java/org/geysermc/geyser/item/type/ArmorItem.java index c20cc490e..5eca97f87 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/ArmorItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/ArmorItem.java @@ -30,6 +30,7 @@ import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ArmorTrim; @@ -43,8 +44,8 @@ public class ArmorItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); ArmorTrim trim = components.get(DataComponentTypes.TRIM); if (trim != null) { @@ -54,6 +55,7 @@ public class ArmorItem extends Item { // discard custom trim patterns/materials to prevent visual glitches on bedrock if (!getNamespace(material.getMaterialId()).equals("minecraft") || !getNamespace(pattern.getPatternId()).equals("minecraft")) { + // TODO - how is this shown in tooltip? should we add a custom trim tooltip to the lore here return; } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/AxolotlBucketItem.java b/core/src/main/java/org/geysermc/geyser/item/type/AxolotlBucketItem.java index 8895d45a8..3d3214697 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/AxolotlBucketItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/AxolotlBucketItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.item.BedrockItemBuilder; @@ -37,8 +38,8 @@ public class AxolotlBucketItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); // Bedrock Edition displays the properties of the axolotl. Java does not. // To work around this, set the custom name to the Axolotl translation and it's displayed correctly diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java index f7229e202..2f3f64b75 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java @@ -36,6 +36,7 @@ import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.inventory.item.DyeColor; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -46,6 +47,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay; import java.util.ArrayList; import java.util.List; @@ -202,8 +204,8 @@ public class BannerItem extends BlockItem { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); List patterns = components.get(DataComponentTypes.BANNER_PATTERNS); if (patterns != null) { @@ -225,7 +227,8 @@ public class BannerItem extends BlockItem { } components.put(DataComponentTypes.BANNER_PATTERNS, patternLayers); - // TODO 1.21.5 hide components??? + // The ominous banner item in the Java creative menu just has banner patterns hidden as of 1.21.5 + components.put(DataComponentTypes.TOOLTIP_DISPLAY, new TooltipDisplay(false, List.of(DataComponentTypes.BANNER_PATTERNS))); components.put(DataComponentTypes.ITEM_NAME, Component .translatable("block.minecraft.ominous_banner") .style(Style.style(TextColor.color(16755200))) diff --git a/core/src/main/java/org/geysermc/geyser/item/type/CompassItem.java b/core/src/main/java/org/geysermc/geyser/item/type/CompassItem.java index d6403a8c3..ef1ca52c5 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/CompassItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/CompassItem.java @@ -29,6 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; @@ -59,8 +60,8 @@ public class CompassItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); LodestoneTracker tracker = components.get(DataComponentTypes.LODESTONE_TRACKER); if (tracker != null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java b/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java index 13e79958e..9eb95e4fb 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/CrossbowItem.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; @@ -44,8 +45,8 @@ public class CrossbowItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); List chargedProjectiles = components.get(DataComponentTypes.CHARGED_PROJECTILES); if (chargedProjectiles != null && !chargedProjectiles.isEmpty()) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/DecoratedPotItem.java b/core/src/main/java/org/geysermc/geyser/item/type/DecoratedPotItem.java index fa08bd7ec..a81a8cb52 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/DecoratedPotItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/DecoratedPotItem.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -44,8 +45,8 @@ public class DecoratedPotItem extends BlockItem { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); List decorations = components.get(DataComponentTypes.POT_DECORATIONS); // TODO maybe unbox in MCProtocolLib if (decorations != null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/DyeableArmorItem.java b/core/src/main/java/org/geysermc/geyser/item/type/DyeableArmorItem.java index 480385d07..b85a41782 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/DyeableArmorItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/DyeableArmorItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; @@ -36,8 +37,8 @@ public class DyeableArmorItem extends ArmorItem { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); // Note that this is handled as of 1.20.5 in the ItemColors class. // But horse leather armor and body leather armor are now both armor items. So it works! diff --git a/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java index f5ddb698b..bc6dcde67 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java @@ -32,6 +32,7 @@ import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.item.BedrockEnchantment; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -50,8 +51,8 @@ public class EnchantedBookItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); List bedrockEnchants = new ArrayList<>(); ItemEnchantments enchantments = components.get(DataComponentTypes.STORED_ENCHANTMENTS); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java index 265d3aad7..a0060044f 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java @@ -31,6 +31,7 @@ import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.level.FireworkColor; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -48,8 +49,8 @@ public class FireworkRocketItem extends Item implements BedrockRequiresTagItem { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); Fireworks fireworks = components.get(DataComponentTypes.FIREWORKS); if (fireworks == null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java index 170d386fd..ff6a19e61 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; @@ -40,8 +41,8 @@ public class FireworkStarItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); Fireworks.FireworkExplosion explosion = components.get(DataComponentTypes.FIREWORK_EXPLOSION); if (explosion != null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FishingRodItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FishingRodItem.java index 32b1d5df5..a066fdbab 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FishingRodItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FishingRodItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; @@ -36,8 +37,8 @@ public class FishingRodItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); // Fix damage inconsistency builder.getDamage().ifPresent(damage -> builder.setDamage(getBedrockDamage(damage))); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java b/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java index e4173d2bb..7d0cfa796 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/GoatHornItem.java @@ -29,6 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.item.GeyserInstrument; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; @@ -63,12 +64,11 @@ public class GoatHornItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); InstrumentComponent component = components.get(DataComponentTypes.INSTRUMENT); - // TODO 1.21.5 hiding???? - if (component != null) { + if (component != null && tooltip.showInTooltip(DataComponentTypes.INSTRUMENT)) { GeyserInstrument instrument = GeyserInstrument.fromComponent(session, component); if (instrument.bedrockInstrument() == null) { builder.getOrCreateLore().add(instrument.description()); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 2c3303689..9919aa308 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -37,6 +37,7 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.Registries; @@ -46,6 +47,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.item.BedrockItemBuilder; +import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; @@ -155,15 +157,14 @@ public class Item { /** * Takes components from Java Edition and map them into Bedrock. */ - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { List loreComponents = components.get(DataComponentTypes.LORE); - // TODO 1.21.5 -// if (loreComponents != null && components.get(DataComponentTypes.HIDE_TOOLTIP) == null) { -// List lore = builder.getOrCreateLore(); -// for (Component loreComponent : loreComponents) { -// lore.add(MessageTranslator.convertMessage(loreComponent, session.locale())); -// } -// } + if (loreComponents != null && tooltip.showInTooltip(DataComponentTypes.LORE)) { + List lore = builder.getOrCreateLore(); + for (Component loreComponent : loreComponents) { + lore.add(MessageTranslator.convertMessage(loreComponent, session.locale())); + } + } Integer damage = components.get(DataComponentTypes.DAMAGE); if (damage != null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/MapItem.java b/core/src/main/java/org/geysermc/geyser/item/type/MapItem.java index f19da5968..332c5210c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/MapItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/MapItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; @@ -37,8 +38,8 @@ public class MapItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); Integer mapValue = components.get(DataComponentTypes.MAP_ID); if (mapValue == null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java index 502d9be0d..7f29751a4 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.session.GeyserSession; @@ -42,8 +43,8 @@ public class PlayerHeadItem extends BlockItem { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); // Use the correct color, determined by the rarity of the item char rarity = Rarity.fromId(components.get(DataComponentTypes.RARITY)).getColor(); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ShieldItem.java b/core/src/main/java/org/geysermc/geyser/item/type/ShieldItem.java index 01cea9c17..9d44920f0 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/ShieldItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/ShieldItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer; @@ -40,8 +41,8 @@ public class ShieldItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); List patterns = components.get(DataComponentTypes.BANNER_PATTERNS); if (patterns != null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java b/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java index a53a9b7bc..e2e910b17 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/ShulkerBoxItem.java @@ -32,6 +32,7 @@ import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; @@ -53,8 +54,8 @@ public class ShulkerBoxItem extends BlockItem { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); List contents = components.get(DataComponentTypes.CONTAINER); if (contents == null || contents.isEmpty()) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/TropicalFishBucketItem.java b/core/src/main/java/org/geysermc/geyser/item/type/TropicalFishBucketItem.java index a93cc5934..011d5bb22 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/TropicalFishBucketItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/TropicalFishBucketItem.java @@ -30,8 +30,8 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextDecoration; import org.checkerframework.checker.nullness.qual.NonNull; -import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.entity.type.living.animal.TropicalFishEntity; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.item.BedrockItemBuilder; @@ -49,37 +49,54 @@ public class TropicalFishBucketItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); // Prevent name from appearing as "Bucket of" builder.putByte("AppendCustomName", (byte) 1); builder.putString("CustomName", MinecraftLocale.getLocaleString("entity.minecraft.tropical_fish", session.locale())); + // Add Java's client side lore tag - // Do you know how frequently Java NBT used to be before 1.20.5? It was a lot. And now it's just this lowly check. - NbtMap entityTag = components.get(DataComponentTypes.BUCKET_ENTITY_DATA); - if (entityTag != null && !entityTag.isEmpty()) { - //TODO test - int bucketVariant = entityTag.getInt("BucketVariantTag"); + Integer pattern = components.get(DataComponentTypes.TROPICAL_FISH_PATTERN); + Integer baseColor = components.get(DataComponentTypes.TROPICAL_FISH_BASE_COLOR); + Integer patternColor = components.get(DataComponentTypes.TROPICAL_FISH_PATTERN_COLOR); + + // The pattern component decides whether to show the tooltip of all 3 components, as of Java 1.21.5 + if ((pattern != null || (baseColor != null && patternColor != null)) && tooltip.showInTooltip(DataComponentTypes.TROPICAL_FISH_PATTERN)) { + //TODO test this for 1.21.5 + int packedVariant = getPackedVariant(pattern, baseColor, patternColor); List lore = builder.getOrCreateLore(); - int predefinedVariantId = TropicalFishEntity.getPredefinedId(bucketVariant); + int predefinedVariantId = TropicalFishEntity.getPredefinedId(packedVariant); if (predefinedVariantId != -1) { - Component tooltip = Component.translatable("entity.minecraft.tropical_fish.predefined." + predefinedVariantId, LORE_STYLE); - lore.add(0, MessageTranslator.convertMessage(tooltip, session.locale())); + Component line = Component.translatable("entity.minecraft.tropical_fish.predefined." + predefinedVariantId, LORE_STYLE); + lore.add(0, MessageTranslator.convertMessage(line, session.locale())); } else { - Component typeTooltip = Component.translatable("entity.minecraft.tropical_fish.type." + TropicalFishEntity.getVariantName(bucketVariant), LORE_STYLE); + Component typeTooltip = Component.translatable("entity.minecraft.tropical_fish.type." + TropicalFishEntity.getVariantName(packedVariant), LORE_STYLE); lore.add(0, MessageTranslator.convertMessage(typeTooltip, session.locale())); - byte baseColor = TropicalFishEntity.getBaseColor(bucketVariant); - byte patternColor = TropicalFishEntity.getPatternColor(bucketVariant); - Component colorTooltip = Component.translatable("color.minecraft." + TropicalFishEntity.getColorName(baseColor), LORE_STYLE); - if (baseColor != patternColor) { - colorTooltip = colorTooltip.append(Component.text(", ", LORE_STYLE)) - .append(Component.translatable("color.minecraft." + TropicalFishEntity.getColorName(patternColor), LORE_STYLE)); + if (baseColor != null && patternColor != null) { + Component colorTooltip = Component.translatable("color.minecraft." + TropicalFishEntity.getColorName(baseColor.byteValue()), LORE_STYLE); + if (!baseColor.equals(patternColor)) { + colorTooltip = colorTooltip.append(Component.text(", ", LORE_STYLE)) + .append(Component.translatable("color.minecraft." + TropicalFishEntity.getColorName(patternColor.byteValue()), LORE_STYLE)); + } + lore.add(1, MessageTranslator.convertMessage(colorTooltip, session.locale())); } - lore.add(1, MessageTranslator.convertMessage(colorTooltip, session.locale())); } } } + + private static int getPackedVariant(Integer pattern, Integer baseColor, Integer patternColor) { + if (pattern == null) { + pattern = 0; + } + if (baseColor == null) { + baseColor = 0; + } + if (patternColor == null) { + patternColor = 0; + } + return TropicalFishEntity.getPackedVariant(pattern, baseColor, patternColor); + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/WolfArmorItem.java b/core/src/main/java/org/geysermc/geyser/item/type/WolfArmorItem.java index 41c72f532..086bf3a9c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/WolfArmorItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/WolfArmorItem.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.item.type; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; @@ -36,8 +37,8 @@ public class WolfArmorItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); // Note that this is handled as of 1.21 in the ItemColors class. translateDyedColor(components, builder); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/WritableBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/WritableBookItem.java index 177ca0b2a..78a744a87 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/WritableBookItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/WritableBookItem.java @@ -29,6 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -46,8 +47,8 @@ public class WritableBookItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); WritableBookContent bookContent = components.get(DataComponentTypes.WRITABLE_BOOK_CONTENT); if (bookContent == null) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java index 9cb661e70..22064e44c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -51,8 +52,8 @@ public class WrittenBookItem extends Item { } @Override - public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull BedrockItemBuilder builder) { - super.translateComponentsToBedrock(session, components, builder); + public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { + super.translateComponentsToBedrock(session, components, tooltip, builder); WrittenBookContent bookContent = components.get(DataComponentTypes.WRITTEN_BOOK_CONTENT); if (bookContent == null) { 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 ecd293bff..1f0de4444 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 @@ -51,6 +51,7 @@ import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.session.cache.registry.JavaRegistry; import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.session.cache.registry.RegistryEntryData; import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry; import org.geysermc.geyser.text.ChatDecoration; import org.geysermc.geyser.translator.level.BiomeTranslator; @@ -189,7 +190,7 @@ public final class RegistryCache { entryIdMap.put(entries.get(i).getId(), i); } - List builder = new ArrayList<>(entries.size()); + List> builder = new ArrayList<>(entries.size()); for (int i = 0; i < entries.size(); i++) { RegistryEntry entry = entries.get(i); // If the data is null, that's the server telling us we need to use our default values. @@ -203,7 +204,7 @@ public final class RegistryCache { RegistryEntryContext context = new RegistryEntryContext(entry, entryIdMap, registryCache.session); // This is what Geyser wants to keep as a value for this registry. T cacheEntry = reader.apply(context); - builder.add(i, cacheEntry); + builder.add(i, new RegistryEntryData<>(entry.getId(), cacheEntry)); } localCache.reset(builder); }); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistry.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistry.java index d7c7782ea..e51dbf043 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistry.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.session.cache.registry; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.index.qual.NonNegative; import java.util.List; @@ -39,15 +40,30 @@ public interface JavaRegistry { */ T byId(@NonNegative int id); + /** + * Looks up a registry entry by its key. The object can be null, or not present. + */ + T byKey(Key key); + + /** + * Looks up a registry entry by its ID, and returns it wrapped in {@link RegistryEntryData} so that its registered key is also known. The object can be null, or not present. + */ + RegistryEntryData entryById(@NonNegative int id); + /** * Reverse looks-up an object to return its network ID, or -1. */ int byValue(T value); + /** + * Reverse looks-up an object to return it wrapped in {@link RegistryEntryData}, or null. + */ + RegistryEntryData entryByValue(T value); + /** * Resets the objects by these IDs. */ - void reset(List values); + void reset(List> values); /** * All values of this registry, as a list. diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java index 369bea7a4..364d998ee 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java @@ -26,10 +26,9 @@ package org.geysermc.geyser.session.cache.registry; import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.session.GeyserSession; -import javax.annotation.Nullable; - /** * Defines a Java registry, which can be hardcoded or data-driven. This class doesn't store registry contents itself, that is handled by {@link org.geysermc.geyser.session.cache.RegistryCache} in the case of * data-driven registries and other classes in the case of hardcoded registries. diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryData.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryData.java new file mode 100644 index 000000000..ed0b2fdec --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/RegistryEntryData.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.session.cache.registry; + +import net.kyori.adventure.key.Key; + +public record RegistryEntryData(Key key, T data) { +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/SimpleJavaRegistry.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/SimpleJavaRegistry.java index 7b79a40be..3e3d8ba6c 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/SimpleJavaRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/SimpleJavaRegistry.java @@ -26,15 +26,34 @@ package org.geysermc.geyser.session.cache.registry; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.index.qual.NonNegative; import java.util.List; public class SimpleJavaRegistry implements JavaRegistry { - protected final ObjectArrayList values = new ObjectArrayList<>(); + protected final ObjectArrayList> values = new ObjectArrayList<>(); @Override public T byId(@NonNegative int id) { + if (id < 0 || id >= this.values.size()) { + return null; + } + return this.values.get(id).data(); + } + + @Override + public T byKey(Key key) { + for (RegistryEntryData entry : values) { + if (entry.key().equals(key)) { + return entry.data(); + } + } + return null; + } + + @Override + public RegistryEntryData entryById(@NonNegative int id) { if (id < 0 || id >= this.values.size()) { return null; } @@ -43,11 +62,26 @@ public class SimpleJavaRegistry implements JavaRegistry { @Override public int byValue(T value) { - return this.values.indexOf(value); + for (int i = 0; i < this.values.size(); i++) { + if (values.get(i).data().equals(value)) { + return i; + } + } + return -1; } @Override - public void reset(List values) { + public RegistryEntryData entryByValue(T value) { + for (RegistryEntryData entry : this.values) { + if (entry.data().equals(value)) { + return entry; + } + } + return null; + } + + @Override + public void reset(List> values) { this.values.clear(); this.values.addAll(values); this.values.trim(); @@ -55,7 +89,7 @@ public class SimpleJavaRegistry implements JavaRegistry { @Override public List values() { - return this.values; + return this.values.stream().map(RegistryEntryData::data).toList(); } @Override 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 9d01f0d8c..04f5f5bb3 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 @@ -43,8 +43,10 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.item.type.PotionItem; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -168,37 +170,32 @@ public final class ItemTranslator { public static ItemData.@NonNull Builder translateToBedrock(GeyserSession session, Item javaItem, ItemMapping bedrockItem, int count, @Nullable DataComponents customComponents) { BedrockItemBuilder nbtBuilder = new BedrockItemBuilder(); - // TODO 1.21.5: - // - Hiding components - // Populates default components that aren't sent over the network DataComponents components = javaItem.gatherComponents(customComponents); + TooltipOptions tooltip = TooltipOptions.fromComponents(components); // Translate item-specific components - javaItem.translateComponentsToBedrock(session, components, nbtBuilder); + javaItem.translateComponentsToBedrock(session, components, tooltip, nbtBuilder); Rarity rarity = Rarity.fromId(components.getOrDefault(DataComponentTypes.RARITY, 0)); String customName = getCustomName(session, customComponents, bedrockItem, rarity.getColor(), false, false); if (customName != null) { PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); - // Make custom effect information visible - // Ignore when item have "hide_additional_tooltip" component - if (potionContents != null) { // && components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) == null) { - customName += getPotionEffectInfo(potionContents, session.locale()); + // Make custom effect information visible when shown in tooltip + if (potionContents != null && tooltip.showInTooltip(DataComponentTypes.POTION_CONTENTS)) { + customName += getPotionEffectInfo(potionContents, session.locale()); // TODO should this be done with lore instead? } nbtBuilder.setCustomName(customName); } - //boolean hideTooltips = components.get(DataComponentTypes.HIDE_TOOLTIP) != null; - ItemAttributeModifiers attributeModifiers = components.get(DataComponentTypes.ATTRIBUTE_MODIFIERS); - if (attributeModifiers != null) { //&& attributeModifiers.isShowInTooltip() && !hideTooltips) { + if (attributeModifiers != null && tooltip.showInTooltip(DataComponentTypes.ATTRIBUTE_MODIFIERS )) { // only add if attribute modifiers do not indicate to hide them addAttributeLore(session, attributeModifiers, nbtBuilder, session.locale()); } - if (session.isAdvancedTooltips()) { //&& !hideTooltips) { + if (session.isAdvancedTooltips() && !TooltipOptions.hideTooltip(components)) { addAdvancedTooltips(components, nbtBuilder, javaItem, session.locale()); } @@ -391,7 +388,7 @@ public final class ItemTranslator { return finalText.toString(); } - public static String getPotionName(PotionContents contents, ItemMapping mapping, boolean hideAdditionalTooltip, String language) { + public static String getPotionName(PotionContents contents, ItemMapping mapping, String language) { String customPotionName = contents.getCustomName(); Potion potion = Potion.getByJavaId(contents.getPotionId()); @@ -401,22 +398,10 @@ public final class ItemTranslator { Component.translatable(mapping.getJavaItem().translationKey() + ".effect." + customPotionName), language); } - if (!hideAdditionalTooltip && !contents.getCustomEffects().isEmpty()) { + if (!contents.getCustomEffects().isEmpty()) { // Make a name when has custom effects - String potionName; - if (potion != null) { - potionName = potion.toString().toLowerCase(Locale.ROOT); - if (potionName.startsWith("strong_")) { - potionName = potionName.substring(6); - } else if (potionName.startsWith("long_")) { - potionName = potionName.substring(4); - } - } else { - potionName = "empty"; - } - return MessageTranslator.convertMessage( - Component.translatable(mapping.getJavaItem().translationKey() + ".effect." + potionName), - language); + String potionName = potion == null ? "empty" : potion.toString().toLowerCase(Locale.ROOT); + return MessageTranslator.convertMessage(Component.translatable(mapping.getJavaItem().translationKey() + ".effect." + potionName), language); } return null; } @@ -538,22 +523,31 @@ public final class ItemTranslator { * @param translationColor if this item is not available on Java, the color that the new name should be. * Normally, this should just be white, but for shulker boxes this should be gray. */ - public static String getCustomName(GeyserSession session, DataComponents components, ItemMapping mapping, char translationColor, boolean customNameOnly, boolean includeAll) { + public static String getCustomName(GeyserSession session, DataComponents components, ItemMapping mapping, + char translationColor, boolean customNameOnly, boolean includeAll) { if (components != null) { + // If the tooltip is hidden entirely, return an empty custom name + if (TooltipOptions.hideTooltip(components)) { + return ""; // TODO test this + } + // ItemStack#getHoverName as of 1.20.5 Component customName = components.get(DataComponentTypes.CUSTOM_NAME); if (customName != null) { return MessageTranslator.convertMessage(customName, session.locale()); } + if (!customNameOnly) { - PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); - if (potionContents != null) { - // TODO 1.21.5 - String potionName = getPotionName(potionContents, mapping, false /*components.get(DataComponentTypes.HIDE_ADDITIONAL_TOOLTIP) != null */, session.locale()); - if (potionName != null) { - return ChatColor.RESET + ChatColor.ESCAPE + translationColor + potionName; + if (mapping.getJavaItem() instanceof PotionItem) { + PotionContents potionContents = components.get(DataComponentTypes.POTION_CONTENTS); + if (potionContents != null) { + String potionName = getPotionName(potionContents, mapping, session.locale()); // TODO also test this + if (potionName != null) { + return ChatColor.RESET + ChatColor.ESCAPE + translationColor + potionName; + } } } + if (includeAll) { // Fix book title display in tooltips of shulker box WrittenBookContent bookContent = components.get(DataComponentTypes.WRITTEN_BOOK_CONTENT); @@ -561,6 +555,7 @@ public final class ItemTranslator { return ChatColor.RESET + ChatColor.ESCAPE + translationColor + bookContent.getTitle().getRaw(); } } + customName = components.get(DataComponentTypes.ITEM_NAME); if (customName != null) { // Get the translated name and prefix it with a reset char to prevent italics - matches Java Edition 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 c44f21213..d3e2ba5df 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 @@ -30,6 +30,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import org.cloudburstmc.math.vector.Vector3d; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; +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; @@ -40,6 +41,8 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.InventoryTra import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetItemSlotData; import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket; import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket; +import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.Entity; @@ -49,6 +52,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.click.Click; +import org.geysermc.geyser.inventory.item.GeyserInstrument; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.BlockItem; import org.geysermc.geyser.item.type.BoatItem; @@ -74,12 +78,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.geyser.util.SoundUtils; 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.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; 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; @@ -380,30 +387,29 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator holder = session.getPlayerInventory() -// .getItemInHand() -// .getComponent(DataComponentTypes.INSTRUMENT); -// if (holder != null) { -// GeyserInstrument instrument = GeyserInstrument.fromComponent(session, holder); -// if (instrument.bedrockInstrument() != null) { -// // 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.bedrockInstrument().ordinal())); -// soundPacket.setPosition(session.getPlayerEntity().getPosition()); -// soundPacket.setIdentifier("minecraft:player"); -// soundPacket.setExtraData(-1); -// session.sendUpstreamPacket(soundPacket); -// } else { -// PlaySoundPacket playSoundPacket = new PlaySoundPacket(); -// playSoundPacket.setPosition(session.getPlayerEntity().position()); -// playSoundPacket.setSound(SoundUtils.translatePlaySound(instrument.soundEvent())); -// playSoundPacket.setPitch(1.0F); -// playSoundPacket.setVolume(instrument.range() / 16.0F); -// session.sendUpstreamPacket(playSoundPacket); -// } -// } + InstrumentComponent component = session.getPlayerInventory() + .getItemInHand() + .getComponent(DataComponentTypes.INSTRUMENT); + if (component != null) { + GeyserInstrument instrument = GeyserInstrument.fromComponent(session, component); + if (instrument.bedrockInstrument() != null) { + // 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.bedrockInstrument().ordinal())); + soundPacket.setPosition(session.getPlayerEntity().getPosition()); + soundPacket.setIdentifier("minecraft:player"); + soundPacket.setExtraData(-1); + session.sendUpstreamPacket(soundPacket); + } else { + PlaySoundPacket playSoundPacket = new PlaySoundPacket(); + playSoundPacket.setPosition(session.getPlayerEntity().position()); + playSoundPacket.setSound(SoundUtils.translatePlaySound(instrument.soundEvent())); + playSoundPacket.setPitch(1.0F); + playSoundPacket.setVolume(instrument.range() / 16.0F); + session.sendUpstreamPacket(playSoundPacket); + } + } } } } From c398d5c62ce25c73afba93db9c1dfc38b3c09e9a Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 08:39:44 +0000 Subject: [PATCH 15/86] Work on farmland variants, not sure if this will work --- .../geyser/entity/EntityDefinitions.java | 6 +- .../type/living/animal/MooshroomEntity.java | 1 + .../animal/{ => farm}/ChickenEntity.java | 14 ++- .../living/animal/{ => farm}/CowEntity.java | 15 +++- .../living/animal/{ => farm}/PigEntity.java | 14 +-- .../animal/farm/TemperatureVariantAnimal.java | 90 +++++++++++++++++++ .../geyser/session/cache/RegistryCache.java | 9 ++ .../cache/registry/JavaRegistries.java | 6 +- .../inventory/LoomInventoryTranslator.java | 2 +- 9 files changed, 141 insertions(+), 16 deletions(-) rename core/src/main/java/org/geysermc/geyser/entity/type/living/animal/{ => farm}/ChickenEntity.java (81%) rename core/src/main/java/org/geysermc/geyser/entity/type/living/animal/{ => farm}/CowEntity.java (87%) rename core/src/main/java/org/geysermc/geyser/entity/type/living/animal/{ => farm}/PigEntity.java (92%) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.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 baf6a9990..05024f274 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -80,8 +80,8 @@ import org.geysermc.geyser.entity.type.living.TadpoleEntity; import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity; import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity; import org.geysermc.geyser.entity.type.living.animal.BeeEntity; -import org.geysermc.geyser.entity.type.living.animal.ChickenEntity; -import org.geysermc.geyser.entity.type.living.animal.CowEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.entity.type.living.animal.FoxEntity; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.GoatEntity; @@ -89,7 +89,7 @@ import org.geysermc.geyser.entity.type.living.animal.HoglinEntity; import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity; import org.geysermc.geyser.entity.type.living.animal.OcelotEntity; import org.geysermc.geyser.entity.type.living.animal.PandaEntity; -import org.geysermc.geyser.entity.type.living.animal.PigEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; import org.geysermc.geyser.entity.type.living.animal.PolarBearEntity; import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity; import org.geysermc.geyser.entity.type.living.animal.RabbitEntity; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index dce1adf79..622c599e7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -30,6 +30,7 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java similarity index 81% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java index 231c408d6..afd8a6ce6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -23,21 +23,24 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.properties.VanillaEntityProperties; +import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import java.util.UUID; -public class ChickenEntity extends AnimalEntity { +public class ChickenEntity extends TemperatureVariantAnimal { public ChickenEntity(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); @@ -54,4 +57,9 @@ public class ChickenEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.CHICKEN_FOOD; } + + @Override + protected JavaRegistryKey variantRegistry() { + return JavaRegistries.CHICKEN_VARIANT; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java similarity index 87% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index 6c83b9dd1..8f7740f90 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -33,10 +33,13 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.properties.VanillaEntityProperties; +import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; @@ -45,7 +48,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CowEntity extends AnimalEntity { +public class CowEntity extends TemperatureVariantAnimal { + public CowEntity(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); } @@ -82,4 +86,9 @@ public class CowEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.COW_FOOD; } + + @Override + protected JavaRegistryKey variantRegistry() { + return JavaRegistries.COW_VARIANT; + } } 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/farm/PigEntity.java similarity index 92% rename from core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java rename to core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index fb7ef3357..4508a0158 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/farm/PigEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 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 @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.entity.type.living.animal; +package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,6 +35,7 @@ import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.Tickable; +import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; import org.geysermc.geyser.entity.vehicle.ClientVehicle; @@ -43,6 +44,8 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; @@ -57,7 +60,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { +public class PigEntity extends TemperatureVariantAnimal 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) { @@ -160,7 +163,8 @@ public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle { return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK); } - public void setVariant(EntityMetadata,? extends MetadataType>> holderEntityMetadata) { - // TODO + @Override + protected JavaRegistryKey variantRegistry() { + return JavaRegistries.PIG_VARIANT; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java new file mode 100644 index 000000000..7b675ef98 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal.farm; + +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; + +import java.util.Locale; +import java.util.UUID; +import java.util.function.Function; + +public abstract class TemperatureVariantAnimal extends AnimalEntity { + + public TemperatureVariantAnimal(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); + } + + protected abstract JavaRegistryKey variantRegistry(); + + public void setVariant(EntityMetadata, ? extends MetadataType>> variant) { + BuiltInVariant animalVariant; + if (variant.getValue().isId()) { + animalVariant = variantRegistry().fromNetworkId(session, variant.getValue().id()); + if (animalVariant == null) { + animalVariant = BuiltInVariant.TEMPERATE; + } + } else { + animalVariant = BuiltInVariant.TEMPERATE; + } + dirtyMetadata.put(EntityDataTypes.VARIANT, animalVariant.ordinal()); + } + + public enum BuiltInVariant { + COLD, + TEMPERATE, + WARM; + + public static final Function READER = context -> getByJavaIdentifier(context.id()); + + private final Key javaIdentifier; + + BuiltInVariant() { + javaIdentifier = MinecraftKey.key(name().toLowerCase(Locale.ROOT)); + } + + public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { + for (BuiltInVariant variant : values()) { + if (variant.javaIdentifier.equals(identifier)) { + return variant; + } + } + return null; + } + } +} 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 1f0de4444..979da99fb 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 @@ -38,6 +38,7 @@ import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.inventory.item.GeyserInstrument; @@ -94,6 +95,10 @@ public final class RegistryCache { register("banner_pattern", cache -> cache.bannerPatterns, context -> BannerPattern.getByJavaIdentifier(context.id())); register("wolf_variant", cache -> cache.wolfVariants, context -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(context.id().asString())); + register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, TemperatureVariantAnimal.BuiltInVariant.READER); + register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, TemperatureVariantAnimal.BuiltInVariant.READER); + register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, TemperatureVariantAnimal.BuiltInVariant.READER); + // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); Map> defaults = new HashMap<>(); @@ -134,6 +139,10 @@ public final class RegistryCache { private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); private final JavaRegistry instruments = new SimpleJavaRegistry<>(); + private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry chickenVariants = new SimpleJavaRegistry<>(); + public RegistryCache(GeyserSession session) { this.session = session; } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index 2792589c7..224c60348 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -27,6 +27,7 @@ package org.geysermc.geyser.session.cache.registry; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.item.type.Item; @@ -49,7 +50,10 @@ public class JavaRegistries { public static final JavaRegistryKey BLOCK = create("block", BlockRegistries.JAVA_BLOCKS, Block::javaId); public static final JavaRegistryKey ITEM = create("item", Registries.JAVA_ITEMS, Item::javaId); public static final JavaRegistryKey ENCHANTMENT = create("enchantment", RegistryCache::enchantments); - public static final JavaRegistryKey BANNER_PATTERNS = create("banner_pattern", RegistryCache::bannerPatterns); + public static final JavaRegistryKey BANNER_PATTERN = create("banner_pattern", RegistryCache::bannerPatterns); + public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); + public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); + public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); private static JavaRegistryKey create(String key, JavaRegistryKey.NetworkSerializer networkSerializer, JavaRegistryKey.NetworkDeserializer networkDeserializer) { JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), networkSerializer, networkDeserializer); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java index cf507b793..8ab21e8c4 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java @@ -60,7 +60,7 @@ import java.util.List; public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator { - private static final Tag NO_ITEMS_REQUIRED = new Tag<>(JavaRegistries.BANNER_PATTERNS, Key.key("no_item_required")); + private static final Tag NO_ITEMS_REQUIRED = new Tag<>(JavaRegistries.BANNER_PATTERN, Key.key("no_item_required")); public LoomInventoryTranslator() { super(4, Blocks.LOOM, ContainerType.LOOM, UIInventoryUpdater.INSTANCE); From d19fa2aa956f67a0998caebadb6e295394b5353a Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 09:35:59 +0000 Subject: [PATCH 16/86] Implement data driven cats --- .../geyser/entity/EntityDefinitions.java | 1 + .../animal/farm/TemperatureVariantAnimal.java | 3 + .../living/animal/tameable/CatEntity.java | 56 ++++++++++++++++--- .../geyser/session/cache/RegistryCache.java | 7 ++- .../cache/registry/JavaRegistries.java | 2 + 5 files changed, 60 insertions(+), 9 deletions(-) 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 05024f274..af3aecd18 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -967,6 +967,7 @@ public final class EntityDefinitions { .type(EntityType.COW) .height(1.4f).width(0.9f) .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant) .build(); FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase) .type(EntityType.FOX) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index 7b675ef98..e6dc63e72 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -62,9 +62,12 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity { } else { animalVariant = BuiltInVariant.TEMPERATE; } + // TODO does this work? dirtyMetadata.put(EntityDataTypes.VARIANT, animalVariant.ordinal()); } + // Ordered by bedrock id + // TODO: are these ordered correctly? Does the order differ for mobs? public enum BuiltInVariant { COLD, TEMPERATE, diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java index fb53c18ed..b5f7ffb19 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; @@ -34,16 +35,21 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.MinecraftKey; 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.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import java.util.Locale; import java.util.UUID; +import java.util.function.Function; public class CatEntity extends TameableEntity { @@ -81,17 +87,17 @@ public class CatEntity extends TameableEntity { updateCollarColor(); } + // TODO this is a holder when MCPL updates + // TODO also checks if this works public void setCatVariant(IntEntityMetadata entityMetadata) { // Different colors in Java and Bedrock for some reason int metadataValue = entityMetadata.getPrimitiveValue(); - int variantColor = switch (metadataValue) { - case 0 -> 8; - case 8 -> 0; - case 9 -> 10; - case 10 -> 9; - default -> metadataValue; - }; - dirtyMetadata.put(EntityDataTypes.VARIANT, variantColor); + + BuiltInVariant variant = JavaRegistries.CAT_VARIANT.fromNetworkId(session, metadataValue); + if (variant == null) { + variant = BuiltInVariant.BLACK; // Default variant on Java + } + dirtyMetadata.put(EntityDataTypes.VARIANT, variant.ordinal()); } public void setResting(BooleanEntityMetadata entityMetadata) { @@ -138,4 +144,38 @@ public class CatEntity extends TameableEntity { return !canEat(itemInHand) || health >= maxHealth && tamed ? InteractionResult.PASS : InteractionResult.SUCCESS; } } + + // Ordered by bedrock id + // TODO: are these ordered correctly? + // TODO lessen code duplication with other variant mobs + public enum BuiltInVariant { + WHITE, + BLACK, + RED, + SIAMESE, + BRITISH_SHORTHAIR, + CALICO, + PERSIAN, + RAGDOLL, + TABBY, + ALL_BLACK, + JELLIE; + + public static final Function READER = context -> getByJavaIdentifier(context.id()); + + private final Key javaIdentifier; + + BuiltInVariant() { + javaIdentifier = MinecraftKey.key(name().toLowerCase(Locale.ROOT)); + } + + public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { + for (BuiltInVariant variant : values()) { + if (variant.javaIdentifier.equals(identifier)) { + return variant; + } + } + return null; + } + } } 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 979da99fb..49e0d5566 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 @@ -39,6 +39,7 @@ import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.inventory.item.GeyserInstrument; @@ -95,6 +96,8 @@ public final class RegistryCache { register("banner_pattern", cache -> cache.bannerPatterns, context -> BannerPattern.getByJavaIdentifier(context.id())); register("wolf_variant", cache -> cache.wolfVariants, context -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(context.id().asString())); + register(JavaRegistries.CAT_VARIANT, cache -> cache.catVariants, CatEntity.BuiltInVariant.READER); + register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, TemperatureVariantAnimal.BuiltInVariant.READER); register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, TemperatureVariantAnimal.BuiltInVariant.READER); register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, TemperatureVariantAnimal.BuiltInVariant.READER); @@ -136,9 +139,11 @@ public final class RegistryCache { private final JavaRegistry trimPatterns = new SimpleJavaRegistry<>(); private final JavaRegistry bannerPatterns = new SimpleJavaRegistry<>(); - private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); private final JavaRegistry instruments = new SimpleJavaRegistry<>(); + private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry catVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); private final JavaRegistry chickenVariants = new SimpleJavaRegistry<>(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index 224c60348..fd195b38a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache.registry; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.item.type.Item; @@ -51,6 +52,7 @@ public class JavaRegistries { public static final JavaRegistryKey ITEM = create("item", Registries.JAVA_ITEMS, Item::javaId); public static final JavaRegistryKey ENCHANTMENT = create("enchantment", RegistryCache::enchantments); public static final JavaRegistryKey BANNER_PATTERN = create("banner_pattern", RegistryCache::bannerPatterns); + public static final JavaRegistryKey CAT_VARIANT = create("cat_variant", RegistryCache::catVariants); public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); From 4ebd048411a7b5ca42da3c163838a0359e7c2446 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 09:44:18 +0000 Subject: [PATCH 17/86] Implement data driven frogs, some refactoring with wolf variants --- .../entity/type/living/animal/FrogEntity.java | 45 ++++++++++++++++--- .../living/animal/tameable/WolfEntity.java | 26 ++++++----- .../geyser/session/cache/RegistryCache.java | 7 ++- .../cache/registry/JavaRegistries.java | 6 +++ 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java index a0b909b75..0826b532d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.entity.type.living.animal; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -33,14 +34,19 @@ import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +import java.util.Locale; import java.util.OptionalInt; import java.util.UUID; +import java.util.function.Function; public class FrogEntity extends AnimalEntity { public FrogEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -56,13 +62,15 @@ public class FrogEntity extends AnimalEntity { super.setPose(pose); } + // TODO this is a holder when MCPL updates + // TODO also check if this works public void setFrogVariant(IntEntityMetadata entityMetadata) { - int variant = entityMetadata.getPrimitiveValue(); - dirtyMetadata.put(EntityDataTypes.VARIANT, switch (variant) { - case 1 -> 2; // White - case 2 -> 1; // Green - default -> variant; - }); + BuiltInVariant variant = JavaRegistries.FROG_VARIANT.fromNetworkId(session, entityMetadata.getPrimitiveValue()); + if (variant == null) { + variant = BuiltInVariant.TEMPERATE; + } + + dirtyMetadata.put(EntityDataTypes.VARIANT, variant.ordinal()); } public void setTongueTarget(ObjectEntityMetadata entityMetadata) { @@ -82,4 +90,29 @@ public class FrogEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.FROG_FOOD; } + + // Ordered by bedrock id + // TODO: are these ordered correctly? + public enum BuiltInVariant { + TEMPERATE, + COLD, + WARM; + + public static final Function READER = context -> getByJavaIdentifier(context.id()); + + private final Key javaIdentifier; + + BuiltInVariant() { + javaIdentifier = MinecraftKey.key(name().toLowerCase(Locale.ROOT)); + } + + public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { + for (BuiltInVariant variant : values()) { + if (variant.javaIdentifier.equals(identifier)) { + return variant; + } + } + return null; + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index 0fb742f71..2e142b72e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; +import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; @@ -39,11 +40,14 @@ import org.geysermc.geyser.item.enchantment.EnchantmentComponent; import org.geysermc.geyser.item.type.DyeItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.WolfVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; @@ -58,6 +62,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.Collections; import java.util.Locale; import java.util.UUID; +import java.util.function.Function; public class WolfEntity extends TameableEntity { private byte collarColor = 14; // Red - default @@ -116,10 +121,11 @@ public class WolfEntity extends TameableEntity { // 1.20.5+ public void setWolfVariant(ObjectEntityMetadata> entityMetadata) { + // TODO set to pale if custom holder? entityMetadata.getValue().ifId(id -> { - BuiltInWolfVariant wolfVariant = session.getRegistryCache().wolfVariants().byId(id); + BuiltInVariant wolfVariant = JavaRegistries.WOLF_VARIANT.fromNetworkId(session, id); if (wolfVariant == null) { - wolfVariant = BuiltInWolfVariant.PALE; + wolfVariant = BuiltInVariant.PALE; } dirtyMetadata.put(EntityDataTypes.VARIANT, wolfVariant.ordinal()); }); @@ -195,7 +201,7 @@ public class WolfEntity extends TameableEntity { } // Ordered by bedrock id - public enum BuiltInWolfVariant { + public enum BuiltInVariant { PALE, ASHEN, BLACK, @@ -206,17 +212,17 @@ public class WolfEntity extends TameableEntity { STRIPED, WOODS; - private static final BuiltInWolfVariant[] VALUES = values(); + public static final Function READER = context -> getByJavaIdentifier(context.id()); - private final String javaIdentifier; + private final Key javaIdentifier; - BuiltInWolfVariant() { - this.javaIdentifier = "minecraft:" + this.name().toLowerCase(Locale.ROOT); + BuiltInVariant() { + this.javaIdentifier = MinecraftKey.key(this.name().toLowerCase(Locale.ROOT)); } - public static @Nullable BuiltInWolfVariant getByJavaIdentifier(String javaIdentifier) { - for (BuiltInWolfVariant wolfVariant : VALUES) { - if (wolfVariant.javaIdentifier.equals(javaIdentifier)) { + public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { + for (BuiltInVariant wolfVariant : values()) { + if (wolfVariant.javaIdentifier.equals(identifier)) { return wolfVariant; } } 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 49e0d5566..4fbcbf08d 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 @@ -38,6 +38,7 @@ import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; @@ -94,9 +95,10 @@ public final class RegistryCache { register("trim_pattern", cache -> cache.trimPatterns, TrimRecipe::readTrimPattern); register("worldgen/biome", (cache, array) -> cache.biomeTranslations = array, BiomeTranslator::loadServerBiome); register("banner_pattern", cache -> cache.bannerPatterns, context -> BannerPattern.getByJavaIdentifier(context.id())); - register("wolf_variant", cache -> cache.wolfVariants, context -> WolfEntity.BuiltInWolfVariant.getByJavaIdentifier(context.id().asString())); register(JavaRegistries.CAT_VARIANT, cache -> cache.catVariants, CatEntity.BuiltInVariant.READER); + register(JavaRegistries.FROG_VARIANT, cache -> cache.frogVariants, FrogEntity.BuiltInVariant.READER); + register(JavaRegistries.WOLF_VARIANT, cache -> cache.wolfVariants, WolfEntity.BuiltInVariant.READER); register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, TemperatureVariantAnimal.BuiltInVariant.READER); register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, TemperatureVariantAnimal.BuiltInVariant.READER); @@ -141,8 +143,9 @@ public final class RegistryCache { private final JavaRegistry bannerPatterns = new SimpleJavaRegistry<>(); private final JavaRegistry instruments = new SimpleJavaRegistry<>(); - private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); private final JavaRegistry catVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry frogVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index fd195b38a..732a2cc4a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -27,8 +27,10 @@ package org.geysermc.geyser.session.cache.registry; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; +import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.item.type.Item; @@ -52,7 +54,11 @@ public class JavaRegistries { public static final JavaRegistryKey ITEM = create("item", Registries.JAVA_ITEMS, Item::javaId); public static final JavaRegistryKey ENCHANTMENT = create("enchantment", RegistryCache::enchantments); public static final JavaRegistryKey BANNER_PATTERN = create("banner_pattern", RegistryCache::bannerPatterns); + public static final JavaRegistryKey CAT_VARIANT = create("cat_variant", RegistryCache::catVariants); + public static final JavaRegistryKey FROG_VARIANT = create("frog_variant", RegistryCache::frogVariants); + public static final JavaRegistryKey WOLF_VARIANT = create("wolf_variant", RegistryCache::wolfVariants); + public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); From c8c09c9b7dc9b8db484efbf70e72ee7b3083fd8e Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 10:46:14 +0000 Subject: [PATCH 18/86] Work on abstracting entity variants to reduce code duplication --- .../entity/type/living/animal/FrogEntity.java | 51 +++---- .../type/living/animal/VariantHolder.java | 126 ++++++++++++++++++ .../animal/farm/TemperatureVariantAnimal.java | 5 +- .../living/animal/tameable/CatEntity.java | 54 +++----- .../living/animal/tameable/WolfEntity.java | 55 +++----- .../geyser/session/cache/RegistryCache.java | 7 +- 6 files changed, 186 insertions(+), 112 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java index 0826b532d..d770770f9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living.animal; -import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; @@ -35,20 +34,17 @@ import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistries; -import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; -import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; -import java.util.Locale; import java.util.OptionalInt; import java.util.UUID; -import java.util.function.Function; -public class FrogEntity extends AnimalEntity { +// TODO this is implementing VariantHolder until MCPL updates +public class FrogEntity extends AnimalEntity implements VariantHolder { public FrogEntity(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); } @@ -62,15 +58,19 @@ public class FrogEntity extends AnimalEntity { super.setPose(pose); } - // TODO this is a holder when MCPL updates - // TODO also check if this works - public void setFrogVariant(IntEntityMetadata entityMetadata) { - BuiltInVariant variant = JavaRegistries.FROG_VARIANT.fromNetworkId(session, entityMetadata.getPrimitiveValue()); - if (variant == null) { - variant = BuiltInVariant.TEMPERATE; - } + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.FROG_VARIANT; + } - dirtyMetadata.put(EntityDataTypes.VARIANT, variant.ordinal()); + @Override + public void setBedrockVariant(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); + } + + @Override + public BuiltIn defaultVariant() { + return BuiltInVariant.TEMPERATE; } public void setTongueTarget(ObjectEntityMetadata entityMetadata) { @@ -93,26 +93,9 @@ public class FrogEntity extends AnimalEntity { // Ordered by bedrock id // TODO: are these ordered correctly? - public enum BuiltInVariant { + public enum BuiltInVariant implements BuiltIn { TEMPERATE, COLD, - WARM; - - public static final Function READER = context -> getByJavaIdentifier(context.id()); - - private final Key javaIdentifier; - - BuiltInVariant() { - javaIdentifier = MinecraftKey.key(name().toLowerCase(Locale.ROOT)); - } - - public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { - for (BuiltInVariant variant : values()) { - if (variant.javaIdentifier.equals(identifier)) { - return variant; - } - } - return null; - } + WARM } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java new file mode 100644 index 000000000..f337810fc --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal; + +import net.kyori.adventure.key.Key; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; + +import java.util.Locale; +import java.util.function.Function; + +/** + * Utility interface to help set up data-driven entity variants for mobs. + * + *

This interface is designed for mobs that have their variant wrapped in a {@link Holder}. Implementations usually have to + * implement {@link VariantHolder#variantRegistry()}, {@link VariantHolder#setBedrockVariant(int)}, and {@link VariantHolder#defaultVariant()}, and should also + * have an enum with built-in variants on bedrock (implementing {@link BuiltIn}).

+ * + * @param the MCPL variant class that a {@link Holder} wraps. + */ +public interface VariantHolder { + + default void setVariant(EntityMetadata, ? extends MetadataType>> variant) { + setVariant(variant.getValue()); + } + + /** + * Sets the variant of the entity. Defaults to {@link VariantHolder#defaultVariant()} for custom holders and non-vanilla IDs. + */ + default void setVariant(Holder variant) { + BuiltIn builtInVariant; + if (variant.isId()) { + builtInVariant = variantRegistry().fromNetworkId(getSession(), variant.id()); + if (builtInVariant == null) { + builtInVariant = defaultVariant(); + } + } else { + builtInVariant = defaultVariant(); + } + setBedrockVariant(builtInVariant.ordinal()); + } + + GeyserSession getSession(); + + /** + * The registry in {@link org.geysermc.geyser.session.cache.registry.JavaRegistries} for this mob's variants. The registry can utilise the {@link VariantHolder#reader(Class)} method + * to create a reader to be used in {@link org.geysermc.geyser.session.cache.RegistryCache}. + */ + JavaRegistryKey> variantRegistry(); + + /** + * Should set the variant on bedrock's metadata (or however the variant is set for the mob). The bedrock ID has already been checked and is always valid. + */ + void setBedrockVariant(int bedrockId); + + /** + * Should return the default variant, that is to be used when this mob's variant is a custom or non-vanilla one. + */ + BuiltIn defaultVariant(); + + /** + * Creates a registry reader for this mob's variants. + * + *

This reader simply matches the identifiers of registry entries with built-in variants. If no built-in variant matches, null is returned.

+ */ + static >> Function reader(Class clazz) { + BuiltInVariant[] variants = clazz.getEnumConstants(); + if (variants == null) { + throw new IllegalArgumentException("Class is not an enum"); + } + return context -> { + for (BuiltInVariant variant : variants) { + if (((BuiltIn) variant).javaIdentifier().equals(context.id())) { + return variant; + } + } + return null; + }; + } + + /** + * Should be implemented on an enum within the entity class. The enum lists vanilla variants that can appear on bedrock, in the order of their bedrock network ID. + * + *

The enum constants should be named the same as their Java identifiers.

+ * + * @param the same as the parent entity class. Used for type checking. + */ + interface BuiltIn { + + String name(); + + int ordinal(); + + default Key javaIdentifier() { + return MinecraftKey.key(name().toLowerCase(Locale.ROOT)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index e6dc63e72..a347ea1ac 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -31,6 +31,7 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; +import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; @@ -43,7 +44,7 @@ import java.util.Locale; import java.util.UUID; import java.util.function.Function; -public abstract class TemperatureVariantAnimal extends AnimalEntity { +public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -68,7 +69,7 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity { // Ordered by bedrock id // TODO: are these ordered correctly? Does the order differ for mobs? - public enum BuiltInVariant { + public enum BuiltInVariant implements BuiltIn { COLD, TEMPERATE, WARM; diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java index b5f7ffb19..2582a2eb8 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java @@ -25,33 +25,31 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; -import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistries; -import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; -import org.geysermc.geyser.util.MinecraftKey; 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.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import java.util.Locale; import java.util.UUID; -import java.util.function.Function; -public class CatEntity extends TameableEntity { +// TODO this is implementing VariantHolder until MCPL updates +public class CatEntity extends TameableEntity implements VariantHolder { private byte collarColor = 14; // Red - default @@ -87,17 +85,19 @@ public class CatEntity extends TameableEntity { updateCollarColor(); } - // TODO this is a holder when MCPL updates - // TODO also checks if this works - public void setCatVariant(IntEntityMetadata entityMetadata) { - // Different colors in Java and Bedrock for some reason - int metadataValue = entityMetadata.getPrimitiveValue(); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.CAT_VARIANT; + } - BuiltInVariant variant = JavaRegistries.CAT_VARIANT.fromNetworkId(session, metadataValue); - if (variant == null) { - variant = BuiltInVariant.BLACK; // Default variant on Java - } - dirtyMetadata.put(EntityDataTypes.VARIANT, variant.ordinal()); + @Override + public void setBedrockVariant(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); + } + + @Override + public BuiltIn defaultVariant() { + return BuiltInVariant.BLACK; // Default variant on Java } public void setResting(BooleanEntityMetadata entityMetadata) { @@ -147,8 +147,7 @@ public class CatEntity extends TameableEntity { // Ordered by bedrock id // TODO: are these ordered correctly? - // TODO lessen code duplication with other variant mobs - public enum BuiltInVariant { + public enum BuiltInVariant implements BuiltIn { WHITE, BLACK, RED, @@ -159,23 +158,6 @@ public class CatEntity extends TameableEntity { RAGDOLL, TABBY, ALL_BLACK, - JELLIE; - - public static final Function READER = context -> getByJavaIdentifier(context.id()); - - private final Key javaIdentifier; - - BuiltInVariant() { - javaIdentifier = MinecraftKey.key(name().toLowerCase(Locale.ROOT)); - } - - public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { - for (BuiltInVariant variant : values()) { - if (variant.javaIdentifier.equals(identifier)) { - return variant; - } - } - return null; - } + JELLIE } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index 2e142b72e..a5a514a0c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.entity.type.living.animal.tameable; -import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; @@ -34,6 +33,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.enchantment.EnchantmentComponent; @@ -41,18 +41,15 @@ import org.geysermc.geyser.item.type.DyeItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistries; -import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.ItemUtils; -import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.WolfVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; 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.item.ItemStack; @@ -60,11 +57,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponen import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.Collections; -import java.util.Locale; import java.util.UUID; -import java.util.function.Function; -public class WolfEntity extends TameableEntity { +public class WolfEntity extends TameableEntity implements VariantHolder { private byte collarColor = 14; // Red - default private HolderSet repairableItems = null; private boolean isCurseOfBinding = false; @@ -119,16 +114,19 @@ public class WolfEntity extends TameableEntity { dirtyMetadata.put(EntityDataTypes.COLOR, time != 0 ? (byte) 0 : collarColor); } - // 1.20.5+ - public void setWolfVariant(ObjectEntityMetadata> entityMetadata) { - // TODO set to pale if custom holder? - entityMetadata.getValue().ifId(id -> { - BuiltInVariant wolfVariant = JavaRegistries.WOLF_VARIANT.fromNetworkId(session, id); - if (wolfVariant == null) { - wolfVariant = BuiltInVariant.PALE; - } - dirtyMetadata.put(EntityDataTypes.VARIANT, wolfVariant.ordinal()); - }); + @Override + public JavaRegistryKey variantRegistry() { + return JavaRegistries.WOLF_VARIANT; + } + + @Override + public void setBedrockVariant(int bedrockId) { + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); + } + + @Override + public BuiltIn defaultVariant() { + return BuiltInVariant.PALE; } @Override @@ -201,7 +199,7 @@ public class WolfEntity extends TameableEntity { } // Ordered by bedrock id - public enum BuiltInVariant { + public enum BuiltInVariant implements BuiltIn { PALE, ASHEN, BLACK, @@ -210,23 +208,6 @@ public class WolfEntity extends TameableEntity { SNOWY, SPOTTED, STRIPED, - WOODS; - - public static final Function READER = context -> getByJavaIdentifier(context.id()); - - private final Key javaIdentifier; - - BuiltInVariant() { - this.javaIdentifier = MinecraftKey.key(this.name().toLowerCase(Locale.ROOT)); - } - - public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { - for (BuiltInVariant wolfVariant : values()) { - if (wolfVariant.javaIdentifier.equals(identifier)) { - return wolfVariant; - } - } - return null; - } + WOODS } } 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 4fbcbf08d..6ce65c9f3 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 @@ -39,6 +39,7 @@ import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; +import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; @@ -96,9 +97,9 @@ public final class RegistryCache { register("worldgen/biome", (cache, array) -> cache.biomeTranslations = array, BiomeTranslator::loadServerBiome); register("banner_pattern", cache -> cache.bannerPatterns, context -> BannerPattern.getByJavaIdentifier(context.id())); - register(JavaRegistries.CAT_VARIANT, cache -> cache.catVariants, CatEntity.BuiltInVariant.READER); - register(JavaRegistries.FROG_VARIANT, cache -> cache.frogVariants, FrogEntity.BuiltInVariant.READER); - register(JavaRegistries.WOLF_VARIANT, cache -> cache.wolfVariants, WolfEntity.BuiltInVariant.READER); + register(JavaRegistries.CAT_VARIANT, cache -> cache.catVariants, VariantHolder.reader(CatEntity.BuiltInVariant.class)); + register(JavaRegistries.FROG_VARIANT, cache -> cache.frogVariants, VariantHolder.reader(FrogEntity.BuiltInVariant.class)); + register(JavaRegistries.WOLF_VARIANT, cache -> cache.wolfVariants, VariantHolder.reader(WolfEntity.BuiltInVariant.class)); register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, TemperatureVariantAnimal.BuiltInVariant.READER); register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, TemperatureVariantAnimal.BuiltInVariant.READER); From 4c2ac05a53ca74f0c06f546276d97a8c640ebf42 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 11:13:54 +0000 Subject: [PATCH 19/86] Implement VariantHolder for farm animals, still have to figure out how to do this efficiently --- .../geyser/entity/EntityDefinitions.java | 6 +-- .../type/living/animal/MooshroomEntity.java | 1 + .../living/animal/farm/ChickenEntity.java | 3 +- .../type/living/animal/farm/CowEntity.java | 3 +- .../type/living/animal/farm/PigEntity.java | 6 +-- .../animal/farm/TemperatureVariantAnimal.java | 51 ++++--------------- .../geyser/session/cache/RegistryCache.java | 6 +-- 7 files changed, 20 insertions(+), 56 deletions(-) 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 af3aecd18..f725a0a78 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -980,7 +980,7 @@ public final class EntityDefinitions { FROG = EntityDefinition.inherited(FrogEntity::new, ageableEntityBase) .type(EntityType.FROG) .heightAndWidth(0.5f) - .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setFrogVariant) + .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setVariant) .addTranslator(MetadataTypes.OPTIONAL_VARINT, FrogEntity::setTongueTarget) .build(); HOGLIN = EntityDefinition.inherited(HoglinEntity::new, ageableEntityBase) @@ -1151,7 +1151,7 @@ public final class EntityDefinitions { CAT = EntityDefinition.inherited(CatEntity::new, tameableEntityBase) .type(EntityType.CAT) .height(0.35f).width(0.3f) - .addTranslator(MetadataTypes.CAT_VARIANT, CatEntity::setCatVariant) + .addTranslator(MetadataTypes.CAT_VARIANT, CatEntity::setVariant) .addTranslator(MetadataTypes.BOOLEAN, CatEntity::setResting) .addTranslator(null) // "resting state one" //TODO .addTranslator(MetadataTypes.INT, CatEntity::setCollarColor) @@ -1169,7 +1169,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor) .addTranslator(MetadataTypes.INT, WolfEntity::setWolfAngerTime) - .addTranslator(MetadataTypes.WOLF_VARIANT, WolfEntity::setWolfVariant) + .addTranslator(MetadataTypes.WOLF_VARIANT, WolfEntity::setVariant) .build(); // As of 1.18 these don't track entity data at all diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index 622c599e7..24386d4f6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -49,6 +49,7 @@ public class MooshroomEntity extends CowEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } + // TODO fix this with super public void setVariant(ObjectEntityMetadata entityMetadata) { isBrown = entityMetadata.getValue().equals("brown"); dirtyMetadata.put(EntityDataTypes.VARIANT, isBrown ? 1 : 0); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java index afd8a6ce6..da0c2c0fb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -30,7 +30,6 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.properties.VanillaEntityProperties; -import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistries; @@ -59,7 +58,7 @@ public class ChickenEntity extends TemperatureVariantAnimal { } @Override - protected JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.CHICKEN_VARIANT; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index 8f7740f90..8ed9172ed 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -33,7 +33,6 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.properties.VanillaEntityProperties; -import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; @@ -88,7 +87,7 @@ public class CowEntity extends TemperatureVariantAnimal { } @Override - protected JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.COW_VARIANT; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index 4508a0158..c9dda65a9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -35,7 +35,6 @@ import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.Tickable; -import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; import org.geysermc.geyser.entity.vehicle.ClientVehicle; @@ -51,9 +50,6 @@ import org.geysermc.geyser.session.cache.tags.Tag; 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.Holder; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.PigVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -164,7 +160,7 @@ public class PigEntity extends TemperatureVariantAnimal implements T } @Override - protected JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.PIG_VARIANT; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index a347ea1ac..0f806e0ba 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -25,25 +25,16 @@ package org.geysermc.geyser.entity.type.living.animal.farm; -import net.kyori.adventure.key.Key; -import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; -import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; -import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; -import java.util.Locale; import java.util.UUID; -import java.util.function.Function; +// TODO figure out how to do the generics here public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, @@ -51,20 +42,15 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity imp super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - protected abstract JavaRegistryKey variantRegistry(); - - public void setVariant(EntityMetadata, ? extends MetadataType>> variant) { - BuiltInVariant animalVariant; - if (variant.getValue().isId()) { - animalVariant = variantRegistry().fromNetworkId(session, variant.getValue().id()); - if (animalVariant == null) { - animalVariant = BuiltInVariant.TEMPERATE; - } - } else { - animalVariant = BuiltInVariant.TEMPERATE; - } + @Override + public void setBedrockVariant(int bedrockId) { // TODO does this work? - dirtyMetadata.put(EntityDataTypes.VARIANT, animalVariant.ordinal()); + dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); + } + + @Override + public BuiltIn defaultVariant() { + return BuiltInVariant.TEMPERATE; } // Ordered by bedrock id @@ -72,23 +58,6 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity imp public enum BuiltInVariant implements BuiltIn { COLD, TEMPERATE, - WARM; - - public static final Function READER = context -> getByJavaIdentifier(context.id()); - - private final Key javaIdentifier; - - BuiltInVariant() { - javaIdentifier = MinecraftKey.key(name().toLowerCase(Locale.ROOT)); - } - - public static @Nullable BuiltInVariant getByJavaIdentifier(Key identifier) { - for (BuiltInVariant variant : values()) { - if (variant.javaIdentifier.equals(identifier)) { - return variant; - } - } - return null; - } + WARM } } 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 6ce65c9f3..dfbfb071c 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 @@ -101,9 +101,9 @@ public final class RegistryCache { register(JavaRegistries.FROG_VARIANT, cache -> cache.frogVariants, VariantHolder.reader(FrogEntity.BuiltInVariant.class)); register(JavaRegistries.WOLF_VARIANT, cache -> cache.wolfVariants, VariantHolder.reader(WolfEntity.BuiltInVariant.class)); - register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, TemperatureVariantAnimal.BuiltInVariant.READER); - register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, TemperatureVariantAnimal.BuiltInVariant.READER); - register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, TemperatureVariantAnimal.BuiltInVariant.READER); + register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, VariantHolder.reader(TemperatureVariantAnimal.BuiltInVariant.class)); + register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, VariantHolder.reader(TemperatureVariantAnimal.BuiltInVariant.class)); + register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, VariantHolder.reader(TemperatureVariantAnimal.BuiltInVariant.class)); // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); From 20863336857001145c29d00b2634381194a0ba60 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sat, 22 Mar 2025 12:28:09 +0000 Subject: [PATCH 20/86] Use entity properties for farm animal variants --- .../entity/type/living/animal/FrogEntity.java | 4 +- .../type/living/animal/VariantHolder.java | 21 ++++---- .../type/living/animal/VariantIntHolder.java | 54 +++++++++++++++++++ .../living/animal/farm/ChickenEntity.java | 8 --- .../type/living/animal/farm/CowEntity.java | 8 --- .../type/living/animal/farm/PigEntity.java | 8 --- .../animal/farm/TemperatureVariantAnimal.java | 24 +++++---- .../living/animal/tameable/CatEntity.java | 6 +-- .../living/animal/tameable/WolfEntity.java | 6 +-- 9 files changed, 87 insertions(+), 52 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java index d770770f9..ef126e8d9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java @@ -44,7 +44,7 @@ import java.util.OptionalInt; import java.util.UUID; // TODO this is implementing VariantHolder until MCPL updates -public class FrogEntity extends AnimalEntity implements VariantHolder { +public class FrogEntity extends AnimalEntity implements VariantIntHolder { public FrogEntity(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); } @@ -64,7 +64,7 @@ public class FrogEntity extends AnimalEntity implements VariantHolder { } @Override - public void setBedrockVariant(int bedrockId) { + public void setBedrockVariantId(int bedrockId) { dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java index f337810fc..5f8da630f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java @@ -41,12 +41,13 @@ import java.util.function.Function; * Utility interface to help set up data-driven entity variants for mobs. * *

This interface is designed for mobs that have their variant wrapped in a {@link Holder}. Implementations usually have to - * implement {@link VariantHolder#variantRegistry()}, {@link VariantHolder#setBedrockVariant(int)}, and {@link VariantHolder#defaultVariant()}, and should also + * implement {@link VariantHolder#variantRegistry()}, {@link VariantHolder#setBedrockVariant(BuiltIn)}, and {@link VariantHolder#defaultVariant()}, and should also * have an enum with built-in variants on bedrock (implementing {@link BuiltIn}).

* * @param the MCPL variant class that a {@link Holder} wraps. + * @param the enum of Bedrock variants. */ -public interface VariantHolder { +public interface VariantHolder> { default void setVariant(EntityMetadata, ? extends MetadataType>> variant) { setVariant(variant.getValue()); @@ -56,7 +57,7 @@ public interface VariantHolder { * Sets the variant of the entity. Defaults to {@link VariantHolder#defaultVariant()} for custom holders and non-vanilla IDs. */ default void setVariant(Holder variant) { - BuiltIn builtInVariant; + BedrockVariant builtInVariant; if (variant.isId()) { builtInVariant = variantRegistry().fromNetworkId(getSession(), variant.id()); if (builtInVariant == null) { @@ -65,7 +66,7 @@ public interface VariantHolder { } else { builtInVariant = defaultVariant(); } - setBedrockVariant(builtInVariant.ordinal()); + setBedrockVariant(builtInVariant); } GeyserSession getSession(); @@ -74,17 +75,17 @@ public interface VariantHolder { * The registry in {@link org.geysermc.geyser.session.cache.registry.JavaRegistries} for this mob's variants. The registry can utilise the {@link VariantHolder#reader(Class)} method * to create a reader to be used in {@link org.geysermc.geyser.session.cache.RegistryCache}. */ - JavaRegistryKey> variantRegistry(); + JavaRegistryKey variantRegistry(); /** - * Should set the variant on bedrock's metadata (or however the variant is set for the mob). The bedrock ID has already been checked and is always valid. + * Should set the variant for bedrock. */ - void setBedrockVariant(int bedrockId); + void setBedrockVariant(BedrockVariant bedrockVariant); /** * Should return the default variant, that is to be used when this mob's variant is a custom or non-vanilla one. */ - BuiltIn defaultVariant(); + BedrockVariant defaultVariant(); /** * Creates a registry reader for this mob's variants. @@ -107,7 +108,7 @@ public interface VariantHolder { } /** - * Should be implemented on an enum within the entity class. The enum lists vanilla variants that can appear on bedrock, in the order of their bedrock network ID. + * Should be implemented on an enum within the entity class. The enum lists vanilla variants that can appear on bedrock. * *

The enum constants should be named the same as their Java identifiers.

* @@ -117,8 +118,6 @@ public interface VariantHolder { String name(); - int ordinal(); - default Key javaIdentifier() { return MinecraftKey.key(name().toLowerCase(Locale.ROOT)); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java new file mode 100644 index 000000000..c2d7bae48 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.entity.type.living.animal; + +/** + * Extension to {@link VariantHolder} to make it easier to implement on mobs that use bedrock's metadata system to set their variants, which are quite common. + * + * @see VariantHolder + */ +public interface VariantIntHolder extends VariantHolder> { + + @Override + default void setBedrockVariant(BuiltIn variant) { + setBedrockVariantId(variant.ordinal()); + } + + /** + * Should set the variant on bedrock's metadata. The bedrock ID has already been checked and is always valid. + */ + void setBedrockVariantId(int bedrockId); + + /** + * The enum constants should be ordered in the order of their bedrock network ID. + * + * @see org.geysermc.geyser.entity.type.living.animal.VariantHolder.BuiltIn + */ + interface BuiltIn extends VariantHolder.BuiltIn { + + int ordinal(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java index da0c2c0fb..d9d26769a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -27,9 +27,7 @@ package org.geysermc.geyser.entity.type.living.animal.farm; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistries; @@ -45,12 +43,6 @@ public class ChickenEntity extends TemperatureVariantAnimal { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - @Override @Nullable protected Tag getFoodTag() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index 8ed9172ed..cecf9b017 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -30,9 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; @@ -53,12 +51,6 @@ public class CowEntity extends TemperatureVariantAnimal { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - @NonNull @Override protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index c9dda65a9..97ef136f4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -31,9 +31,7 @@ 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.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent; @@ -63,12 +61,6 @@ public class PigEntity extends TemperatureVariantAnimal implements T super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - @Override @Nullable protected Tag getFoodTag() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index 0f806e0ba..f9954a10f 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -26,16 +26,18 @@ package org.geysermc.geyser.entity.type.living.animal.farm; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.session.GeyserSession; +import java.util.Locale; import java.util.UUID; // TODO figure out how to do the generics here -public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { +public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -43,19 +45,23 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity imp } @Override - public void setBedrockVariant(int bedrockId) { - // TODO does this work? - dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); + public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); } @Override - public BuiltIn defaultVariant() { + public void setBedrockVariant(BuiltInVariant variant) { + propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.name().toLowerCase(Locale.ROOT)); + updateBedrockEntityProperties(); + } + + @Override + public BuiltInVariant defaultVariant() { return BuiltInVariant.TEMPERATE; } - // Ordered by bedrock id - // TODO: are these ordered correctly? Does the order differ for mobs? - public enum BuiltInVariant implements BuiltIn { + public enum BuiltInVariant implements BuiltIn { COLD, TEMPERATE, WARM diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java index 2582a2eb8..b7637214d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/CatEntity.java @@ -31,7 +31,7 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.type.living.animal.VariantHolder; +import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; @@ -49,7 +49,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; // TODO this is implementing VariantHolder until MCPL updates -public class CatEntity extends TameableEntity implements VariantHolder { +public class CatEntity extends TameableEntity implements VariantIntHolder { private byte collarColor = 14; // Red - default @@ -91,7 +91,7 @@ public class CatEntity extends TameableEntity implements VariantHolder { } @Override - public void setBedrockVariant(int bedrockId) { + public void setBedrockVariantId(int bedrockId) { dirtyMetadata.put(EntityDataTypes.VARIANT, bedrockId); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index a5a514a0c..a41e46793 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -33,7 +33,7 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.type.living.animal.VariantHolder; +import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.enchantment.EnchantmentComponent; @@ -59,7 +59,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.Collections; import java.util.UUID; -public class WolfEntity extends TameableEntity implements VariantHolder { +public class WolfEntity extends TameableEntity implements VariantIntHolder { private byte collarColor = 14; // Red - default private HolderSet repairableItems = null; private boolean isCurseOfBinding = false; @@ -120,7 +120,7 @@ public class WolfEntity extends TameableEntity implements VariantHolder Date: Sat, 22 Mar 2025 12:50:20 +0000 Subject: [PATCH 21/86] "Fix" farm animal variants --- .../type/living/animal/MooshroomEntity.java | 1 - .../type/living/animal/farm/ChickenEntity.java | 13 ++++++++++++- .../type/living/animal/farm/CowEntity.java | 13 ++++++++++++- .../type/living/animal/farm/PigEntity.java | 13 ++++++++++++- .../animal/farm/TemperatureVariantAnimal.java | 16 +++------------- .../geyser/session/cache/RegistryCache.java | 16 +++++++++------- .../session/cache/registry/JavaRegistries.java | 10 ++++++---- 7 files changed, 54 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index 24386d4f6..622c599e7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -49,7 +49,6 @@ public class MooshroomEntity extends CowEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - // TODO fix this with super public void setVariant(ObjectEntityMetadata entityMetadata) { isBrown = entityMetadata.getValue().equals("brown"); dirtyMetadata.put(EntityDataTypes.VARIANT, isBrown ? 1 : 0); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java index d9d26769a..e5638dc17 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -37,7 +37,7 @@ import org.geysermc.geyser.session.cache.tags.Tag; import java.util.UUID; -public class ChickenEntity extends TemperatureVariantAnimal { +public class ChickenEntity extends TemperatureVariantAnimal { public ChickenEntity(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); @@ -53,4 +53,15 @@ public class ChickenEntity extends TemperatureVariantAnimal { public JavaRegistryKey variantRegistry() { return JavaRegistries.CHICKEN_VARIANT; } + + @Override + public BuiltInVariant defaultVariant() { + return BuiltInVariant.TEMPERATE; + } + + public enum BuiltInVariant implements BuiltIn { + COLD, + TEMPERATE, + WARM + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index cecf9b017..442fb27aa 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -45,7 +45,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CowEntity extends TemperatureVariantAnimal { +public class CowEntity extends TemperatureVariantAnimal { public CowEntity(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); @@ -82,4 +82,15 @@ public class CowEntity extends TemperatureVariantAnimal { public JavaRegistryKey variantRegistry() { return JavaRegistries.COW_VARIANT; } + + @Override + public BuiltInVariant defaultVariant() { + return BuiltInVariant.TEMPERATE; + } + + public enum BuiltInVariant implements BuiltIn { + COLD, + TEMPERATE, + WARM + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index 97ef136f4..bf0416842 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -54,7 +54,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class PigEntity extends TemperatureVariantAnimal implements Tickable, ClientVehicle { +public class PigEntity extends TemperatureVariantAnimal 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) { @@ -155,4 +155,15 @@ public class PigEntity extends TemperatureVariantAnimal implements T public JavaRegistryKey variantRegistry() { return JavaRegistries.PIG_VARIANT; } + + @Override + public BuiltInVariant defaultVariant() { + return BuiltInVariant.TEMPERATE; + } + + public enum BuiltInVariant implements BuiltIn { + COLD, + TEMPERATE, + WARM + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index f9954a10f..dd24cd7c6 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -37,7 +37,8 @@ import java.util.Locale; import java.util.UUID; // TODO figure out how to do the generics here -public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { +public abstract class TemperatureVariantAnimal> extends AnimalEntity + implements VariantHolder { public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -51,19 +52,8 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity imp } @Override - public void setBedrockVariant(BuiltInVariant variant) { + public void setBedrockVariant(BedrockVariant variant) { propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.name().toLowerCase(Locale.ROOT)); updateBedrockEntityProperties(); } - - @Override - public BuiltInVariant defaultVariant() { - return BuiltInVariant.TEMPERATE; - } - - public enum BuiltInVariant implements BuiltIn { - COLD, - TEMPERATE, - WARM - } } 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 dfbfb071c..bbc630c04 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 @@ -40,7 +40,9 @@ import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.VariantHolder; -import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; @@ -101,9 +103,9 @@ public final class RegistryCache { register(JavaRegistries.FROG_VARIANT, cache -> cache.frogVariants, VariantHolder.reader(FrogEntity.BuiltInVariant.class)); register(JavaRegistries.WOLF_VARIANT, cache -> cache.wolfVariants, VariantHolder.reader(WolfEntity.BuiltInVariant.class)); - register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, VariantHolder.reader(TemperatureVariantAnimal.BuiltInVariant.class)); - register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, VariantHolder.reader(TemperatureVariantAnimal.BuiltInVariant.class)); - register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, VariantHolder.reader(TemperatureVariantAnimal.BuiltInVariant.class)); + register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, VariantHolder.reader(PigEntity.BuiltInVariant.class)); + register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, VariantHolder.reader(CowEntity.BuiltInVariant.class)); + register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, VariantHolder.reader(ChickenEntity.BuiltInVariant.class)); // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); @@ -148,9 +150,9 @@ public final class RegistryCache { private final JavaRegistry frogVariants = new SimpleJavaRegistry<>(); private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); - private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); - private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); - private final JavaRegistry chickenVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry chickenVariants = new SimpleJavaRegistry<>(); public RegistryCache(GeyserSession session) { this.session = session; diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index 732a2cc4a..5ee1ddb9a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -28,7 +28,9 @@ package org.geysermc.geyser.session.cache.registry; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; +import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; @@ -59,9 +61,9 @@ public class JavaRegistries { public static final JavaRegistryKey FROG_VARIANT = create("frog_variant", RegistryCache::frogVariants); public static final JavaRegistryKey WOLF_VARIANT = create("wolf_variant", RegistryCache::wolfVariants); - public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); - public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); - public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); + public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); + public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); + public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); private static JavaRegistryKey create(String key, JavaRegistryKey.NetworkSerializer networkSerializer, JavaRegistryKey.NetworkDeserializer networkDeserializer) { JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), networkSerializer, networkDeserializer); From 85f1a607533b9799036e95d76eddd9b4a0a94d2c Mon Sep 17 00:00:00 2001 From: Eclipse Date: Sun, 23 Mar 2025 14:22:15 +0000 Subject: [PATCH 22/86] Changes to variants: - All entity variants in rc1 are sent as int IDs by java, holders are no longer used - Fixed reading of mooshroom variants - Temperature animal variants now look a lot cleaner It builds! --- .../kotlin/geyser.base-conventions.gradle.kts | 2 +- .../geyser/entity/EntityDefinitions.java | 2 +- .../entity/type/living/animal/FrogEntity.java | 7 ++-- .../type/living/animal/MooshroomEntity.java | 8 ++-- .../type/living/animal/VariantHolder.java | 41 ++++++++----------- .../type/living/animal/VariantIntHolder.java | 6 +-- .../living/animal/farm/ChickenEntity.java | 13 +----- .../type/living/animal/farm/CowEntity.java | 13 +----- .../type/living/animal/farm/PigEntity.java | 14 +------ .../animal/farm/TemperatureVariantAnimal.java | 21 ++++++++-- .../living/animal/tameable/CatEntity.java | 7 ++-- .../living/animal/tameable/WolfEntity.java | 7 ++-- .../geyser/session/cache/RegistryCache.java | 16 ++++---- .../cache/registry/JavaRegistries.java | 10 ++--- .../network/ScoreboardIssueTests.java | 2 +- 15 files changed, 67 insertions(+), 102 deletions(-) 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 3f7b48a2f..dca1bcef5 100644 --- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts @@ -26,7 +26,7 @@ dependencies { } repositories { - mavenLocal() + // mavenLocal() mavenCentral() 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 f725a0a78..01206fa8d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -998,7 +998,7 @@ public final class EntityDefinitions { MOOSHROOM = EntityDefinition.inherited(MooshroomEntity::new, ageableEntityBase) .type(EntityType.MOOSHROOM) .height(1.4f).width(0.9f) - .addTranslator(MetadataTypes.STRING, MooshroomEntity::setVariant) + .addTranslator(MetadataTypes.INT, MooshroomEntity::setMooshroomVariant) .build(); OCELOT = EntityDefinition.inherited(OcelotEntity::new, ageableEntityBase) .type(EntityType.OCELOT) diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java index ef126e8d9..3b5c28c7d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/FrogEntity.java @@ -43,8 +43,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.Object import java.util.OptionalInt; import java.util.UUID; -// TODO this is implementing VariantHolder until MCPL updates -public class FrogEntity extends AnimalEntity implements VariantIntHolder { +public class FrogEntity extends AnimalEntity implements VariantIntHolder { public FrogEntity(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); } @@ -69,7 +68,7 @@ public class FrogEntity extends AnimalEntity implements VariantIntHolder } @Override - public BuiltIn defaultVariant() { + public BuiltIn defaultVariant() { return BuiltInVariant.TEMPERATE; } @@ -93,7 +92,7 @@ public class FrogEntity extends AnimalEntity implements VariantIntHolder // Ordered by bedrock id // TODO: are these ordered correctly? - public enum BuiltInVariant implements BuiltIn { + public enum BuiltInVariant implements BuiltIn { TEMPERATE, COLD, WARM diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java index 622c599e7..3314344cb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/MooshroomEntity.java @@ -37,7 +37,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata; +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; @@ -49,9 +49,9 @@ public class MooshroomEntity extends CowEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - public void setVariant(ObjectEntityMetadata entityMetadata) { - isBrown = entityMetadata.getValue().equals("brown"); - dirtyMetadata.put(EntityDataTypes.VARIANT, isBrown ? 1 : 0); + public void setMooshroomVariant(IntEntityMetadata metadata) { + isBrown = metadata.getPrimitiveValue() == 1; + dirtyMetadata.put(EntityDataTypes.VARIANT, metadata.getPrimitiveValue()); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java index 5f8da630f..b3390a3ed 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantHolder.java @@ -30,40 +30,35 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.Locale; import java.util.function.Function; /** - * Utility interface to help set up data-driven entity variants for mobs. + * Interface to help set up data-driven entity variants for mobs. * - *

This interface is designed for mobs that have their variant wrapped in a {@link Holder}. Implementations usually have to - * implement {@link VariantHolder#variantRegistry()}, {@link VariantHolder#setBedrockVariant(BuiltIn)}, and {@link VariantHolder#defaultVariant()}, and should also + *

Data-driven variants are sent as an int ID of their variant registry by Java, but can be a metadata ID or entity property on bedrock. + * This interface helps translate data-driven variants to built-in bedrock ones.

+ * + *

Implementations usually have to implement {@link VariantHolder#variantRegistry()}, + * {@link VariantHolder#setBedrockVariant(BuiltIn)}, and {@link VariantHolder#defaultVariant()}, and should also * have an enum with built-in variants on bedrock (implementing {@link BuiltIn}).

* - * @param the MCPL variant class that a {@link Holder} wraps. * @param the enum of Bedrock variants. */ -public interface VariantHolder> { +public interface VariantHolder { - default void setVariant(EntityMetadata, ? extends MetadataType>> variant) { - setVariant(variant.getValue()); + default void setVariant(IntEntityMetadata variant) { + setVariantFromJavaId(variant.getPrimitiveValue()); } /** - * Sets the variant of the entity. Defaults to {@link VariantHolder#defaultVariant()} for custom holders and non-vanilla IDs. + * Sets the variant of the entity. Defaults to {@link VariantHolder#defaultVariant()} for non-vanilla IDs. */ - default void setVariant(Holder variant) { - BedrockVariant builtInVariant; - if (variant.isId()) { - builtInVariant = variantRegistry().fromNetworkId(getSession(), variant.id()); - if (builtInVariant == null) { - builtInVariant = defaultVariant(); - } - } else { + default void setVariantFromJavaId(int variant) { + BedrockVariant builtInVariant = variantRegistry().fromNetworkId(getSession(), variant); + if (builtInVariant == null) { builtInVariant = defaultVariant(); } setBedrockVariant(builtInVariant); @@ -92,14 +87,14 @@ public interface VariantHolderThis reader simply matches the identifiers of registry entries with built-in variants. If no built-in variant matches, null is returned.

*/ - static >> Function reader(Class clazz) { + static > Function reader(Class clazz) { BuiltInVariant[] variants = clazz.getEnumConstants(); if (variants == null) { throw new IllegalArgumentException("Class is not an enum"); } return context -> { for (BuiltInVariant variant : variants) { - if (((BuiltIn) variant).javaIdentifier().equals(context.id())) { + if (((BuiltIn) variant).javaIdentifier().equals(context.id())) { return variant; } } @@ -111,10 +106,8 @@ public interface VariantHolderThe enum constants should be named the same as their Java identifiers.

- * - * @param the same as the parent entity class. Used for type checking. */ - interface BuiltIn { + interface BuiltIn { String name(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java index c2d7bae48..f1d45a447 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/VariantIntHolder.java @@ -30,10 +30,10 @@ package org.geysermc.geyser.entity.type.living.animal; * * @see VariantHolder */ -public interface VariantIntHolder extends VariantHolder> { +public interface VariantIntHolder extends VariantHolder { @Override - default void setBedrockVariant(BuiltIn variant) { + default void setBedrockVariant(BuiltIn variant) { setBedrockVariantId(variant.ordinal()); } @@ -47,7 +47,7 @@ public interface VariantIntHolder extends VariantHolder extends VariantHolder.BuiltIn { + interface BuiltIn extends VariantHolder.BuiltIn { int ordinal(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java index e5638dc17..4ee7175de 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/ChickenEntity.java @@ -37,7 +37,7 @@ import org.geysermc.geyser.session.cache.tags.Tag; import java.util.UUID; -public class ChickenEntity extends TemperatureVariantAnimal { +public class ChickenEntity extends TemperatureVariantAnimal { public ChickenEntity(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); @@ -53,15 +53,4 @@ public class ChickenEntity extends TemperatureVariantAnimal variantRegistry() { return JavaRegistries.CHICKEN_VARIANT; } - - @Override - public BuiltInVariant defaultVariant() { - return BuiltInVariant.TEMPERATE; - } - - public enum BuiltInVariant implements BuiltIn { - COLD, - TEMPERATE, - WARM - } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java index 442fb27aa..de79e9a52 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/CowEntity.java @@ -45,7 +45,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; -public class CowEntity extends TemperatureVariantAnimal { +public class CowEntity extends TemperatureVariantAnimal { public CowEntity(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); @@ -82,15 +82,4 @@ public class CowEntity extends TemperatureVariantAnimal variantRegistry() { return JavaRegistries.COW_VARIANT; } - - @Override - public BuiltInVariant defaultVariant() { - return BuiltInVariant.TEMPERATE; - } - - public enum BuiltInVariant implements BuiltIn { - COLD, - TEMPERATE, - WARM - } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index bf0416842..d6a8ece7c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -48,13 +48,12 @@ import org.geysermc.geyser.session.cache.tags.Tag; 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.PigVariant; 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 TemperatureVariantAnimal implements Tickable, ClientVehicle { +public class PigEntity extends TemperatureVariantAnimal 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) { @@ -155,15 +154,4 @@ public class PigEntity extends TemperatureVariantAnimal variantRegistry() { return JavaRegistries.PIG_VARIANT; } - - @Override - public BuiltInVariant defaultVariant() { - return BuiltInVariant.TEMPERATE; - } - - public enum BuiltInVariant implements BuiltIn { - COLD, - TEMPERATE, - WARM - } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index dd24cd7c6..ed4336215 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -32,13 +32,15 @@ import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.living.animal.VariantHolder; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import java.util.Locale; import java.util.UUID; +import java.util.function.Function; -// TODO figure out how to do the generics here -public abstract class TemperatureVariantAnimal> extends AnimalEntity - implements VariantHolder { +public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { + + public static final Function VARIANT_READER = VariantHolder.reader(BuiltInVariant.class); public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { @@ -52,8 +54,19 @@ public abstract class TemperatureVariantAnimal until MCPL updates -public class CatEntity extends TameableEntity implements VariantIntHolder { +public class CatEntity extends TameableEntity implements VariantIntHolder { private byte collarColor = 14; // Red - default @@ -96,7 +95,7 @@ public class CatEntity extends TameableEntity implements VariantIntHolder defaultVariant() { + public BuiltIn defaultVariant() { return BuiltInVariant.BLACK; // Default variant on Java } @@ -147,7 +146,7 @@ public class CatEntity extends TameableEntity implements VariantIntHolder { + public enum BuiltInVariant implements BuiltIn { WHITE, BLACK, RED, diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index a41e46793..753a6e3c4 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -47,7 +47,6 @@ import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.geyser.util.ItemUtils; -import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.WolfVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; @@ -59,7 +58,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.Collections; import java.util.UUID; -public class WolfEntity extends TameableEntity implements VariantIntHolder { +public class WolfEntity extends TameableEntity implements VariantIntHolder { private byte collarColor = 14; // Red - default private HolderSet repairableItems = null; private boolean isCurseOfBinding = false; @@ -125,7 +124,7 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder defaultVariant() { + public BuiltIn defaultVariant() { return BuiltInVariant.PALE; } @@ -199,7 +198,7 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder { + public enum BuiltInVariant implements BuiltIn { PALE, ASHEN, BLACK, 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 bbc630c04..808ab7ee2 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 @@ -40,9 +40,7 @@ import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.VariantHolder; -import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; @@ -103,9 +101,9 @@ public final class RegistryCache { register(JavaRegistries.FROG_VARIANT, cache -> cache.frogVariants, VariantHolder.reader(FrogEntity.BuiltInVariant.class)); register(JavaRegistries.WOLF_VARIANT, cache -> cache.wolfVariants, VariantHolder.reader(WolfEntity.BuiltInVariant.class)); - register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, VariantHolder.reader(PigEntity.BuiltInVariant.class)); - register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, VariantHolder.reader(CowEntity.BuiltInVariant.class)); - register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, VariantHolder.reader(ChickenEntity.BuiltInVariant.class)); + register(JavaRegistries.PIG_VARIANT, cache -> cache.pigVariants, TemperatureVariantAnimal.VARIANT_READER); + register(JavaRegistries.COW_VARIANT, cache -> cache.cowVariants, TemperatureVariantAnimal.VARIANT_READER); + register(JavaRegistries.CHICKEN_VARIANT, cache -> cache.chickenVariants, TemperatureVariantAnimal.VARIANT_READER); // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); @@ -150,9 +148,9 @@ public final class RegistryCache { private final JavaRegistry frogVariants = new SimpleJavaRegistry<>(); private final JavaRegistry wolfVariants = new SimpleJavaRegistry<>(); - private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); - private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); - private final JavaRegistry chickenVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry pigVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry cowVariants = new SimpleJavaRegistry<>(); + private final JavaRegistry chickenVariants = new SimpleJavaRegistry<>(); public RegistryCache(GeyserSession session) { this.session = session; diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index 5ee1ddb9a..732a2cc4a 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -28,9 +28,7 @@ package org.geysermc.geyser.session.cache.registry; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.inventory.item.BannerPattern; @@ -61,9 +59,9 @@ public class JavaRegistries { public static final JavaRegistryKey FROG_VARIANT = create("frog_variant", RegistryCache::frogVariants); public static final JavaRegistryKey WOLF_VARIANT = create("wolf_variant", RegistryCache::wolfVariants); - public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); - public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); - public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); + public static final JavaRegistryKey PIG_VARIANT = create("pig_variant", RegistryCache::pigVariants); + public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); + public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); private static JavaRegistryKey create(String key, JavaRegistryKey.NetworkSerializer networkSerializer, JavaRegistryKey.NetworkDeserializer networkDeserializer) { JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), networkSerializer, networkDeserializer); diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java index 040ceaa66..8311f1a5b 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java @@ -52,8 +52,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibilit import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundPlayerInfoUpdatePacket; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundAddEntityPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundSetEntityDataPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket; import org.junit.jupiter.api.Test; From ae8062c5bc43e3bd6a1556536416e8c7c4eed344 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 25 Mar 2025 17:23:43 +0100 Subject: [PATCH 23/86] Revert api breaking change --- api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java index c83fce4c4..b6626aa6a 100644 --- a/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java +++ b/api/src/main/java/org/geysermc/geyser/api/pack/PackCodec.java @@ -98,7 +98,7 @@ public abstract class PackCodec { * @since 2.1.1 */ @NonNull - public static PathPackCodec path(@NonNull Path path) { + public static PackCodec path(@NonNull Path path) { return GeyserApi.api().provider(PathPackCodec.class, path); } @@ -110,7 +110,7 @@ public abstract class PackCodec { * @since 2.6.2 */ @NonNull - public static UrlPackCodec url(@NonNull String url) { + public static PackCodec url(@NonNull String url) { return GeyserApi.api().provider(UrlPackCodec.class, url); } } From f2ec8d5cee9e7d87f8a3b78f9fb82a183d749221 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Mon, 24 Mar 2025 06:51:30 +0000 Subject: [PATCH 24/86] Random stuff to make it run --- .../geyser/inventory/click/ClickPlan.java | 12 ++- .../DataComponentRegistryPopulator.java | 7 +- .../populator/ItemRegistryPopulator.java | 89 +++++++++++-------- .../geyser/session/GeyserSession.java | 4 +- ...BedrockInventoryTransactionTranslator.java | 9 +- gradle/libs.versions.toml | 2 +- 6 files changed, 76 insertions(+), 47 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 d4344f6e8..4d8af3882 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 @@ -41,6 +41,7 @@ import org.geysermc.geyser.util.thirdparty.Fraction; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction; +import org.geysermc.mcprotocollib.protocol.data.game.item.HashedStack; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSelectBundleItemPacket; @@ -50,6 +51,8 @@ import org.jetbrains.annotations.Contract; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; +import java.util.Map; +import java.util.Set; public final class ClickPlan { private final List plan = new ArrayList<>(); @@ -157,8 +160,8 @@ public final class ClickPlan { action.slot, action.click.actionType, action.click.action, - clickedItemStack, - changedItems + hashStack(clickedItemStack), + new Int2ObjectOpenHashMap<>() // TODO fixme ); session.sendDownstreamGamePacket(clickPacket); @@ -514,4 +517,9 @@ public final class ClickPlan { private record ClickAction(Click click, int slot, boolean force) { } + + // TODO probably move this + public static HashedStack hashStack(ItemStack stack) { + return stack == null ? null : new HashedStack(stack.getId(), stack.getAmount(), Map.of(), Set.of()); // TODO this is WRONG. figure out stack hashing + } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java index 29d85cdd2..1a247f609 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java @@ -75,8 +75,13 @@ public final class DataComponentRegistryPopulator { byte[] bytes = Base64.getDecoder().decode(encodedValue); ByteBuf buf = Unpooled.wrappedBuffer(bytes); int varInt = MinecraftTypes.readVarInt(buf); - System.out.println("int: " + varInt + " " + componentEntry.getKey()); + //System.out.println("int: " + varInt + " " + componentEntry.getKey()); DataComponentType dataComponentType = DataComponentTypes.from(varInt); + if (dataComponentType == DataComponentTypes.ENCHANTMENTS + || dataComponentType == DataComponentTypes.STORED_ENCHANTMENTS + || dataComponentType == DataComponentTypes.JUKEBOX_PLAYABLE) { + continue; // TODO Broken reading in MCPL + } DataComponent dataComponent = dataComponentType.readDataComponent(buf); map.put(dataComponentType, dataComponent); 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 0b8d7f982..6de62ceac 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 @@ -112,46 +112,61 @@ public class ItemRegistryPopulator { } public static void populate() { + // 1.21.5 Map itemFallbacks = new HashMap<>(); - itemFallbacks.put(Items.PALE_OAK_PLANKS, Items.BIRCH_PLANKS); - itemFallbacks.put(Items.PALE_OAK_FENCE, Items.BIRCH_FENCE); - itemFallbacks.put(Items.PALE_OAK_FENCE_GATE, Items.BIRCH_FENCE_GATE); - itemFallbacks.put(Items.PALE_OAK_STAIRS, Items.BIRCH_STAIRS); - itemFallbacks.put(Items.PALE_OAK_DOOR, Items.BIRCH_DOOR); - itemFallbacks.put(Items.PALE_OAK_TRAPDOOR, Items.BIRCH_TRAPDOOR); - itemFallbacks.put(Items.PALE_OAK_SLAB, Items.BIRCH_SLAB); - itemFallbacks.put(Items.PALE_OAK_LOG, Items.BIRCH_LOG); - itemFallbacks.put(Items.STRIPPED_PALE_OAK_LOG, Items.STRIPPED_BIRCH_LOG); - itemFallbacks.put(Items.PALE_OAK_WOOD, Items.BIRCH_WOOD); - itemFallbacks.put(Items.PALE_OAK_LEAVES, Items.BIRCH_LEAVES); - itemFallbacks.put(Items.PALE_OAK_SAPLING, Items.BIRCH_SAPLING); - itemFallbacks.put(Items.STRIPPED_PALE_OAK_WOOD, Items.STRIPPED_BIRCH_WOOD); - itemFallbacks.put(Items.PALE_OAK_SIGN, Items.BIRCH_SIGN); - itemFallbacks.put(Items.PALE_OAK_HANGING_SIGN, Items.BIRCH_HANGING_SIGN); - itemFallbacks.put(Items.PALE_OAK_BOAT, Items.BIRCH_BOAT); - itemFallbacks.put(Items.PALE_OAK_CHEST_BOAT, Items.BIRCH_CHEST_BOAT); - itemFallbacks.put(Items.PALE_OAK_BUTTON, Items.BIRCH_BUTTON); - itemFallbacks.put(Items.PALE_OAK_PRESSURE_PLATE, Items.BIRCH_PRESSURE_PLATE); - itemFallbacks.put(Items.RESIN_CLUMP, Items.RAW_COPPER); - itemFallbacks.put(Items.RESIN_BRICK_WALL, Items.RED_SANDSTONE_WALL); - itemFallbacks.put(Items.RESIN_BRICK_STAIRS, Items.RED_SANDSTONE_STAIRS); - itemFallbacks.put(Items.RESIN_BRICK_SLAB, Items.RED_SANDSTONE_SLAB); - itemFallbacks.put(Items.RESIN_BLOCK, Items.RED_SANDSTONE); - itemFallbacks.put(Items.RESIN_BRICK, Items.BRICK); - itemFallbacks.put(Items.RESIN_BRICKS, Items.CUT_RED_SANDSTONE); - itemFallbacks.put(Items.CHISELED_RESIN_BRICKS, Items.CHISELED_RED_SANDSTONE); - itemFallbacks.put(Items.CLOSED_EYEBLOSSOM, Items.WHITE_TULIP); - itemFallbacks.put(Items.OPEN_EYEBLOSSOM, Items.OXEYE_DAISY); - itemFallbacks.put(Items.PALE_MOSS_BLOCK, Items.MOSS_BLOCK); - itemFallbacks.put(Items.PALE_MOSS_CARPET, Items.MOSS_CARPET); - itemFallbacks.put(Items.PALE_HANGING_MOSS, Items.HANGING_ROOTS); - itemFallbacks.put(Items.CREAKING_HEART, Items.CHISELED_POLISHED_BLACKSTONE); - itemFallbacks.put(Items.CREAKING_SPAWN_EGG, Items.HOGLIN_SPAWN_EGG); + itemFallbacks.put(Items.BUSH, Items.SHORT_GRASS); + itemFallbacks.put(Items.CACTUS_FLOWER, Items.BUBBLE_CORAL_FAN); + itemFallbacks.put(Items.FIREFLY_BUSH, Items.SHORT_GRASS); + itemFallbacks.put(Items.LEAF_LITTER, Items.PINK_PETALS); + itemFallbacks.put(Items.SHORT_DRY_GRASS, Items.DEAD_BUSH); + itemFallbacks.put(Items.TALL_DRY_GRASS, Items.TALL_GRASS); + itemFallbacks.put(Items.WILDFLOWERS, Items.PINK_PETALS); + itemFallbacks.put(Items.TEST_BLOCK, Items.STRUCTURE_BLOCK); + itemFallbacks.put(Items.TEST_INSTANCE_BLOCK, Items.JIGSAW); + itemFallbacks.put(Items.BLUE_EGG, Items.EGG); + itemFallbacks.put(Items.BROWN_EGG, Items.EGG); + + // 1.21.4 + Map oneTwentyFourFallbacks = new HashMap<>(itemFallbacks); + oneTwentyFourFallbacks.put(Items.PALE_OAK_PLANKS, Items.BIRCH_PLANKS); + oneTwentyFourFallbacks.put(Items.PALE_OAK_FENCE, Items.BIRCH_FENCE); + oneTwentyFourFallbacks.put(Items.PALE_OAK_FENCE_GATE, Items.BIRCH_FENCE_GATE); + oneTwentyFourFallbacks.put(Items.PALE_OAK_STAIRS, Items.BIRCH_STAIRS); + oneTwentyFourFallbacks.put(Items.PALE_OAK_DOOR, Items.BIRCH_DOOR); + oneTwentyFourFallbacks.put(Items.PALE_OAK_TRAPDOOR, Items.BIRCH_TRAPDOOR); + oneTwentyFourFallbacks.put(Items.PALE_OAK_SLAB, Items.BIRCH_SLAB); + oneTwentyFourFallbacks.put(Items.PALE_OAK_LOG, Items.BIRCH_LOG); + oneTwentyFourFallbacks.put(Items.STRIPPED_PALE_OAK_LOG, Items.STRIPPED_BIRCH_LOG); + oneTwentyFourFallbacks.put(Items.PALE_OAK_WOOD, Items.BIRCH_WOOD); + oneTwentyFourFallbacks.put(Items.PALE_OAK_LEAVES, Items.BIRCH_LEAVES); + oneTwentyFourFallbacks.put(Items.PALE_OAK_SAPLING, Items.BIRCH_SAPLING); + oneTwentyFourFallbacks.put(Items.STRIPPED_PALE_OAK_WOOD, Items.STRIPPED_BIRCH_WOOD); + oneTwentyFourFallbacks.put(Items.PALE_OAK_SIGN, Items.BIRCH_SIGN); + oneTwentyFourFallbacks.put(Items.PALE_OAK_HANGING_SIGN, Items.BIRCH_HANGING_SIGN); + oneTwentyFourFallbacks.put(Items.PALE_OAK_BOAT, Items.BIRCH_BOAT); + oneTwentyFourFallbacks.put(Items.PALE_OAK_CHEST_BOAT, Items.BIRCH_CHEST_BOAT); + oneTwentyFourFallbacks.put(Items.PALE_OAK_BUTTON, Items.BIRCH_BUTTON); + oneTwentyFourFallbacks.put(Items.PALE_OAK_PRESSURE_PLATE, Items.BIRCH_PRESSURE_PLATE); + oneTwentyFourFallbacks.put(Items.RESIN_CLUMP, Items.RAW_COPPER); + oneTwentyFourFallbacks.put(Items.RESIN_BRICK_WALL, Items.RED_SANDSTONE_WALL); + oneTwentyFourFallbacks.put(Items.RESIN_BRICK_STAIRS, Items.RED_SANDSTONE_STAIRS); + oneTwentyFourFallbacks.put(Items.RESIN_BRICK_SLAB, Items.RED_SANDSTONE_SLAB); + oneTwentyFourFallbacks.put(Items.RESIN_BLOCK, Items.RED_SANDSTONE); + oneTwentyFourFallbacks.put(Items.RESIN_BRICK, Items.BRICK); + oneTwentyFourFallbacks.put(Items.RESIN_BRICKS, Items.CUT_RED_SANDSTONE); + oneTwentyFourFallbacks.put(Items.CHISELED_RESIN_BRICKS, Items.CHISELED_RED_SANDSTONE); + oneTwentyFourFallbacks.put(Items.CLOSED_EYEBLOSSOM, Items.WHITE_TULIP); + oneTwentyFourFallbacks.put(Items.OPEN_EYEBLOSSOM, Items.OXEYE_DAISY); + oneTwentyFourFallbacks.put(Items.PALE_MOSS_BLOCK, Items.MOSS_BLOCK); + oneTwentyFourFallbacks.put(Items.PALE_MOSS_CARPET, Items.MOSS_CARPET); + oneTwentyFourFallbacks.put(Items.PALE_HANGING_MOSS, Items.HANGING_ROOTS); + oneTwentyFourFallbacks.put(Items.CREAKING_HEART, Items.CHISELED_POLISHED_BLACKSTONE); + oneTwentyFourFallbacks.put(Items.CREAKING_SPAWN_EGG, Items.HOGLIN_SPAWN_EGG); List paletteVersions = new ArrayList<>(2); - paletteVersions.add(new PaletteVersion("1_21_40", Bedrock_v748.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping)); - paletteVersions.add(new PaletteVersion("1_21_50", Bedrock_v766.CODEC.getProtocolVersion())); - paletteVersions.add(new PaletteVersion("1_21_60", Bedrock_v776.CODEC.getProtocolVersion())); + paletteVersions.add(new PaletteVersion("1_21_40", Bedrock_v748.CODEC.getProtocolVersion(), oneTwentyFourFallbacks, (item, mapping) -> mapping)); + paletteVersions.add(new PaletteVersion("1_21_50", Bedrock_v766.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping)); + paletteVersions.add(new PaletteVersion("1_21_60", Bedrock_v776.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping)); paletteVersions.add(new PaletteVersion("1_21_70", Bedrock_v786.CODEC.getProtocolVersion())); GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap(); 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 c1ca49af4..6ce811356 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1443,14 +1443,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * Sends a chat message to the Java server. */ public void sendChat(String message) { - sendDownstreamGamePacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, null, 0, new BitSet())); + sendDownstreamGamePacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, null, 0, new BitSet(), 0)); } /** * Sends a command to the Java server. */ public void sendCommand(String command) { - sendDownstreamGamePacket(new ServerboundChatCommandSignedPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet())); + sendDownstreamGamePacket(new ServerboundChatCommandSignedPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet(), (byte) 0)); } public void setClientRenderDistance(int clientRenderDistance) { 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 d3e2ba5df..404e213a7 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 @@ -52,6 +52,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.click.Click; +import org.geysermc.geyser.inventory.click.ClickPlan; import org.geysermc.geyser.inventory.item.GeyserInstrument; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.BlockItem; @@ -84,7 +85,7 @@ 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.HashedStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket; @@ -132,7 +133,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator changedItem; + Int2ObjectMap changedItem; if (dropAll) { inventory.setItem(hotbarSlot, GeyserItemStack.EMPTY, session); changedItem = Int2ObjectMaps.singleton(hotbarSlot, null); @@ -142,11 +143,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator Date: Mon, 24 Mar 2025 08:35:29 +0000 Subject: [PATCH 25/86] Work on hashing components using their vanilla codecs Whoops remove this Pretty big refactor, looks a lot cleaner now More components hashing Hashers with registry access and more stuff Hasher helpers, NBT hashers Some hasher method renames Something something component hashing --- .../geyser/item/hashing/ComponentHashers.java | 115 +++++++++ .../geyser/item/hashing/MapHasher.java | 81 +++++++ .../item/hashing/MinecraftHashEncoder.java | 221 ++++++++++++++++++ .../geyser/item/hashing/MinecraftHasher.java | 108 +++++++++ .../cache/registry/JavaRegistries.java | 9 +- .../cache/registry/JavaRegistryKey.java | 16 +- 6 files changed, 546 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java new file mode 100644 index 000000000..9afcb1170 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import com.google.common.hash.HashCode; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.FoodProperties; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.IntComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +@SuppressWarnings("UnstableApiUsage") +public class ComponentHashers { + private static final Map, MinecraftHasher> hashers = new HashMap<>(); + + static { + register(DataComponentTypes.CUSTOM_DATA, hasher -> hasher.nbtMap(Function.identity())); + register(DataComponentTypes.MAX_STACK_SIZE); + register(DataComponentTypes.MAX_DAMAGE); + register(DataComponentTypes.DAMAGE); + register(DataComponentTypes.UNBREAKABLE); + + // TODO custom name, component + // TODO item name, component + + register(DataComponentTypes.ITEM_MODEL, MinecraftHasher.KEY); + + // TODO lore, component + + register(DataComponentTypes.RARITY, MinecraftHasher.RARITY); + register(DataComponentTypes.ENCHANTMENTS, MinecraftHasher.map(MinecraftHasher.ENCHANTMENT, MinecraftHasher.INT).convert(ItemEnchantments::getEnchantments)); + + // TODO can place on/can break on, complicated + // TODO attribute modifiers, attribute registry and equipment slot group hashers + + registerMap(DataComponentTypes.CUSTOM_MODEL_DATA, builder -> builder + .optionalList("floats", MinecraftHasher.FLOAT, CustomModelData::floats) + .optionalList("flags", MinecraftHasher.BOOL, CustomModelData::flags) + .optionalList("strings", MinecraftHasher.STRING, CustomModelData::strings) + .optionalList("colors", MinecraftHasher.INT, CustomModelData::colors)); + + registerMap(DataComponentTypes.TOOLTIP_DISPLAY, builder -> builder + .optional("hide_tooltip", MinecraftHasher.BOOL, TooltipDisplay::hideTooltip, false) + .optionalList("hidden_components", MinecraftHasher.DATA_COMPONENT_TYPE, TooltipDisplay::hiddenComponents)); + + register(DataComponentTypes.REPAIR_COST); + + register(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, MinecraftHasher.BOOL); + register(DataComponentTypes.INTANGIBLE_PROJECTILE); // TODO MCPL is wrong + + registerMap(DataComponentTypes.FOOD, builder -> builder + .accept("nutrition", MinecraftHasher.INT, FoodProperties::getNutrition) + .accept("saturation", MinecraftHasher.FLOAT, FoodProperties::getSaturationModifier) + .optional("can_always_eat", MinecraftHasher.BOOL, FoodProperties::isCanAlwaysEat, false)); + } + + private static void register(DataComponentType component) { + register(component, MinecraftHasher.UNIT); + } + + private static void register(IntComponentType component) { + register(component, MinecraftHasher.INT); + } + + private static void registerMap(DataComponentType component, UnaryOperator> builder) { + register(component, MinecraftHasher.mapBuilder(builder)); + } + + private static void register(DataComponentType component, MinecraftHasher hasher) { + if (hashers.containsKey(component)) { + throw new IllegalArgumentException("Tried to register a hasher for a component twice"); + } + hashers.put(component, hasher); + } + + public static HashCode hash(GeyserSession session, DataComponentType component, T value) { + MinecraftHasher hasher = (MinecraftHasher) hashers.get(component); + if (hasher == null) { + throw new IllegalStateException("Unregistered hasher for component " + component + "!"); + } + return hasher.hash(value, new MinecraftHashEncoder(session)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java new file mode 100644 index 000000000..bea4e216e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import com.google.common.hash.HashCode; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + +@SuppressWarnings("UnstableApiUsage") +public class MapHasher { + private final MinecraftHashEncoder encoder; + private final T object; + private final Map map; + + MapHasher(T object, MinecraftHashEncoder encoder) { + this.encoder = encoder; + this.object = object; + map = new HashMap<>(); + } + + public MapHasher accept(String key, HashCode hash) { + map.put(encoder.string(key), hash); + return this; + } + + public MapHasher accept(String key, MinecraftHasher hasher, Function extractor) { + return accept(key, hasher.hash(extractor.apply(object), encoder)); + } + + public MapHasher optional(String key, MinecraftHasher hasher, Function extractor, V defaultValue) { + V value = extractor.apply(object); + if (!value.equals(defaultValue)) { + accept(key, hasher.hash(value, encoder)); + } + return this; + } + + public MapHasher acceptList(String key, MinecraftHasher valueHasher, Function> extractor) { + return accept(key, valueHasher.list().hash(extractor.apply(object), encoder)); + } + + public MapHasher optionalList(String key, MinecraftHasher valueHasher, Function> extractor) { + List list = extractor.apply(object); + if (!list.isEmpty()) { + accept(key, valueHasher.list().hash(list, encoder)); + } + return this; + } + + public HashCode build() { + return encoder.map(map); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java new file mode 100644 index 000000000..3697b7c3e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import org.cloudburstmc.nbt.NbtList; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.session.GeyserSession; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Based off the HashOps in mojmap, hashes a component, TODO: documentation +@SuppressWarnings("UnstableApiUsage") +public class MinecraftHashEncoder { + private static final byte TAG_EMPTY = 1; + private static final byte TAG_MAP_START = 2; + private static final byte TAG_MAP_END = 3; + private static final byte TAG_LIST_START = 4; + private static final byte TAG_LIST_END = 5; + private static final byte TAG_BYTE = 6; + private static final byte TAG_SHORT = 7; + private static final byte TAG_INT = 8; + private static final byte TAG_LONG = 9; + private static final byte TAG_FLOAT = 10; + private static final byte TAG_DOUBLE = 11; + private static final byte TAG_STRING = 12; + private static final byte TAG_BOOLEAN = 13; + private static final byte TAG_BYTE_ARRAY_START = 14; + private static final byte TAG_BYTE_ARRAY_END = 15; + private static final byte TAG_INT_ARRAY_START = 16; + private static final byte TAG_INT_ARRAY_END = 17; + private static final byte TAG_LONG_ARRAY_START = 18; + private static final byte TAG_LONG_ARRAY_END = 19; + + private static final Comparator HASH_COMPARATOR = Comparator.comparingLong(HashCode::padToLong); + private static final Comparator> MAP_ENTRY_ORDER = Map.Entry.comparingByKey(HASH_COMPARATOR) + .thenComparing(Map.Entry.comparingByValue(HASH_COMPARATOR)); + + private static final byte[] EMPTY = new byte[]{TAG_EMPTY}; + private static final byte[] FALSE = new byte[]{TAG_BOOLEAN, 0}; + private static final byte[] TRUE = new byte[]{TAG_BOOLEAN, 1}; + + private final HashFunction hasher; + private final GeyserSession session; + + private final HashCode empty; + private final HashCode falseHash; + private final HashCode trueHash; + + public MinecraftHashEncoder(GeyserSession session) { + hasher = Hashing.crc32(); + this.session = session; + + empty = hasher.hashBytes(EMPTY); + falseHash = hasher.hashBytes(FALSE); + trueHash = hasher.hashBytes(TRUE); + } + + public GeyserSession session() { + return session; + } + + public HashCode empty() { + return empty; + } + + public HashCode number(Number number) { + if (number instanceof Byte b) { + return hasher.newHasher(2).putByte(TAG_BYTE).putByte(b).hash(); + } else if (number instanceof Short s) { + return hasher.newHasher(3).putByte(TAG_SHORT).putShort(s).hash(); + } else if (number instanceof Integer i) { + return hasher.newHasher(5).putByte(TAG_INT).putInt(i).hash(); + } else if (number instanceof Long l) { + return hasher.newHasher(9).putByte(TAG_LONG).putLong(l).hash(); + } else if (number instanceof Float f) { + return hasher.newHasher(5).putByte(TAG_FLOAT).putFloat(f).hash(); + } + + return hasher.newHasher(9).putByte(TAG_DOUBLE).putDouble(number.doubleValue()).hash(); + } + + public HashCode string(String string) { + return hasher.newHasher().putByte(TAG_STRING).putInt(string.length()).putUnencodedChars(string).hash(); + } + + public HashCode bool(boolean b) { + return b ? trueHash : falseHash; + } + + public HashCode map(Map map) { + Hasher mapHasher = hasher.newHasher(); + mapHasher.putByte(TAG_MAP_START); + map.entrySet().stream() + .sorted(MAP_ENTRY_ORDER) + .forEach(entry -> mapHasher.putBytes(entry.getKey().asBytes()).putBytes(entry.getValue().asBytes())); + mapHasher.putByte(TAG_MAP_END); + return mapHasher.hash(); + } + + public HashCode nbtMap(NbtMap map) { + Map hashed = new HashMap<>(); + for (String key : map.keySet()) { + HashCode hashedKey = string(key); + Object value = map.get(key); + if (value instanceof NbtList list) { + hashed.put(hashedKey, nbtList(list)); + } else { + map.listenForNumber(key, n -> hashed.put(hashedKey, number(n))); + map.listenForString(key, s -> hashed.put(hashedKey, string(s))); + map.listenForBoolean(key, b -> hashed.put(hashedKey, bool(b))); + map.listenForCompound(key, compound -> hashed.put(hashedKey, nbtMap(compound))); + + map.listenForByteArray(key, bytes -> hashed.put(hashedKey, byteArray(bytes))); + map.listenForIntArray(key, ints -> hashed.put(hashedKey, intArray(ints))); + map.listenForLongArray(key, longs -> hashed.put(hashedKey, longArray(longs))); + } + } + return map(hashed); + } + + public HashCode list(List list) { + Hasher listHasher = hasher.newHasher(); + listHasher.putByte(TAG_LIST_START); + list.forEach(hash -> listHasher.putBytes(hash.asBytes())); + listHasher.putByte(TAG_LIST_END); + return listHasher.hash(); + } + + // TODO can this be written better? + @SuppressWarnings("unchecked") + public HashCode nbtList(NbtList nbtList) { + NbtType type = nbtList.getType(); + List hashed = new ArrayList<>(); + + if (type == NbtType.BYTE) { + hashed.addAll(((List) nbtList).stream().map(this::number).toList()); + } else if (type == NbtType.SHORT) { + hashed.addAll(((List) nbtList).stream().map(this::number).toList()); + } else if (type == NbtType.INT) { + hashed.addAll(((List) nbtList).stream().map(this::number).toList()); + } else if (type == NbtType.LONG) { + hashed.addAll(((List) nbtList).stream().map(this::number).toList()); + } else if (type == NbtType.FLOAT) { + hashed.addAll(((List) nbtList).stream().map(this::number).toList()); + } else if (type == NbtType.DOUBLE) { + hashed.addAll(((List) nbtList).stream().map(this::number).toList()); + } else if (type == NbtType.STRING) { + hashed.addAll(((List) nbtList).stream().map(this::string).toList()); + } else if (type == NbtType.LIST) { + for (NbtList list : (List>) nbtList) { + hashed.add(nbtList(list)); + } + } else if (type == NbtType.COMPOUND) { + for (NbtMap compound : (List) nbtList) { + hashed.add(nbtMap(compound)); + } + } + + return list(hashed); + } + + public HashCode byteArray(byte[] bytes) { + Hasher arrayHasher = hasher.newHasher(); + arrayHasher.putByte(TAG_BYTE_ARRAY_START); + arrayHasher.putBytes(bytes); + arrayHasher.putByte(TAG_BYTE_ARRAY_END); + return arrayHasher.hash(); + } + + public HashCode intArray(int[] ints) { + Hasher arrayHasher = hasher.newHasher(); + arrayHasher.putByte(TAG_INT_ARRAY_START); + for (int i : ints) { + arrayHasher.putInt(i); + } + arrayHasher.putByte(TAG_INT_ARRAY_END); + return arrayHasher.hash(); + } + + public HashCode longArray(long[] longs) { + Hasher arrayHasher = hasher.newHasher(); + arrayHasher.putByte(TAG_LONG_ARRAY_START); + for (long l : longs) { + arrayHasher.putLong(l); + } + arrayHasher.putByte(TAG_LONG_ARRAY_END); + return arrayHasher.hash(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java new file mode 100644 index 000000000..98573824b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import com.google.common.hash.HashCode; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.item.components.Rarity; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +@SuppressWarnings("UnstableApiUsage") +@FunctionalInterface +public interface MinecraftHasher { + + MinecraftHasher UNIT = (unit, encoder) -> encoder.empty(); + + MinecraftHasher BYTE = (b, encoder) -> encoder.number(b); + + MinecraftHasher SHORT = (s, encoder) -> encoder.number(s); + + MinecraftHasher INT = (i, encoder) -> encoder.number(i); + + MinecraftHasher LONG = (l, encoder) -> encoder.number(l); + + MinecraftHasher FLOAT = (f, encoder) -> encoder.number(f); + + MinecraftHasher DOUBLE = (d, encoder) -> encoder.number(d); + + MinecraftHasher STRING = (s, encoder) -> encoder.string(s); + + MinecraftHasher BOOL = (b, encoder) -> encoder.bool(b); + + MinecraftHasher NBT_MAP = (map, encoder) -> encoder.nbtMap(map); + + MinecraftHasher KEY = STRING.convert(Key::asString); + + MinecraftHasher RARITY = fromIdEnum(Rarity.values(), Rarity::getName); + + MinecraftHasher ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT); + + MinecraftHasher> DATA_COMPONENT_TYPE = KEY.convert(DataComponentType::getKey); + + HashCode hash(T value, MinecraftHashEncoder encoder); + + default MinecraftHasher> list() { + return (list, encoder) -> encoder.list(list.stream().map(element -> hash(element, encoder)).toList()); + } + + default MinecraftHasher convert(Function converter) { + return (object, encoder) -> hash(converter.apply(object), encoder); + } + + default MinecraftHasher sessionConvert(BiFunction converter) { + return (object, encoder) -> hash(converter.apply(encoder.session(), object), encoder); + } + + static > MinecraftHasher fromIdEnum(T[] values, Function toName) { + return STRING.convert(id -> toName.apply(values[id])); + } + + static MinecraftHasher registry(JavaRegistryKey registry) { + return KEY.sessionConvert(registry::keyFromNetworkId); + } + + static MinecraftHasher mapBuilder(UnaryOperator> builder) { + return (object, encoder) -> builder.apply(new MapHasher<>(object, encoder)).build(); + } + + static MinecraftHasher> map(MinecraftHasher keyHasher, MinecraftHasher valueHasher) { + return (map, encoder) -> encoder.map(map.entrySet().stream() + .map(entry -> Map.entry(keyHasher.hash(entry.getKey(), encoder), valueHasher.hash(entry.getValue(), encoder))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index 732a2cc4a..fe7f55b85 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -63,8 +63,9 @@ public class JavaRegistries { public static final JavaRegistryKey COW_VARIANT = create("cow_variant", RegistryCache::cowVariants); public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant", RegistryCache::chickenVariants); - private static JavaRegistryKey create(String key, JavaRegistryKey.NetworkSerializer networkSerializer, JavaRegistryKey.NetworkDeserializer networkDeserializer) { - JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), networkSerializer, networkDeserializer); + private static JavaRegistryKey create(String key, JavaRegistryKey.NetworkSerializer networkSerializer, JavaRegistryKey.NetworkDeserializer networkDeserializer, + JavaRegistryKey.NetworkIdentifier networkIdentifier) { + JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), networkSerializer, networkDeserializer, networkIdentifier); VALUES.add(registry); return registry; } @@ -74,7 +75,9 @@ public class JavaRegistries { } private static JavaRegistryKey create(String key, RegistryGetter getter) { - return create(key, (session, object) -> getter.get(session.getRegistryCache()).byValue(object), (session, id) -> getter.get(session.getRegistryCache()).byId(id)); + return create(key, (session, object) -> getter.get(session.getRegistryCache()).byValue(object), + (session, id) -> getter.get(session.getRegistryCache()).byId(id), + (session, id) -> getter.get(session.getRegistryCache()).entryById(id).key()); } @Nullable diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java index 364d998ee..b5b40f606 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistryKey.java @@ -29,6 +29,7 @@ import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.session.GeyserSession; +// TODO describe usage in component hashers in documentation /** * Defines a Java registry, which can be hardcoded or data-driven. This class doesn't store registry contents itself, that is handled by {@link org.geysermc.geyser.session.cache.RegistryCache} in the case of * data-driven registries and other classes in the case of hardcoded registries. @@ -40,9 +41,11 @@ import org.geysermc.geyser.session.GeyserSession; * @param registryKey the registry key, as it appears on Java. * @param networkSerializer a method that converts an object in this registry to its network ID. * @param networkDeserializer a method that converts a network ID to an object in this registry. + * @param networkIdentifier a method that converts a network ID to its respective key in this registry. * @param the object type this registry holds. */ -public record JavaRegistryKey(Key registryKey, @Nullable NetworkSerializer networkSerializer, @Nullable NetworkDeserializer networkDeserializer) { +public record JavaRegistryKey(Key registryKey, @Nullable NetworkSerializer networkSerializer, @Nullable NetworkDeserializer networkDeserializer, + NetworkIdentifier networkIdentifier) { /** * Converts an object in this registry to its network ID. This will fail if this registry doesn't have a network serializer. @@ -64,6 +67,11 @@ public record JavaRegistryKey(Key registryKey, @Nullable NetworkSerializer return networkDeserializer.fromNetworkId(session, networkId); } + // TODO document + public Key keyFromNetworkId(GeyserSession session, int networkId) { + return networkIdentifier.keyFromNetworkId(session, networkId); + } + /** * @return true if this registry has a network serializer and deserializer. */ @@ -82,4 +90,10 @@ public record JavaRegistryKey(Key registryKey, @Nullable NetworkSerializer T fromNetworkId(GeyserSession session, int networkId); } + + @FunctionalInterface + public interface NetworkIdentifier { + + Key keyFromNetworkId(GeyserSession session, int networkId); + } } From 37f18d515d6a5396adae58fb1d7d3d5d4275b24e Mon Sep 17 00:00:00 2001 From: Eclipse Date: Mon, 24 Mar 2025 19:36:01 +0000 Subject: [PATCH 26/86] Some small refactors and a bunch of component hashers --- .../geyser/item/hashing/ComponentHashers.java | 30 ++++++- .../geyser/item/hashing/MapHasher.java | 17 +++- .../geyser/item/hashing/MinecraftHasher.java | 19 +++-- .../geyser/item/hashing/RegistryHasher.java | 78 +++++++++++++++++++ .../org/geysermc/geyser/item/type/Item.java | 5 ++ .../cache/registry/JavaRegistries.java | 16 +++- 6 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java index 9afcb1170..04ec5fece 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -27,14 +27,18 @@ package org.geysermc.geyser.item.hashing; import com.google.common.hash.HashCode; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.FoodProperties; import org.geysermc.mcprotocollib.protocol.data.game.item.component.IntComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown; +import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import java.util.HashMap; import java.util.Map; @@ -46,7 +50,7 @@ public class ComponentHashers { private static final Map, MinecraftHasher> hashers = new HashMap<>(); static { - register(DataComponentTypes.CUSTOM_DATA, hasher -> hasher.nbtMap(Function.identity())); + register(DataComponentTypes.CUSTOM_DATA, MinecraftHasher.NBT_MAP); register(DataComponentTypes.MAX_STACK_SIZE); register(DataComponentTypes.MAX_DAMAGE); register(DataComponentTypes.DAMAGE); @@ -60,7 +64,7 @@ public class ComponentHashers { // TODO lore, component register(DataComponentTypes.RARITY, MinecraftHasher.RARITY); - register(DataComponentTypes.ENCHANTMENTS, MinecraftHasher.map(MinecraftHasher.ENCHANTMENT, MinecraftHasher.INT).convert(ItemEnchantments::getEnchantments)); + register(DataComponentTypes.ENCHANTMENTS, MinecraftHasher.map(RegistryHasher.ENCHANTMENT, MinecraftHasher.INT).convert(ItemEnchantments::getEnchantments)); // TODO can place on/can break on, complicated // TODO attribute modifiers, attribute registry and equipment slot group hashers @@ -73,17 +77,35 @@ public class ComponentHashers { registerMap(DataComponentTypes.TOOLTIP_DISPLAY, builder -> builder .optional("hide_tooltip", MinecraftHasher.BOOL, TooltipDisplay::hideTooltip, false) - .optionalList("hidden_components", MinecraftHasher.DATA_COMPONENT_TYPE, TooltipDisplay::hiddenComponents)); + .optionalList("hidden_components", RegistryHasher.DATA_COMPONENT_TYPE, TooltipDisplay::hiddenComponents)); register(DataComponentTypes.REPAIR_COST); register(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, MinecraftHasher.BOOL); - register(DataComponentTypes.INTANGIBLE_PROJECTILE); // TODO MCPL is wrong + //register(DataComponentTypes.INTANGIBLE_PROJECTILE); // TODO MCPL is wrong registerMap(DataComponentTypes.FOOD, builder -> builder .accept("nutrition", MinecraftHasher.INT, FoodProperties::getNutrition) .accept("saturation", MinecraftHasher.FLOAT, FoodProperties::getSaturationModifier) .optional("can_always_eat", MinecraftHasher.BOOL, FoodProperties::isCanAlwaysEat, false)); + registerMap(DataComponentTypes.CONSUMABLE, builder -> builder + .optional("consume_seconds", MinecraftHasher.FLOAT, Consumable::consumeSeconds, 1.6F) + .optional("animation", MinecraftHasher.ITEM_USE_ANIMATION, Consumable::animation, Consumable.ItemUseAnimation.EAT) + .optional("sound", RegistryHasher.SOUND_EVENT, Consumable::sound, BuiltinSound.ENTITY_GENERIC_EAT) + .optional("has_consume_particles", MinecraftHasher.BOOL, Consumable::hasConsumeParticles, true)); // TODO consume effect needs identifier in MCPL + + // TODO use remainder needs item stack codec, recursion go brr + + registerMap(DataComponentTypes.USE_COOLDOWN, builder -> builder + .accept("seconds", MinecraftHasher.FLOAT, UseCooldown::seconds) + .optionalNullable("cooldown_group", MinecraftHasher.KEY, UseCooldown::cooldownGroup)); + registerMap(DataComponentTypes.DAMAGE_RESISTANT, builder -> builder + .accept("types", MinecraftHasher.TAG, Function.identity())); + registerMap(DataComponentTypes.TOOL, builder -> builder + .accept("rules", MinecraftHasher.TOOL_RULE.list(), ToolData::getRules) + .optional("default_mining_speed", MinecraftHasher.FLOAT, ToolData::getDefaultMiningSpeed, 1.0F) + .optional("damage_per_block", MinecraftHasher.INT, ToolData::getDamagePerBlock, 1) + .optional("can_destroy_blocks_in_creative", MinecraftHasher.BOOL, ToolData::isCanDestroyBlocksInCreative, true)); } private static void register(DataComponentType component) { diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java index bea4e216e..112b603f9 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MapHasher.java @@ -30,9 +30,8 @@ import com.google.common.hash.HashCode; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; +import java.util.Optional; import java.util.function.Function; -import java.util.function.Predicate; @SuppressWarnings("UnstableApiUsage") public class MapHasher { @@ -55,6 +54,20 @@ public class MapHasher { return accept(key, hasher.hash(extractor.apply(object), encoder)); } + public MapHasher optionalNullable(String key, MinecraftHasher hasher, Function extractor) { + V value = extractor.apply(object); + if (value != null) { + accept(key, hasher.hash(value, encoder)); + } + return this; + } + + public MapHasher optional(String key, MinecraftHasher hasher, Function> extractor) { + Optional value = extractor.apply(object); + value.ifPresent(v -> accept(key, hasher.hash(v, encoder))); + return this; + } + public MapHasher optional(String key, MinecraftHasher hasher, Function extractor, V defaultValue) { V value = extractor.apply(object); if (!value.equals(defaultValue)) { diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java index 98573824b..53ae6b8ce 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java @@ -30,9 +30,8 @@ import net.kyori.adventure.key.Key; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.session.GeyserSession; -import org.geysermc.geyser.session.cache.registry.JavaRegistries; -import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; import java.util.List; @@ -68,11 +67,16 @@ public interface MinecraftHasher { MinecraftHasher KEY = STRING.convert(Key::asString); + MinecraftHasher TAG = STRING.convert(key -> "#" + key.asString()); + MinecraftHasher RARITY = fromIdEnum(Rarity.values(), Rarity::getName); - MinecraftHasher ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT); + MinecraftHasher ITEM_USE_ANIMATION = fromEnum(); - MinecraftHasher> DATA_COMPONENT_TYPE = KEY.convert(DataComponentType::getKey); + MinecraftHasher TOOL_RULE = mapBuilder(builder -> builder + .accept("blocks", RegistryHasher.BLOCK.holderSet(), ToolData.Rule::getBlocks) + .optionalNullable("speed", MinecraftHasher.FLOAT, ToolData.Rule::getSpeed) + .optionalNullable("correct_for_drops", MinecraftHasher.BOOL, ToolData.Rule::getCorrectForDrops)); HashCode hash(T value, MinecraftHashEncoder encoder); @@ -92,8 +96,9 @@ public interface MinecraftHasher { return STRING.convert(id -> toName.apply(values[id])); } - static MinecraftHasher registry(JavaRegistryKey registry) { - return KEY.sessionConvert(registry::keyFromNetworkId); + // TODO: note that this only works correctly if enum constants are named appropriately + static > MinecraftHasher fromEnum() { + return STRING.convert(Enum::name); } static MinecraftHasher mapBuilder(UnaryOperator> builder) { diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java new file mode 100644 index 000000000..09948a9f1 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; +import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; +import org.geysermc.mcprotocollib.protocol.data.game.level.sound.CustomSound; +import org.geysermc.mcprotocollib.protocol.data.game.level.sound.Sound; + +import java.util.Arrays; + +public interface RegistryHasher extends MinecraftHasher { + + RegistryHasher BLOCK = registry(JavaRegistries.BLOCK); + + RegistryHasher ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT); + + MinecraftHasher> DATA_COMPONENT_TYPE = KEY.convert(DataComponentType::getKey); + + MinecraftHasher BUILTIN_SOUND = KEY.convert(sound -> MinecraftKey.key(sound.getName())); + + MinecraftHasher CUSTOM_SOUND = MinecraftHasher.mapBuilder(builder -> builder + .accept("sound_id", KEY, sound -> MinecraftKey.key(sound.getName())) + .optional("range", FLOAT, CustomSound::getRange, 16.0F)); + + MinecraftHasher SOUND_EVENT = (sound, encoder) -> { + if (sound instanceof BuiltinSound builtin) { + return BUILTIN_SOUND.hash(builtin, encoder); + } + return CUSTOM_SOUND.hash((CustomSound) sound, encoder); + }; + + static RegistryHasher registry(JavaRegistryKey registry) { + MinecraftHasher hasher = KEY.sessionConvert(registry::keyFromNetworkId); + return hasher::hash; + } + + default MinecraftHasher holderSet() { + return (holder, encoder) -> { + if (holder.getLocation() != null) { + return TAG.hash(holder.getLocation(), encoder); + } else if (holder.getHolders() != null) { + if (holder.getHolders().length == 1) { + return hash(holder.getHolders()[0], encoder); + } + return list().hash(Arrays.stream(holder.getHolders()).boxed().toList(), encoder); + } + throw new IllegalStateException("HolderSet must have either tag location or holders"); + }; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 9919aa308..013aaab65 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -75,10 +75,15 @@ public class Item { this.attackDamage = builder.attackDamage; } + // TODO maybe deprecate? public String javaIdentifier() { return javaIdentifier.asString(); } + public Key javaKey() { + return javaIdentifier; + } + public int javaId() { return javaId; } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java index fe7f55b85..0a50f63b8 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/registry/JavaRegistries.java @@ -47,11 +47,12 @@ import java.util.List; /** * Stores {@link JavaRegistryKey} for Java registries that are used for loading of data-driven objects, tags, or both. Read {@link JavaRegistryKey} for more information on how to use one. */ +// TODO for component hashing, elements should never be null (looking at you, animal variants) public class JavaRegistries { private static final List> VALUES = new ArrayList<>(); - public static final JavaRegistryKey BLOCK = create("block", BlockRegistries.JAVA_BLOCKS, Block::javaId); - public static final JavaRegistryKey ITEM = create("item", Registries.JAVA_ITEMS, Item::javaId); + public static final JavaRegistryKey BLOCK = create("block", BlockRegistries.JAVA_BLOCKS, Block::javaId, Block::javaIdentifier); + public static final JavaRegistryKey ITEM = create("item", Registries.JAVA_ITEMS, Item::javaId, Item::javaKey); public static final JavaRegistryKey ENCHANTMENT = create("enchantment", RegistryCache::enchantments); public static final JavaRegistryKey BANNER_PATTERN = create("banner_pattern", RegistryCache::bannerPatterns); @@ -70,8 +71,9 @@ public class JavaRegistries { return registry; } - private static JavaRegistryKey create(String key, ListRegistry registry, RegistryNetworkMapper networkSerializer) { - return create(key, (session, object) -> networkSerializer.get(object), (session, id) -> registry.get(id)); + private static JavaRegistryKey create(String key, ListRegistry registry, RegistryNetworkMapper networkSerializer, RegistryIdentifierMapper identifierMapper) { + return create(key, (session, object) -> networkSerializer.get(object), + (session, id) -> registry.get(id), (session, id) -> identifierMapper.get(registry.get(id))); } private static JavaRegistryKey create(String key, RegistryGetter getter) { @@ -101,4 +103,10 @@ public class JavaRegistries { int get(T object); } + + @FunctionalInterface + interface RegistryIdentifierMapper { + + Key get(T object); + } } From 51cc5eb41a202ac38477c1e0279a0409a6b1827e Mon Sep 17 00:00:00 2001 From: Eclipse Date: Mon, 24 Mar 2025 21:15:59 +0000 Subject: [PATCH 27/86] Some fixes, and actually test everything --- .../geyser/item/hashing/ComponentHashers.java | 75 +++++++++++++++++++ .../item/hashing/MinecraftHashEncoder.java | 10 ++- .../geyser/item/hashing/MinecraftHasher.java | 2 +- .../player/JavaPlayerPositionTranslator.java | 3 + 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java index 04ec5fece..fd6b012af 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -26,12 +26,20 @@ package org.geysermc.geyser.item.hashing; import com.google.common.hash.HashCode; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.nbt.NbtList; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.item.components.Rarity; +import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.FoodProperties; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import org.geysermc.mcprotocollib.protocol.data.game.item.component.IntComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; @@ -41,6 +49,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -134,4 +143,70 @@ public class ComponentHashers { } return hasher.hash(value, new MinecraftHashEncoder(session)); } + + public static void testHashing(GeyserSession session) { + // Hashed values generated by vanilla Java + + NbtMap customData = NbtMap.builder() + .putString("hello", "g'day") + .putBoolean("nice?", false) + .putByte("coolness", (byte) 100) + .putCompound("geyser", NbtMap.builder() + .putString("is", "very cool") + .build()) + .putList("a list", NbtType.LIST, List.of(new NbtList<>(NbtType.STRING, "in a list"))) + .build(); + + testHash(session, DataComponentTypes.CUSTOM_DATA, customData, -385053299); + + testHash(session, DataComponentTypes.MAX_STACK_SIZE, 64, 733160003); + testHash(session, DataComponentTypes.MAX_DAMAGE, 13, -801733367); + testHash(session, DataComponentTypes.DAMAGE, 459, 1211405277); + testHash(session, DataComponentTypes.UNBREAKABLE, Unit.INSTANCE, -982207288); + + testHash(session, DataComponentTypes.ITEM_MODEL, MinecraftKey.key("testing"), -689946239); + + testHash(session, DataComponentTypes.RARITY, Rarity.COMMON.ordinal(), 75150990); + testHash(session, DataComponentTypes.RARITY, Rarity.RARE.ordinal(), -1420566726); + testHash(session, DataComponentTypes.RARITY, Rarity.EPIC.ordinal(), -292715907); + + testHash(session, DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(Map.of( + 0, 1 + ), false), 0); // TODO identifier lookup + + testHash(session, DataComponentTypes.CUSTOM_MODEL_DATA, + new CustomModelData(List.of(5.0F, 3.0F, -1.0F), List.of(false, true, false), List.of("1", "3", "2"), List.of(3424, -123, 345)), 1947635619); + + testHash(session, DataComponentTypes.CUSTOM_MODEL_DATA, + new CustomModelData(List.of(5.03F, 3.0F, -1.11F), List.of(true, true, false), List.of("2", "5", "7"), List.of()), -512419908); + + testHash(session, DataComponentTypes.TOOLTIP_DISPLAY, new TooltipDisplay(false, List.of(DataComponentTypes.CONSUMABLE, DataComponentTypes.DAMAGE)), -816418453); + testHash(session, DataComponentTypes.TOOLTIP_DISPLAY, new TooltipDisplay(true, List.of()), 14016722); + testHash(session, DataComponentTypes.TOOLTIP_DISPLAY, new TooltipDisplay(false, List.of()), -982207288); + + testHash(session, DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true, -1019818302); + testHash(session, DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, false, 828198337); + + testHash(session, DataComponentTypes.FOOD, new FoodProperties(5, 1.4F, false), 445786378); + testHash(session, DataComponentTypes.FOOD, new FoodProperties(3, 5.7F, true), 1917653498); + testHash(session, DataComponentTypes.FOOD, new FoodProperties(7, 0.15F, false), -184166204); + + testHash(session, DataComponentTypes.DAMAGE_RESISTANT, Key.key("testing"), -1230493835); + + testHash(session, DataComponentTypes.TOOL, new ToolData(List.of(), 5.0F, 3, false), -1789071928); + testHash(session, DataComponentTypes.TOOL, new ToolData(List.of(), 3.0F, 1, true), -7422944); + testHash(session, DataComponentTypes.TOOL, new ToolData(List.of( + new ToolData.Rule(new HolderSet(Key.key("acacia_logs")), null, null), + new ToolData.Rule(new HolderSet(new int[]{Blocks.JACK_O_LANTERN.javaId(), Blocks.WALL_TORCH.javaId()}), 4.2F, true), + new ToolData.Rule(new HolderSet(new int[]{Blocks.PUMPKIN.javaId()}), 7.0F, false)), + 1.0F, 1, true), 2103678261); + + // Chunk errors are spamming logs and I don't need to log in anyway + session.disconnect("AAAAAA"); + } + + private static void testHash(GeyserSession session, DataComponentType component, T value, int expected) { + int got = hash(session, component, value).asInt(); + System.out.println("Testing hashing component " + component.getKey() + ", expected " + expected + ", got " + got + " " + (got == expected ? "PASS" : "ERROR")); + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java index 3697b7c3e..ab8ca435a 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHashEncoder.java @@ -68,6 +68,7 @@ public class MinecraftHashEncoder { .thenComparing(Map.Entry.comparingByValue(HASH_COMPARATOR)); private static final byte[] EMPTY = new byte[]{TAG_EMPTY}; + public static final byte[] EMPTY_MAP = new byte[]{TAG_MAP_START, TAG_MAP_END}; private static final byte[] FALSE = new byte[]{TAG_BOOLEAN, 0}; private static final byte[] TRUE = new byte[]{TAG_BOOLEAN, 1}; @@ -75,14 +76,16 @@ public class MinecraftHashEncoder { private final GeyserSession session; private final HashCode empty; + private final HashCode emptyMap; private final HashCode falseHash; private final HashCode trueHash; public MinecraftHashEncoder(GeyserSession session) { - hasher = Hashing.crc32(); + hasher = Hashing.crc32c(); this.session = session; empty = hasher.hashBytes(EMPTY); + emptyMap = hasher.hashBytes(EMPTY_MAP); falseHash = hasher.hashBytes(FALSE); trueHash = hasher.hashBytes(TRUE); } @@ -95,6 +98,10 @@ public class MinecraftHashEncoder { return empty; } + public HashCode emptyMap() { + return emptyMap; + } + public HashCode number(Number number) { if (number instanceof Byte b) { return hasher.newHasher(2).putByte(TAG_BYTE).putByte(b).hash(); @@ -139,7 +146,6 @@ public class MinecraftHashEncoder { } else { map.listenForNumber(key, n -> hashed.put(hashedKey, number(n))); map.listenForString(key, s -> hashed.put(hashedKey, string(s))); - map.listenForBoolean(key, b -> hashed.put(hashedKey, bool(b))); map.listenForCompound(key, compound -> hashed.put(hashedKey, nbtMap(compound))); map.listenForByteArray(key, bytes -> hashed.put(hashedKey, byteArray(bytes))); diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java index 53ae6b8ce..61feaa4ef 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java @@ -45,7 +45,7 @@ import java.util.stream.Collectors; @FunctionalInterface public interface MinecraftHasher { - MinecraftHasher UNIT = (unit, encoder) -> encoder.empty(); + MinecraftHasher UNIT = (unit, encoder) -> encoder.emptyMap(); MinecraftHasher BYTE = (b, encoder) -> encoder.number(b); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java index d56c5d2d3..7ad560974 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java @@ -32,6 +32,7 @@ import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.RespawnPacket; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.item.hashing.ComponentHashers; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.TeleportCache; import org.geysermc.geyser.translator.protocol.PacketTranslator; @@ -83,6 +84,8 @@ public class JavaPlayerPositionTranslator extends PacketTranslator Date: Mon, 24 Mar 2025 22:53:20 +0000 Subject: [PATCH 28/86] A whole bunch more components and tests --- .../geyser/item/hashing/ComponentHashers.java | 98 ++++++++++++++++++- .../geyser/item/hashing/MinecraftHasher.java | 14 ++- .../geyser/item/hashing/RegistryHasher.java | 20 ++++ 3 files changed, 127 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java index fd6b012af..ca2b17c19 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -26,26 +26,35 @@ package org.geysermc.geyser.item.hashing; import com.google.common.hash.HashCode; -import net.kyori.adventure.key.Key; import org.cloudburstmc.nbt.NbtList; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.inventory.item.Potion; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.level.block.Blocks; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.FoodProperties; import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import org.geysermc.mcprotocollib.protocol.data.game.item.component.IntComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectDetails; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectInstance; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.TooltipDisplay; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Weapon; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import java.util.HashMap; @@ -111,10 +120,50 @@ public class ComponentHashers { registerMap(DataComponentTypes.DAMAGE_RESISTANT, builder -> builder .accept("types", MinecraftHasher.TAG, Function.identity())); registerMap(DataComponentTypes.TOOL, builder -> builder - .accept("rules", MinecraftHasher.TOOL_RULE.list(), ToolData::getRules) + .acceptList("rules", MinecraftHasher.TOOL_RULE, ToolData::getRules) .optional("default_mining_speed", MinecraftHasher.FLOAT, ToolData::getDefaultMiningSpeed, 1.0F) .optional("damage_per_block", MinecraftHasher.INT, ToolData::getDamagePerBlock, 1) .optional("can_destroy_blocks_in_creative", MinecraftHasher.BOOL, ToolData::isCanDestroyBlocksInCreative, true)); + registerMap(DataComponentTypes.WEAPON, builder -> builder + .optional("item_damage_per_attack", MinecraftHasher.INT, Weapon::itemDamagePerAttack, 1) + .optional("disable_blocking_for_seconds", MinecraftHasher.FLOAT, Weapon::disableBlockingForSeconds, 0.0F)); + registerMap(DataComponentTypes.ENCHANTABLE, builder -> builder + .accept("value", MinecraftHasher.INT, Function.identity())); + registerMap(DataComponentTypes.EQUIPPABLE, builder -> builder + .accept("slot", MinecraftHasher.EQUIPMENT_SLOT, Equippable::slot) + .optional("equip_sound", RegistryHasher.SOUND_EVENT, Equippable::equipSound, BuiltinSound.ITEM_ARMOR_EQUIP_GENERIC) + .optionalNullable("asset_id", MinecraftHasher.KEY, Equippable::model) + .optionalNullable("camera_overlay", MinecraftHasher.KEY, Equippable::cameraOverlay) + .optionalNullable("allowed_entities", RegistryHasher.ENTITY_TYPE.holderSet(), Equippable::allowedEntities) + .optional("dispensable", MinecraftHasher.BOOL, Equippable::dispensable, true) + .optional("swappable", MinecraftHasher.BOOL, Equippable::swappable, true) + .optional("damage_on_hurt", MinecraftHasher.BOOL, Equippable::damageOnHurt, true) + .optional("equip_on_interact", MinecraftHasher.BOOL, Equippable::equipOnInteract, false)); + registerMap(DataComponentTypes.REPAIRABLE, builder -> builder + .accept("items", RegistryHasher.ITEM.holderSet(), Function.identity())); + + register(DataComponentTypes.GLIDER); + register(DataComponentTypes.TOOLTIP_STYLE, MinecraftHasher.KEY); + + registerMap(DataComponentTypes.DEATH_PROTECTION, builder -> builder); // TODO consume effect needs identifier in MCPL + registerMap(DataComponentTypes.BLOCKS_ATTACKS, builder -> builder); // TODO needs damage types, add a way to cache identifiers without reading objects in registrycache + register(DataComponentTypes.STORED_ENCHANTMENTS, MinecraftHasher.map(RegistryHasher.ENCHANTMENT, MinecraftHasher.INT).convert(ItemEnchantments::getEnchantments)); // TODO duplicate code? + + register(DataComponentTypes.DYED_COLOR); + register(DataComponentTypes.MAP_COLOR); + register(DataComponentTypes.MAP_ID); + register(DataComponentTypes.MAP_DECORATIONS, MinecraftHasher.NBT_MAP); + + // TODO charged projectiles also need the recursionâ„¢ + // TODO same for bundle contents + + registerMap(DataComponentTypes.POTION_CONTENTS, builder -> builder + .optional("potion", RegistryHasher.POTION, PotionContents::getPotionId, -1) + .optional("custom_color", MinecraftHasher.INT, PotionContents::getCustomColor, -1) + .optionalList("custom_effects", MinecraftHasher.MOB_EFFECT_INSTANCE, PotionContents::getCustomEffects) + .optionalNullable("custom_name", MinecraftHasher.STRING, PotionContents::getCustomName)); + + register(DataComponentTypes.POTION_DURATION_SCALE, MinecraftHasher.FLOAT); } private static void register(DataComponentType component) { @@ -191,16 +240,57 @@ public class ComponentHashers { testHash(session, DataComponentTypes.FOOD, new FoodProperties(3, 5.7F, true), 1917653498); testHash(session, DataComponentTypes.FOOD, new FoodProperties(7, 0.15F, false), -184166204); - testHash(session, DataComponentTypes.DAMAGE_RESISTANT, Key.key("testing"), -1230493835); + testHash(session, DataComponentTypes.DAMAGE_RESISTANT, MinecraftKey.key("testing"), -1230493835); testHash(session, DataComponentTypes.TOOL, new ToolData(List.of(), 5.0F, 3, false), -1789071928); testHash(session, DataComponentTypes.TOOL, new ToolData(List.of(), 3.0F, 1, true), -7422944); testHash(session, DataComponentTypes.TOOL, new ToolData(List.of( - new ToolData.Rule(new HolderSet(Key.key("acacia_logs")), null, null), + new ToolData.Rule(new HolderSet(MinecraftKey.key("acacia_logs")), null, null), new ToolData.Rule(new HolderSet(new int[]{Blocks.JACK_O_LANTERN.javaId(), Blocks.WALL_TORCH.javaId()}), 4.2F, true), new ToolData.Rule(new HolderSet(new int[]{Blocks.PUMPKIN.javaId()}), 7.0F, false)), 1.0F, 1, true), 2103678261); + testHash(session, DataComponentTypes.WEAPON, new Weapon(5, 2.0F), -154556976); + testHash(session, DataComponentTypes.WEAPON, new Weapon(1, 7.3F), 885347995); + + testHash(session, DataComponentTypes.ENCHANTABLE, 3, -1834983819); + + testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.ITEM_ARMOR_EQUIP_GENERIC, null, null, null, + true, true, true, false), 1294431019); + testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.ITEM_ARMOR_EQUIP_CHAIN, MinecraftKey.key("testing"), null, null, + true, true, true, false), 1226203061); + testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.AMBIENT_CAVE, null, null, null, + false, true, false, false), 1416408052); + testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.ENTITY_BREEZE_WIND_BURST, null, MinecraftKey.key("testing"), + new HolderSet(new int[]{EntityType.ACACIA_BOAT.ordinal()}), false, true, false, false), 1711275245); + + testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.HELMET, BuiltinSound.ITEM_ARMOR_EQUIP_GENERIC, null, null, null, + true, true, true, false), 497790992); // TODO broken because equipment slot names don't match + + testHash(session, DataComponentTypes.REPAIRABLE, new HolderSet(new int[]{Items.AMETHYST_BLOCK.javaId(), Items.PUMPKIN.javaId()}), -36715567); + + NbtMap mapDecorations = NbtMap.builder() + .putCompound("test_decoration", NbtMap.builder() + .putString("type", "minecraft:player") + .putDouble("x", 45.0) + .putDouble("z", 67.4) + .putFloat("rotation", 39.5F) + .build()) + .build(); + + testHash(session, DataComponentTypes.MAP_DECORATIONS, mapDecorations, -625782954); + + testHash(session, DataComponentTypes.POTION_CONTENTS, new PotionContents(Potion.FIRE_RESISTANCE.ordinal(), -1, List.of(), null), -772576502); + testHash(session, DataComponentTypes.POTION_CONTENTS, new PotionContents(-1, 20, + List.of(new MobEffectInstance(Effect.CONDUIT_POWER, new MobEffectDetails(0, 0, false, true, true, null))), + null), -902075187); + testHash(session, DataComponentTypes.POTION_CONTENTS, new PotionContents(-1, 96, + List.of(new MobEffectInstance(Effect.JUMP_BOOST, new MobEffectDetails(57, 17, true, false, false, null))), + null), -17231244); + testHash(session, DataComponentTypes.POTION_CONTENTS, new PotionContents(-1, 87, + List.of(new MobEffectInstance(Effect.SPEED, new MobEffectDetails(29, 1004, false, true, true, null))), + "testing"), 2007296036); + // Chunk errors are spamming logs and I don't need to log in anyway session.disconnect("AAAAAA"); } diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java index 61feaa4ef..b675a236e 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java @@ -30,7 +30,9 @@ import net.kyori.adventure.key.Key; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectInstance; import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; @@ -78,6 +80,16 @@ public interface MinecraftHasher { .optionalNullable("speed", MinecraftHasher.FLOAT, ToolData.Rule::getSpeed) .optionalNullable("correct_for_drops", MinecraftHasher.BOOL, ToolData.Rule::getCorrectForDrops)); + MinecraftHasher EQUIPMENT_SLOT = fromEnum(); + + MinecraftHasher MOB_EFFECT_INSTANCE = mapBuilder(builder -> builder + .accept("id", RegistryHasher.EFFECT, MobEffectInstance::getEffect) + .optional("amplifier", BYTE, instance -> (byte) instance.getDetails().getAmplifier(), (byte) 0) + .optional("duration", INT, instance -> instance.getDetails().getDuration(), 0) + .optional("ambient", BOOL, instance -> instance.getDetails().isAmbient(), false) + .optional("show_particles", BOOL, instance -> instance.getDetails().isShowParticles(), true) + .accept("show_icon", BOOL, instance -> instance.getDetails().isShowIcon())); // TODO check this, also hidden effect but is recursive + HashCode hash(T value, MinecraftHashEncoder encoder); default MinecraftHasher> list() { @@ -98,7 +110,7 @@ public interface MinecraftHasher { // TODO: note that this only works correctly if enum constants are named appropriately static > MinecraftHasher fromEnum() { - return STRING.convert(Enum::name); + return STRING.convert(t -> t.name().toLowerCase()); } static MinecraftHasher mapBuilder(UnaryOperator> builder) { diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java index 09948a9f1..c30805952 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java @@ -25,9 +25,12 @@ package org.geysermc.geyser.item.hashing; +import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; @@ -40,8 +43,16 @@ public interface RegistryHasher extends MinecraftHasher { RegistryHasher BLOCK = registry(JavaRegistries.BLOCK); + RegistryHasher ITEM = registry(JavaRegistries.ITEM); + + RegistryHasher ENTITY_TYPE = enumIdRegistry(EntityType.values()); + RegistryHasher ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT); + MinecraftHasher EFFECT = enumRegistry(); + + RegistryHasher POTION = enumIdRegistry(Potion.values()); + MinecraftHasher> DATA_COMPONENT_TYPE = KEY.convert(DataComponentType::getKey); MinecraftHasher BUILTIN_SOUND = KEY.convert(sound -> MinecraftKey.key(sound.getName())); @@ -62,6 +73,15 @@ public interface RegistryHasher extends MinecraftHasher { return hasher::hash; } + static > MinecraftHasher enumRegistry() { + return KEY.convert(t -> MinecraftKey.key(t.name().toLowerCase())); + } + + static > RegistryHasher enumIdRegistry(T[] values) { + MinecraftHasher hasher = KEY.convert(i -> MinecraftKey.key(values[i].name().toLowerCase())); + return hasher::hash; + } + default MinecraftHasher holderSet() { return (holder, encoder) -> { if (holder.getLocation() != null) { From 4e06608f8402ca8c0e9f9b90de06cca40dd57220 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Tue, 25 Mar 2025 20:27:05 +0000 Subject: [PATCH 29/86] MCPL fixes, make it run --- .../geyser/entity/EntityDefinitions.java | 40 +++++++++---------- .../geyser/item/hashing/ComponentHashers.java | 5 +-- .../geyser/item/type/EnchantedBookItem.java | 2 +- .../entity/VaultBlockEntityTranslator.java | 2 +- gradle/libs.versions.toml | 2 +- 5 files changed, 24 insertions(+), 27 deletions(-) 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 01206fa8d..41d69f002 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -312,7 +312,7 @@ public final class EntityDefinitions { EntityDefinition entityBase = EntityDefinition.builder(Entity::new) .addTranslator(MetadataTypes.BYTE, Entity::setFlags) .addTranslator(MetadataTypes.INT, Entity::setAir) // Air/bubbles - .addTranslator(MetadataTypes.OPTIONAL_CHAT, Entity::setDisplayName) + .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, Entity::setDisplayName) .addTranslator(MetadataTypes.BOOLEAN, Entity::setDisplayNameVisible) .addTranslator(MetadataTypes.BOOLEAN, Entity::setSilent) .addTranslator(MetadataTypes.BOOLEAN, Entity::setGravity) @@ -337,7 +337,7 @@ public final class EntityDefinitions { .type(EntityType.END_CRYSTAL) .heightAndWidth(2.0f) .identifier("minecraft:ender_crystal") - .addTranslator(MetadataTypes.OPTIONAL_POSITION, EnderCrystalEntity::setBlockTarget) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, EnderCrystalEntity::setBlockTarget) .addTranslator(MetadataTypes.BOOLEAN, (enderCrystalEntity, entityMetadata) -> enderCrystalEntity.setFlag(EntityFlag.SHOW_BOTTOM, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) // There is a base located on the ender crystal .build(); @@ -365,8 +365,8 @@ public final class EntityDefinitions { .type(EntityType.FIREWORK_ROCKET) .heightAndWidth(0.25f) .identifier("minecraft:fireworks_rocket") - .addTranslator(MetadataTypes.ITEM, FireworkEntity::setFireworkItem) - .addTranslator(MetadataTypes.OPTIONAL_VARINT, FireworkEntity::setPlayerGliding) + .addTranslator(MetadataTypes.ITEM_STACK, FireworkEntity::setFireworkItem) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FireworkEntity::setPlayerGliding) .addTranslator(null) // Shot at angle .build(); FISHING_BOBBER = EntityDefinition.inherited(null, entityBase) @@ -379,7 +379,7 @@ public final class EntityDefinitions { .type(EntityType.ITEM) .heightAndWidth(0.25f) .offset(0.125f) - .addTranslator(MetadataTypes.ITEM, ItemEntity::setItem) + .addTranslator(MetadataTypes.ITEM_STACK, ItemEntity::setItem) .build(); LEASH_KNOT = EntityDefinition.inherited(LeashKnotEntity::new, entityBase) .type(EntityType.LEASH_KNOT) @@ -428,7 +428,7 @@ public final class EntityDefinitions { .type(EntityType.TEXT_DISPLAY) .identifier("minecraft:armor_stand") .offset(-0.5f) - .addTranslator(MetadataTypes.CHAT, TextDisplayEntity::setText) + .addTranslator(MetadataTypes.COMPONENT, TextDisplayEntity::setText) .addTranslator(null) // Line width .addTranslator(null) // Background color .addTranslator(null) // Text opacity @@ -457,7 +457,7 @@ public final class EntityDefinitions { .build(); EntityDefinition throwableItemBase = EntityDefinition.inherited(ThrowableItemEntity::new, entityBase) - .addTranslator(MetadataTypes.ITEM, ThrowableItemEntity::setItem) + .addTranslator(MetadataTypes.ITEM_STACK, ThrowableItemEntity::setItem) .build(); EGG = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.EGG) @@ -521,7 +521,7 @@ public final class EntityDefinitions { // Item frames are handled differently as they are blocks, not items, in Bedrock ITEM_FRAME = EntityDefinition.inherited(null, entityBase) .type(EntityType.ITEM_FRAME) - .addTranslator(MetadataTypes.ITEM, ItemFrameEntity::setItemInFrame) + .addTranslator(MetadataTypes.ITEM_STACK, ItemFrameEntity::setItemInFrame) .addTranslator(MetadataTypes.INT, ItemFrameEntity::setItemRotation) .build(); GLOW_ITEM_FRAME = EntityDefinition.inherited(ITEM_FRAME.factory(), ITEM_FRAME) @@ -547,7 +547,7 @@ public final class EntityDefinitions { COMMAND_BLOCK_MINECART = EntityDefinition.inherited(CommandBlockMinecartEntity::new, MINECART) .type(EntityType.COMMAND_BLOCK_MINECART) .addTranslator(MetadataTypes.STRING, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_NAME, entityMetadata.getValue())) - .addTranslator(MetadataTypes.CHAT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue()))) + .addTranslator(MetadataTypes.COMPONENT, (entity, entityMetadata) -> entity.getDirtyMetadata().put(EntityDataTypes.COMMAND_BLOCK_LAST_OUTPUT, MessageTranslator.convertMessage(entityMetadata.getValue()))) .build(); FURNACE_MINECART = EntityDefinition.inherited(FurnaceMinecartEntity::new, MINECART) .type(EntityType.FURNACE_MINECART) @@ -623,19 +623,19 @@ public final class EntityDefinitions { (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityDataTypes.EFFECT_AMBIENCE, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0))) .addTranslator(null) // Arrow count .addTranslator(null) // Stinger count - .addTranslator(MetadataTypes.OPTIONAL_POSITION, LivingEntity::setBedPosition) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, LivingEntity::setBedPosition) .build(); ARMOR_STAND = EntityDefinition.inherited(ArmorStandEntity::new, livingEntityBase) .type(EntityType.ARMOR_STAND) .height(1.975f).width(0.5f) .addTranslator(MetadataTypes.BYTE, ArmorStandEntity::setArmorStandFlags) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setHeadRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setBodyRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setLeftArmRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setRightArmRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setLeftLegRotation) - .addTranslator(MetadataTypes.ROTATION, ArmorStandEntity::setRightLegRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setHeadRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setBodyRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftArmRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightArmRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setLeftLegRotation) + .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .build(); PLAYER = EntityDefinition.inherited(null, livingEntityBase) .type(EntityType.PLAYER) @@ -645,8 +645,8 @@ public final class EntityDefinitions { .addTranslator(null) // Player score .addTranslator(MetadataTypes.BYTE, PlayerEntity::setSkinVisibility) .addTranslator(null) // Player main hand - .addTranslator(MetadataTypes.NBT_TAG, PlayerEntity::setLeftParrot) - .addTranslator(MetadataTypes.NBT_TAG, PlayerEntity::setRightParrot) + .addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setLeftParrot) + .addTranslator(MetadataTypes.COMPOUND_TAG, PlayerEntity::setRightParrot) .build(); EntityDefinition mobEntityBase = EntityDefinition.inherited(MobEntity::new, livingEntityBase) @@ -686,7 +686,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setCanMove) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown) - .addTranslator(MetadataTypes.OPTIONAL_POSITION, CreakingEntity::setHomePos) + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos) .properties(VanillaEntityProperties.CREAKING) .build(); CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase) @@ -981,7 +981,7 @@ public final class EntityDefinitions { .type(EntityType.FROG) .heightAndWidth(0.5f) .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setVariant) - .addTranslator(MetadataTypes.OPTIONAL_VARINT, FrogEntity::setTongueTarget) + .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FrogEntity::setTongueTarget) .build(); HOGLIN = EntityDefinition.inherited(HoglinEntity::new, ageableEntityBase) .type(EntityType.HOGLIN) diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java index ca2b17c19..9cfd7e517 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -221,7 +221,7 @@ public class ComponentHashers { testHash(session, DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(Map.of( 0, 1 - ), false), 0); // TODO identifier lookup + )), 0); // TODO identifier lookup testHash(session, DataComponentTypes.CUSTOM_MODEL_DATA, new CustomModelData(List.of(5.0F, 3.0F, -1.0F), List.of(false, true, false), List.of("1", "3", "2"), List.of(3424, -123, 345)), 1947635619); @@ -290,9 +290,6 @@ public class ComponentHashers { testHash(session, DataComponentTypes.POTION_CONTENTS, new PotionContents(-1, 87, List.of(new MobEffectInstance(Effect.SPEED, new MobEffectDetails(29, 1004, false, true, true, null))), "testing"), 2007296036); - - // Chunk errors are spamming logs and I don't need to log in anyway - session.disconnect("AAAAAA"); } private static void testHash(GeyserSession session, DataComponentType component, T value, int expected) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java index bc6dcde67..09dbca050 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java @@ -95,7 +95,7 @@ public class EnchantedBookItem extends Item { } } if (!javaEnchantments.isEmpty()) { - components.put(DataComponentTypes.STORED_ENCHANTMENTS, new ItemEnchantments(javaEnchantments, true)); + components.put(DataComponentTypes.STORED_ENCHANTMENTS, new ItemEnchantments(javaEnchantments)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java index 46a4b78bf..cff1f0bb7 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/VaultBlockEntityTranslator.java @@ -145,6 +145,6 @@ public class VaultBlockEntityTranslator extends BlockEntityTranslator { } } } - components.put(DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(enchantments, true)); + components.put(DataComponentTypes.ENCHANTMENTS, new ItemEnchantments(enchantments)); }); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7aa0c9e75..a29969ab2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ protocol-common = "3.0.0.Beta6-20250324.162731-5" protocol-codec = "3.0.0.Beta6-20250324.162731-5" raknet = "1.0.0.CR3-20250218.160705-18" minecraftauth = "4.1.1" -mcprotocollib = "1.21.5-20250323.182633-13" +mcprotocollib = "1.21.5-20250325.093126-16" adventure = "4.14.0" adventure-platform = "4.3.0" junit = "5.9.2" From 413be6c8b8038a44500bf26e13d3d03da5af1c48 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Tue, 25 Mar 2025 20:37:14 +0000 Subject: [PATCH 30/86] Some cleanups and stuff --- .../geyser/item/hashing/ComponentHashers.java | 23 ++++++++++--------- .../geyser/item/hashing/MinecraftHasher.java | 2 +- .../DataComponentRegistryPopulator.java | 7 ------ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java index 9cfd7e517..3bcd6aa38 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -69,10 +69,10 @@ public class ComponentHashers { static { register(DataComponentTypes.CUSTOM_DATA, MinecraftHasher.NBT_MAP); - register(DataComponentTypes.MAX_STACK_SIZE); - register(DataComponentTypes.MAX_DAMAGE); - register(DataComponentTypes.DAMAGE); - register(DataComponentTypes.UNBREAKABLE); + registerInt(DataComponentTypes.MAX_STACK_SIZE); + registerInt(DataComponentTypes.MAX_DAMAGE); + registerInt(DataComponentTypes.DAMAGE); + registerUnit(DataComponentTypes.UNBREAKABLE); // TODO custom name, component // TODO item name, component @@ -97,7 +97,7 @@ public class ComponentHashers { .optional("hide_tooltip", MinecraftHasher.BOOL, TooltipDisplay::hideTooltip, false) .optionalList("hidden_components", RegistryHasher.DATA_COMPONENT_TYPE, TooltipDisplay::hiddenComponents)); - register(DataComponentTypes.REPAIR_COST); + registerInt(DataComponentTypes.REPAIR_COST); register(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, MinecraftHasher.BOOL); //register(DataComponentTypes.INTANGIBLE_PROJECTILE); // TODO MCPL is wrong @@ -142,16 +142,16 @@ public class ComponentHashers { registerMap(DataComponentTypes.REPAIRABLE, builder -> builder .accept("items", RegistryHasher.ITEM.holderSet(), Function.identity())); - register(DataComponentTypes.GLIDER); + registerUnit(DataComponentTypes.GLIDER); register(DataComponentTypes.TOOLTIP_STYLE, MinecraftHasher.KEY); registerMap(DataComponentTypes.DEATH_PROTECTION, builder -> builder); // TODO consume effect needs identifier in MCPL registerMap(DataComponentTypes.BLOCKS_ATTACKS, builder -> builder); // TODO needs damage types, add a way to cache identifiers without reading objects in registrycache register(DataComponentTypes.STORED_ENCHANTMENTS, MinecraftHasher.map(RegistryHasher.ENCHANTMENT, MinecraftHasher.INT).convert(ItemEnchantments::getEnchantments)); // TODO duplicate code? - register(DataComponentTypes.DYED_COLOR); - register(DataComponentTypes.MAP_COLOR); - register(DataComponentTypes.MAP_ID); + registerInt(DataComponentTypes.DYED_COLOR); + registerInt(DataComponentTypes.MAP_COLOR); + registerInt(DataComponentTypes.MAP_ID); register(DataComponentTypes.MAP_DECORATIONS, MinecraftHasher.NBT_MAP); // TODO charged projectiles also need the recursionâ„¢ @@ -166,11 +166,11 @@ public class ComponentHashers { register(DataComponentTypes.POTION_DURATION_SCALE, MinecraftHasher.FLOAT); } - private static void register(DataComponentType component) { + private static void registerUnit(DataComponentType component) { register(component, MinecraftHasher.UNIT); } - private static void register(IntComponentType component) { + private static void registerInt(IntComponentType component) { register(component, MinecraftHasher.INT); } @@ -193,6 +193,7 @@ public class ComponentHashers { return hasher.hash(value, new MinecraftHashEncoder(session)); } + // TODO better hashing, at the moment this is just called when the player is spawned public static void testHashing(GeyserSession session) { // Hashed values generated by vanilla Java diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java index b675a236e..0c84a2b6f 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java @@ -80,7 +80,7 @@ public interface MinecraftHasher { .optionalNullable("speed", MinecraftHasher.FLOAT, ToolData.Rule::getSpeed) .optionalNullable("correct_for_drops", MinecraftHasher.BOOL, ToolData.Rule::getCorrectForDrops)); - MinecraftHasher EQUIPMENT_SLOT = fromEnum(); + MinecraftHasher EQUIPMENT_SLOT = fromEnum(); // FIXME MCPL enum constants aren't right MinecraftHasher MOB_EFFECT_INSTANCE = mapBuilder(builder -> builder .accept("id", RegistryHasher.EFFECT, MobEffectInstance::getEffect) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java index 1a247f609..ef3168f41 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/DataComponentRegistryPopulator.java @@ -75,13 +75,7 @@ public final class DataComponentRegistryPopulator { byte[] bytes = Base64.getDecoder().decode(encodedValue); ByteBuf buf = Unpooled.wrappedBuffer(bytes); int varInt = MinecraftTypes.readVarInt(buf); - //System.out.println("int: " + varInt + " " + componentEntry.getKey()); DataComponentType dataComponentType = DataComponentTypes.from(varInt); - if (dataComponentType == DataComponentTypes.ENCHANTMENTS - || dataComponentType == DataComponentTypes.STORED_ENCHANTMENTS - || dataComponentType == DataComponentTypes.JUKEBOX_PLAYABLE) { - continue; // TODO Broken reading in MCPL - } DataComponent dataComponent = dataComponentType.readDataComponent(buf); map.put(dataComponentType, dataComponent); @@ -90,7 +84,6 @@ public final class DataComponentRegistryPopulator { defaultComponents.add(new DataComponents(ImmutableMap.copyOf(map))); } } catch (Exception e) { - // TODO 1.21.5 enchantment reading is broken throw new AssertionError("Unable to load or parse components", e); } From e78b6b431edd873a7d41ea78c5faaa954d4ce492 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Tue, 25 Mar 2025 20:49:33 +0000 Subject: [PATCH 31/86] Fix test --- .../geyser/scoreboard/network/ScoreboardIssueTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java index 8311f1a5b..21424dcf3 100644 --- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java +++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java @@ -209,7 +209,7 @@ public class ScoreboardIssueTests { // metadata set: invisible, custom name, custom name visible context.translate(setEntityDataTranslator, new ClientboundSetEntityDataPacket(1298, new EntityMetadata[]{ new ByteEntityMetadata(0, MetadataTypes.BYTE, (byte) 0x20), - new ObjectEntityMetadata<>(2, MetadataTypes.OPTIONAL_CHAT, Optional.of(Component.text("tesss"))), + new ObjectEntityMetadata<>(2, MetadataTypes.OPTIONAL_COMPONENT, Optional.of(Component.text("tesss"))), new BooleanEntityMetadata(3, MetadataTypes.BOOLEAN, true) })); From ba9544713a51f9e0294bcaaf45acf88093e67180 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Tue, 25 Mar 2025 21:29:28 +0000 Subject: [PATCH 32/86] Item stack, use remainder component hashers, update mappings --- .../geyser/item/hashing/ComponentHashers.java | 25 ++++++++++-- .../geyser/item/hashing/MinecraftHasher.java | 40 ++++++++++--------- .../geyser/item/hashing/RegistryHasher.java | 35 +++++++++++++++- core/src/main/resources/mappings | 2 +- 4 files changed, 77 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java index 3bcd6aa38..6d441bdb8 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHashers.java @@ -38,10 +38,12 @@ import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.FoodProperties; import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; @@ -112,7 +114,7 @@ public class ComponentHashers { .optional("sound", RegistryHasher.SOUND_EVENT, Consumable::sound, BuiltinSound.ENTITY_GENERIC_EAT) .optional("has_consume_particles", MinecraftHasher.BOOL, Consumable::hasConsumeParticles, true)); // TODO consume effect needs identifier in MCPL - // TODO use remainder needs item stack codec, recursion go brr + register(DataComponentTypes.USE_REMAINDER, RegistryHasher.ITEM_STACK); registerMap(DataComponentTypes.USE_COOLDOWN, builder -> builder .accept("seconds", MinecraftHasher.FLOAT, UseCooldown::seconds) @@ -120,7 +122,7 @@ public class ComponentHashers { registerMap(DataComponentTypes.DAMAGE_RESISTANT, builder -> builder .accept("types", MinecraftHasher.TAG, Function.identity())); registerMap(DataComponentTypes.TOOL, builder -> builder - .acceptList("rules", MinecraftHasher.TOOL_RULE, ToolData::getRules) + .acceptList("rules", RegistryHasher.TOOL_RULE, ToolData::getRules) .optional("default_mining_speed", MinecraftHasher.FLOAT, ToolData::getDefaultMiningSpeed, 1.0F) .optional("damage_per_block", MinecraftHasher.INT, ToolData::getDamagePerBlock, 1) .optional("can_destroy_blocks_in_creative", MinecraftHasher.BOOL, ToolData::isCanDestroyBlocksInCreative, true)); @@ -160,7 +162,7 @@ public class ComponentHashers { registerMap(DataComponentTypes.POTION_CONTENTS, builder -> builder .optional("potion", RegistryHasher.POTION, PotionContents::getPotionId, -1) .optional("custom_color", MinecraftHasher.INT, PotionContents::getCustomColor, -1) - .optionalList("custom_effects", MinecraftHasher.MOB_EFFECT_INSTANCE, PotionContents::getCustomEffects) + .optionalList("custom_effects", RegistryHasher.MOB_EFFECT_INSTANCE, PotionContents::getCustomEffects) .optionalNullable("custom_name", MinecraftHasher.STRING, PotionContents::getCustomName)); register(DataComponentTypes.POTION_DURATION_SCALE, MinecraftHasher.FLOAT); @@ -185,10 +187,18 @@ public class ComponentHashers { hashers.put(component, hasher); } + public static MinecraftHasher hasherOrEmpty(DataComponentType component) { + MinecraftHasher hasher = (MinecraftHasher) hashers.get(component); + if (hasher == null) { + return MinecraftHasher.UNIT.convert(value -> Unit.INSTANCE); + } + return hasher; + } + public static HashCode hash(GeyserSession session, DataComponentType component, T value) { MinecraftHasher hasher = (MinecraftHasher) hashers.get(component); if (hasher == null) { - throw new IllegalStateException("Unregistered hasher for component " + component + "!"); + throw new IllegalStateException("Unregistered hasher for component " + component + "!"); // TODO we might not have hashers for every component, in which case, fix this } return hasher.hash(value, new MinecraftHashEncoder(session)); } @@ -241,6 +251,13 @@ public class ComponentHashers { testHash(session, DataComponentTypes.FOOD, new FoodProperties(3, 5.7F, true), 1917653498); testHash(session, DataComponentTypes.FOOD, new FoodProperties(7, 0.15F, false), -184166204); + testHash(session, DataComponentTypes.USE_REMAINDER, new ItemStack(Items.MELON.javaId(), 52), -1279684916); + + DataComponents specialComponents = new DataComponents(new HashMap<>()); + specialComponents.put(DataComponentTypes.ITEM_MODEL, MinecraftKey.key("testing")); + specialComponents.put(DataComponentTypes.MAX_STACK_SIZE, 44); + testHash(session, DataComponentTypes.USE_REMAINDER, new ItemStack(Items.PUMPKIN.javaId(), 32, specialComponents), 1991032843); + testHash(session, DataComponentTypes.DAMAGE_RESISTANT, MinecraftKey.key("testing"), -1230493835); testHash(session, DataComponentTypes.TOOL, new ToolData(List.of(), 5.0F, 3, false), -1789071928); diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java index 0c84a2b6f..cfab00504 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.item.hashing; +import com.google.common.base.Suppliers; import com.google.common.hash.HashCode; import net.kyori.adventure.key.Key; import org.cloudburstmc.nbt.NbtMap; @@ -32,14 +33,13 @@ import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectInstance; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Unit; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @@ -75,21 +75,8 @@ public interface MinecraftHasher { MinecraftHasher ITEM_USE_ANIMATION = fromEnum(); - MinecraftHasher TOOL_RULE = mapBuilder(builder -> builder - .accept("blocks", RegistryHasher.BLOCK.holderSet(), ToolData.Rule::getBlocks) - .optionalNullable("speed", MinecraftHasher.FLOAT, ToolData.Rule::getSpeed) - .optionalNullable("correct_for_drops", MinecraftHasher.BOOL, ToolData.Rule::getCorrectForDrops)); - MinecraftHasher EQUIPMENT_SLOT = fromEnum(); // FIXME MCPL enum constants aren't right - MinecraftHasher MOB_EFFECT_INSTANCE = mapBuilder(builder -> builder - .accept("id", RegistryHasher.EFFECT, MobEffectInstance::getEffect) - .optional("amplifier", BYTE, instance -> (byte) instance.getDetails().getAmplifier(), (byte) 0) - .optional("duration", INT, instance -> instance.getDetails().getDuration(), 0) - .optional("ambient", BOOL, instance -> instance.getDetails().isAmbient(), false) - .optional("show_particles", BOOL, instance -> instance.getDetails().isShowParticles(), true) - .accept("show_icon", BOOL, instance -> instance.getDetails().isShowIcon())); // TODO check this, also hidden effect but is recursive - HashCode hash(T value, MinecraftHashEncoder encoder); default MinecraftHasher> list() { @@ -97,11 +84,15 @@ public interface MinecraftHasher { } default MinecraftHasher convert(Function converter) { - return (object, encoder) -> hash(converter.apply(object), encoder); + return (value, encoder) -> hash(converter.apply(value), encoder); } default MinecraftHasher sessionConvert(BiFunction converter) { - return (object, encoder) -> hash(converter.apply(encoder.session(), object), encoder); + return (value, encoder) -> hash(converter.apply(encoder.session(), value), encoder); + } + + static MinecraftHasher recursive(UnaryOperator> delegate) { + return new Recursive<>(delegate); } static > MinecraftHasher fromIdEnum(T[] values, Function toName) { @@ -114,7 +105,7 @@ public interface MinecraftHasher { } static MinecraftHasher mapBuilder(UnaryOperator> builder) { - return (object, encoder) -> builder.apply(new MapHasher<>(object, encoder)).build(); + return (value, encoder) -> builder.apply(new MapHasher<>(value, encoder)).build(); } static MinecraftHasher> map(MinecraftHasher keyHasher, MinecraftHasher valueHasher) { @@ -122,4 +113,17 @@ public interface MinecraftHasher { .map(entry -> Map.entry(keyHasher.hash(entry.getKey(), encoder), valueHasher.hash(entry.getValue(), encoder))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); } + + class Recursive implements MinecraftHasher { + private final Supplier> delegate; + + public Recursive(UnaryOperator> delegate) { + this.delegate = Suppliers.memoize(() -> delegate.apply(this)); + } + + @Override + public HashCode hash(T value, MinecraftHashEncoder encoder) { + return delegate.get().hash(value, encoder); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java index c30805952..2b09c50e3 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/RegistryHasher.java @@ -31,8 +31,13 @@ import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponent; 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.MobEffectInstance; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ToolData; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.CustomSound; import org.geysermc.mcprotocollib.protocol.data.game.level.sound.Sound; @@ -49,11 +54,32 @@ public interface RegistryHasher extends MinecraftHasher { RegistryHasher ENCHANTMENT = registry(JavaRegistries.ENCHANTMENT); + MinecraftHasher> DATA_COMPONENT_TYPE = KEY.convert(DataComponentType::getKey); + + @SuppressWarnings({"unchecked", "rawtypes"}) // Java generics :( + MinecraftHasher> DATA_COMPONENT = (component, encoder) -> { + MinecraftHasher hasher = ComponentHashers.hasherOrEmpty(component.getType()); + return hasher.hash(component.getValue(), encoder); + }; + + MinecraftHasher DATA_COMPONENTS = MinecraftHasher.map(RegistryHasher.DATA_COMPONENT_TYPE, DATA_COMPONENT).convert(DataComponents::getDataComponents); // TODO component removals (needs unit value and ! component prefix) + + MinecraftHasher ITEM_STACK = MinecraftHasher.mapBuilder(builder -> builder + .accept("id", ITEM, ItemStack::getId) + .accept("count", INT, ItemStack::getAmount) + .optionalNullable("components", DATA_COMPONENTS, ItemStack::getDataComponentsPatch)); + MinecraftHasher EFFECT = enumRegistry(); - RegistryHasher POTION = enumIdRegistry(Potion.values()); + MinecraftHasher MOB_EFFECT_INSTANCE = MinecraftHasher.mapBuilder(builder -> builder + .accept("id", RegistryHasher.EFFECT, MobEffectInstance::getEffect) + .optional("amplifier", BYTE, instance -> (byte) instance.getDetails().getAmplifier(), (byte) 0) + .optional("duration", INT, instance -> instance.getDetails().getDuration(), 0) + .optional("ambient", BOOL, instance -> instance.getDetails().isAmbient(), false) + .optional("show_particles", BOOL, instance -> instance.getDetails().isShowParticles(), true) + .accept("show_icon", BOOL, instance -> instance.getDetails().isShowIcon())); // TODO check this, also hidden effect but is recursive - MinecraftHasher> DATA_COMPONENT_TYPE = KEY.convert(DataComponentType::getKey); + RegistryHasher POTION = enumIdRegistry(Potion.values()); MinecraftHasher BUILTIN_SOUND = KEY.convert(sound -> MinecraftKey.key(sound.getName())); @@ -68,6 +94,11 @@ public interface RegistryHasher extends MinecraftHasher { return CUSTOM_SOUND.hash((CustomSound) sound, encoder); }; + MinecraftHasher TOOL_RULE = MinecraftHasher.mapBuilder(builder -> builder + .accept("blocks", RegistryHasher.BLOCK.holderSet(), ToolData.Rule::getBlocks) + .optionalNullable("speed", MinecraftHasher.FLOAT, ToolData.Rule::getSpeed) + .optionalNullable("correct_for_drops", MinecraftHasher.BOOL, ToolData.Rule::getCorrectForDrops)); + static RegistryHasher registry(JavaRegistryKey registry) { MinecraftHasher hasher = KEY.sessionConvert(registry::keyFromNetworkId); return hasher::hash; diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 7654aabd9..a3a2a30fe 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 7654aabd95a06f02c3827fc212b819fea63d72bb +Subproject commit a3a2a30fe9b102bedf9097b5532176550bf3fd75 From 8cfe776363559cb3fcf495c894c88413b3132da1 Mon Sep 17 00:00:00 2001 From: Eclipse Date: Wed, 26 Mar 2025 08:38:43 +0000 Subject: [PATCH 33/86] Some more components, still needs their tests, work on text component hashing --- .../geyser/item/hashing/ComponentHasher.java | 102 ++++++++++++++++++ ...Hashers.java => DataComponentHashers.java} | 22 ++-- .../geyser/item/hashing/MapBuilder.java | 32 ++++++ .../geyser/item/hashing/MapHasher.java | 12 ++- .../geyser/item/hashing/MinecraftHasher.java | 23 +++- .../geyser/item/hashing/RegistryHasher.java | 32 +++++- .../geyser/session/cache/RegistryCache.java | 3 +- .../cache/registry/JavaRegistries.java | 2 + .../player/JavaPlayerPositionTranslator.java | 4 +- 9 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java rename core/src/main/java/org/geysermc/geyser/item/hashing/{ComponentHashers.java => DataComponentHashers.java} (94%) create mode 100644 core/src/main/java/org/geysermc/geyser/item/hashing/MapBuilder.java diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java new file mode 100644 index 000000000..7b2415b14 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.item.hashing; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +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; + +public interface ComponentHasher { + + MinecraftHasher COMPONENT = (value, encoder) -> encoder.empty(); // TODO + + MinecraftHasher NAMED_COLOR = MinecraftHasher.STRING.convert(NamedTextColor::toString); + + MinecraftHasher DIRECT_COLOR = MinecraftHasher.STRING.convert(TextColor::asHexString); + + MinecraftHasher COLOR = (value, encoder) -> { + if (value instanceof NamedTextColor named) { + return NAMED_COLOR.hash(named, encoder); + } + return DIRECT_COLOR.hash(value, encoder); + }; + + MinecraftHasher DECORATION_STATE = MinecraftHasher.BOOL.convert(state -> switch (state) { + case NOT_SET -> null; // Should never happen since we're using .optional() with NOT_SET as default value below + case FALSE -> false; + case TRUE -> true; + }); + + MinecraftHasher CLICK_EVENT_ACTION = MinecraftHasher.STRING.convert(ClickEvent.Action::toString); + + MinecraftHasher CLICK_EVENT = CLICK_EVENT_ACTION.dispatch("action", ClickEvent::action, action -> switch (action) { + case OPEN_URL -> builder -> builder.accept("url", MinecraftHasher.STRING, ClickEvent::value); + case OPEN_FILE -> builder -> builder.accept("path", MinecraftHasher.STRING, ClickEvent::value); + case RUN_COMMAND, SUGGEST_COMMAND -> builder -> builder.accept("command", MinecraftHasher.STRING, ClickEvent::value); + case CHANGE_PAGE -> builder -> builder.accept("page", MinecraftHasher.STRING, ClickEvent::value); + case COPY_TO_CLIPBOARD -> builder -> builder.accept("value", MinecraftHasher.STRING, ClickEvent::value); + }); + + MinecraftHasher> HOVER_EVENT_ACTION = MinecraftHasher.STRING.convert(HoverEvent.Action::toString); + + MinecraftHasher> HOVER_EVENT = HOVER_EVENT_ACTION.dispatch("action", HoverEvent::action, action -> { + if (action == HoverEvent.Action.SHOW_TEXT) { + + } else if (action == HoverEvent.Action.SHOW_ITEM) { + + } + return builder -> builder; + }); + + // TODO shadow colours - needs kyori bump + MinecraftHasher