diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 6a40d5efaf2..796cf705f0f 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -252,6 +252,7 @@ public void initialize() { return; } + MinecraftLocale.downloadDeprecations(); MinecraftLocale.ensureEN_US(); String locale = GeyserLocale.getDefaultLocale(); if (!"en_us".equals(locale)) { 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 8638267c899..dc7b2aba446 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 @@ -58,7 +58,7 @@ public void setPose(Pose pose) { } @Override - public JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.FROG_VARIANT; } 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 c474805631f..843c5e2abb7 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 @@ -64,7 +64,7 @@ default void setVariantFromJavaId(int variant) { * 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, Enum)} method * to create a reader to be used in {@link org.geysermc.geyser.session.cache.RegistryCache}. */ - JavaRegistryKey variantRegistry(); + JavaRegistryKey variantRegistry(); /** * Should set the variant for bedrock. 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 4ee7175dea2..45723886257 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 @@ -50,7 +50,7 @@ protected Tag getFoodTag() { } @Override - public 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 de79e9a5222..58f272775fc 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 @@ -79,7 +79,7 @@ protected Tag getFoodTag() { } @Override - public 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 7ae672bcc48..cfa2e39916f 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 @@ -152,7 +152,7 @@ public boolean isClientControlled() { } @Override - public JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.PIG_VARIANT; } 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 e90cb3f1c61..8b00afcdc24 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 @@ -85,7 +85,7 @@ public void setTameableFlags(ByteEntityMetadata entityMetadata) { } @Override - public JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.CAT_VARIANT; } 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 46911480e60..c8ffa2b5c68 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 @@ -113,7 +113,7 @@ public void setWolfAngerTime(IntEntityMetadata entityMetadata) { } @Override - public JavaRegistryKey variantRegistry() { + public JavaRegistryKey variantRegistry() { return JavaRegistries.WOLF_VARIANT; } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java index 5f4ce6b452d..1d8ee973724 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java @@ -30,6 +30,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -45,6 +46,7 @@ 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.WrittenBookContent; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay; @@ -173,6 +175,10 @@ public T getComponentElseGet(@NonNull DataComponentType type, Supplier return value == null ? supplier.get() : value; } + public boolean hasComponent(@NonNull DataComponentType type) { + return getComponent(type) != null; + } + public int getNetId() { return isEmpty() ? 0 : netId; } @@ -248,6 +254,17 @@ public SlotDisplay asSlotDisplay() { return new ItemStackSlotDisplay(this.getItemStack()); } + public Component getName() { + return getComponentElseGet(DataComponentTypes.CUSTOM_NAME, () -> { + WrittenBookContent book = getComponent(DataComponentTypes.WRITTEN_BOOK_CONTENT); + if (book != null) { + return Component.text(book.getTitle().getRaw()); + } + + return asItem().getName(this); + }); + } + public int getMaxDamage() { return getComponentElseGet(DataComponentTypes.MAX_DAMAGE, () -> 0); } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java b/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java index 33935e19037..c559526d20e 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/BannerPattern.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * 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 @@ -25,84 +25,36 @@ package org.geysermc.geyser.inventory.item; -import lombok.Getter; -import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; -import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; -import java.util.Locale; +public record BannerPattern(BedrockBannerPattern bedrockPattern, String translationKey) { -@Getter -public enum BannerPattern { - BASE("b"), - SQUARE_BOTTOM_LEFT("bl"), - SQUARE_BOTTOM_RIGHT("br"), - SQUARE_TOP_LEFT("tl"), - SQUARE_TOP_RIGHT("tr"), - STRIPE_BOTTOM("bs"), - STRIPE_TOP("ts"), - STRIPE_LEFT("ls"), - STRIPE_RIGHT("rs"), - STRIPE_CENTER("cs"), - STRIPE_MIDDLE("ms"), - STRIPE_DOWNRIGHT("drs"), - STRIPE_DOWNLEFT("dls"), - SMALL_STRIPES("ss"), - CROSS("cr"), - STRAIGHT_CROSS("sc"), - TRIANGLE_BOTTOM("bt"), - TRIANGLE_TOP("tt"), - TRIANGLES_BOTTOM("bts"), - TRIANGLES_TOP("tts"), - DIAGONAL_LEFT("ld"), - DIAGONAL_UP_RIGHT("rd"), - DIAGONAL_UP_LEFT("lud"), - DIAGONAL_RIGHT("rud"), - CIRCLE("mc"), - RHOMBUS("mr"), - HALF_VERTICAL("vh"), - HALF_HORIZONTAL("hh"), - HALF_VERTICAL_RIGHT("vhr"), - HALF_HORIZONTAL_BOTTOM("hhb"), - BORDER("bo"), - CURLY_BORDER("cbo"), - GRADIENT("gra"), - GRADIENT_UP("gru"), - BRICKS("bri"), - GLOBE("glb"), - CREEPER("cre"), - SKULL("sku"), - FLOWER("flo"), - MOJANG("moj"), - PIGLIN("pig"), - FLOW("flw"), - GUSTER("gus"); - - private static final BannerPattern[] VALUES = values(); - - private final Key javaIdentifier; - private final String bedrockIdentifier; - - BannerPattern(String bedrockIdentifier) { - this.javaIdentifier = MinecraftKey.key(this.name().toLowerCase(Locale.ROOT)); - this.bedrockIdentifier = bedrockIdentifier; + public static BannerPattern read(RegistryEntryContext context) { + String translationKey = context.data().getString("translation_key"); + // getByJavaIdentifier defaults to BASE + return new BannerPattern(BedrockBannerPattern.getByJavaIdentifier(context.id()), translationKey); } - public static BannerPattern getByJavaIdentifier(Key key) { - for (BannerPattern bannerPattern : VALUES) { - if (bannerPattern.javaIdentifier.equals(key)) { - return bannerPattern; - } + /** + * @return the corresponding registered {@link BannerPattern} for the given {@link BedrockBannerPattern}, or null if none exists + */ + public static BannerPattern fromBedrockPattern(GeyserSession session, @Nullable BedrockBannerPattern bedrockPattern) { + if (bedrockPattern == null) { + return null; } - return BASE; // Default fallback - } - public static @Nullable BannerPattern getByBedrockIdentifier(String bedrockIdentifier) { - for (BannerPattern bannerPattern : VALUES) { - if (bannerPattern.bedrockIdentifier.equals(bedrockIdentifier)) { - return bannerPattern; + for (BannerPattern javaPattern : session.getRegistryCache().registry(JavaRegistries.BANNER_PATTERN).values()) { + if (javaPattern.bedrockPattern == bedrockPattern) { + return javaPattern; } } return null; } + + public static int findNetworkId(GeyserSession session, BedrockBannerPattern bedrockPattern) { + return JavaRegistries.BANNER_PATTERN.networkId(session, fromBedrockPattern(session, bedrockPattern)); + } } diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/BedrockBannerPattern.java b/core/src/main/java/org/geysermc/geyser/inventory/item/BedrockBannerPattern.java new file mode 100644 index 00000000000..311902ead48 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/BedrockBannerPattern.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory.item; + +import lombok.Getter; +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.util.MinecraftKey; + +import java.util.Locale; + +@Getter +public enum BedrockBannerPattern { + BASE("b"), + SQUARE_BOTTOM_LEFT("bl"), + SQUARE_BOTTOM_RIGHT("br"), + SQUARE_TOP_LEFT("tl"), + SQUARE_TOP_RIGHT("tr"), + STRIPE_BOTTOM("bs"), + STRIPE_TOP("ts"), + STRIPE_LEFT("ls"), + STRIPE_RIGHT("rs"), + STRIPE_CENTER("cs"), + STRIPE_MIDDLE("ms"), + STRIPE_DOWNRIGHT("drs"), + STRIPE_DOWNLEFT("dls"), + SMALL_STRIPES("ss"), + CROSS("cr"), + STRAIGHT_CROSS("sc"), + TRIANGLE_BOTTOM("bt"), + TRIANGLE_TOP("tt"), + TRIANGLES_BOTTOM("bts"), + TRIANGLES_TOP("tts"), + DIAGONAL_LEFT("ld"), + DIAGONAL_UP_RIGHT("rd"), + DIAGONAL_UP_LEFT("lud"), + DIAGONAL_RIGHT("rud"), + CIRCLE("mc"), + RHOMBUS("mr"), + HALF_VERTICAL("vh"), + HALF_HORIZONTAL("hh"), + HALF_VERTICAL_RIGHT("vhr"), + HALF_HORIZONTAL_BOTTOM("hhb"), + BORDER("bo"), + CURLY_BORDER("cbo"), + GRADIENT("gra"), + GRADIENT_UP("gru"), + BRICKS("bri"), + GLOBE("glb"), + CREEPER("cre"), + SKULL("sku"), + FLOWER("flo"), + MOJANG("moj"), + PIGLIN("pig"), + FLOW("flw"), + GUSTER("gus"); + + private static final BedrockBannerPattern[] VALUES = values(); + + private final Key javaIdentifier; + private final String bedrockIdentifier; + + BedrockBannerPattern(String bedrockIdentifier) { + this.javaIdentifier = MinecraftKey.key(this.name().toLowerCase(Locale.ROOT)); + this.bedrockIdentifier = bedrockIdentifier; + } + + public static BedrockBannerPattern getByJavaIdentifier(Key key) { + for (BedrockBannerPattern bannerPattern : VALUES) { + if (bannerPattern.javaIdentifier.equals(key)) { + return bannerPattern; + } + } + return BASE; // Default fallback + } + + public static @Nullable BedrockBannerPattern getByBedrockIdentifier(String bedrockIdentifier) { + for (BedrockBannerPattern bannerPattern : VALUES) { + if (bannerPattern.bedrockIdentifier.equals(bedrockIdentifier)) { + return bannerPattern; + } + } + return null; + } +} 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 dc4a2030d95..16ee1dd4a1c 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 @@ -26,6 +26,7 @@ package org.geysermc.geyser.inventory.item; import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.GeyserSession; @@ -40,32 +41,21 @@ import java.util.Locale; -public interface GeyserInstrument { +public record GeyserInstrument(String soundEvent, float range, Component description, @Nullable BedrockInstrument bedrockInstrument) { - static GeyserInstrument read(RegistryEntryContext context) { + public static GeyserInstrument read(RegistryEntryContext context) { NbtMap data = context.data(); String soundEvent = SoundUtils.readSoundEvent(data, "instrument " + context.id()); float range = data.getFloat("range"); - String description = MessageTranslator.deserializeDescriptionForTooltip(context.session(), data); + Component description = MessageTranslator.componentFromNbtTag(data.get("description")); BedrockInstrument bedrockInstrument = BedrockInstrument.getByJavaIdentifier(context.id()); - return new GeyserInstrument.Impl(soundEvent, range, description, bedrockInstrument); + return new GeyserInstrument(soundEvent, range, description, bedrockInstrument); } - String soundEvent(); - - float range(); - - /** - * In Bedrock format - */ - String description(); - - BedrockInstrument bedrockInstrument(); - /** * @return the ID of the Bedrock counterpart for this instrument. If there is none ({@link #bedrockInstrument()} is null), then -1 is returned. */ - default int bedrockId() { + public int bedrockId() { BedrockInstrument bedrockInstrument = bedrockInstrument(); if (bedrockInstrument != null) { return bedrockInstrument.ordinal(); @@ -76,13 +66,13 @@ default int bedrockId() { /** * @return the ID of the Java counterpart for the given Bedrock ID. If an invalid Bedrock ID was given, or there is no counterpart, -1 is returned. */ - static int bedrockIdToJava(GeyserSession session, int id) { + public static int bedrockIdToJava(GeyserSession session, int id) { JavaRegistry instruments = session.getRegistryCache().registry(JavaRegistries.INSTRUMENT); BedrockInstrument bedrockInstrument = BedrockInstrument.getByBedrockId(id); if (bedrockInstrument != null) { for (int i = 0; i < instruments.values().size(); i++) { GeyserInstrument instrument = instruments.byId(i); - if (instrument.bedrockInstrument() == bedrockInstrument) { + if (instrument != null && instrument.bedrockInstrument() == bedrockInstrument) { return i; } } @@ -91,52 +81,27 @@ static int bedrockIdToJava(GeyserSession session, int id) { } // TODO test in 1.21.5 - static GeyserInstrument fromComponent(GeyserSession session, InstrumentComponent component) { + public static GeyserInstrument fromComponent(GeyserSession session, InstrumentComponent component) { if (component.instrumentLocation() != null) { - return session.getRegistryCache().registry(JavaRegistries.INSTRUMENT).byKey(component.instrumentLocation()); + return JavaRegistries.INSTRUMENT.value(session, component.instrumentLocation()); } else if (component.instrumentHolder() != null) { - if (component.instrumentHolder().isId()) { - return session.getRegistryCache().registry(JavaRegistries.INSTRUMENT).byId(component.instrumentHolder().id()); - } - InstrumentComponent.Instrument custom = component.instrumentHolder().custom(); - return new Wrapper(custom, session.locale()); + return JavaRegistries.INSTRUMENT.value(session, component.instrumentHolder()); } throw new IllegalStateException("InstrumentComponent must have either a location or a holder"); } - record Wrapper(InstrumentComponent.Instrument instrument, String locale) implements GeyserInstrument { - @Override - public String soundEvent() { - return instrument.soundEvent().getName(); - } - - @Override - public float range() { - return instrument.range(); + public static GeyserInstrument fromInstrument(InstrumentComponent.Instrument instrument) { + BedrockInstrument bedrock = null; + if (instrument.soundEvent() instanceof BuiltinSound) { + bedrock = BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.soundEvent().getName())); } - - @Override - public String description() { - return MessageTranslator.convertMessageForTooltip(instrument.description(), locale); - } - - @Override - public BedrockInstrument bedrockInstrument() { - if (instrument.soundEvent() instanceof BuiltinSound) { - return BedrockInstrument.getByJavaIdentifier(MinecraftKey.key(instrument.soundEvent().getName())); - } - // Probably custom - return null; - } - } - - record Impl(String soundEvent, float range, String description, @Nullable BedrockInstrument bedrockInstrument) implements GeyserInstrument { + return new GeyserInstrument(instrument.soundEvent().getName(), instrument.range(), instrument.description(), bedrock); } /** * Each vanilla instrument on Bedrock, ordered in their network IDs. */ - enum BedrockInstrument { + public enum BedrockInstrument { PONDER, SING, SEEK, diff --git a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java b/core/src/main/java/org/geysermc/geyser/item/components/FireworkExplosionShape.java similarity index 82% rename from core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java rename to core/src/main/java/org/geysermc/geyser/item/components/FireworkExplosionShape.java index 316253e3e29..86d4235e1be 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/data/FireworkExplosionShape.java +++ b/core/src/main/java/org/geysermc/geyser/item/components/FireworkExplosionShape.java @@ -23,7 +23,11 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.item.hashing.data; +package org.geysermc.geyser.item.components; + +import net.kyori.adventure.text.Component; + +import java.util.Locale; // Ordered and named by Java ID public enum FireworkExplosionShape { @@ -31,5 +35,9 @@ public enum FireworkExplosionShape { LARGE_BALL, STAR, CREEPER, - BURST + BURST; + + public Component displayName() { + return Component.translatable("item.minecraft.firework_star.shape." + name().toLowerCase(Locale.ROOT)); + } } diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java index 8ad58c9c3b0..d98aebf1fb1 100644 --- a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java +++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.item.enchantment; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.inventory.item.BedrockEnchantment; @@ -39,13 +40,12 @@ import java.util.Set; /** - * @param description only populated if {@link #bedrockEnchantment()} is null. * @param anvilCost also as a rarity multiplier */ public record Enchantment(Set effects, GeyserHolderSet supportedItems, int maxLevel, - String description, + Component description, int anvilCost, GeyserHolderSet exclusiveSet, @Nullable BedrockEnchantment bedrockEnchantment) { @@ -63,7 +63,7 @@ public static Enchantment read(RegistryEntryContext context) { BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(context.id().asString()); - String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(context.session(), data) : null; + Component description = MessageTranslator.componentFromNbtTag(data.get("description")); return new Enchantment(effects, supportedItems, maxLevel, description, anvilCost, exclusiveSet, bedrockEnchantment); } 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 cd72874e548..6e516c7be31 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 @@ -30,7 +30,7 @@ import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.item.hashing.data.ConsumeEffectType; -import org.geysermc.geyser.item.hashing.data.FireworkExplosionShape; +import org.geysermc.geyser.item.components.FireworkExplosionShape; import org.geysermc.geyser.item.hashing.data.ItemContainerSlot; import org.geysermc.geyser.item.hashing.data.entity.AxolotlVariant; import org.geysermc.geyser.item.hashing.data.entity.FoxVariant; @@ -355,7 +355,7 @@ public interface RegistryHasher extends MinecraftHasher { * * @param registry the registry to create a hasher for. */ - static RegistryHasher registry(JavaRegistryKey registry) { + static RegistryHasher registry(JavaRegistryKey registry) { MinecraftHasher hasher = KEY.sessionCast(registry::key); return hasher::hash; } @@ -371,7 +371,7 @@ static RegistryHasher registry(JavaRegistryKey registry) { * @see RegistryHasher#holder() */ // We don't use the registry generic type, because various registries don't use the MCPL type as their type - static RegistryHasher registry(JavaRegistryKey registry, MinecraftHasher directHasher) { + static RegistryHasher registry(JavaRegistryKey registry, MinecraftHasher directHasher) { return new RegistryHasherWithDirectHasher<>(registry(registry), directHasher); } diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/ComponentTooltipProvider.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/ComponentTooltipProvider.java new file mode 100644 index 00000000000..360a8f8afef --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/ComponentTooltipProvider.java @@ -0,0 +1,37 @@ +/* + * 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.tooltip; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Consumer; + +@FunctionalInterface +public interface ComponentTooltipProvider { + + void addTooltip(TooltipContext context, Consumer adder, @NonNull T component); +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/TooltipContext.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/TooltipContext.java new file mode 100644 index 00000000000..94e84d460d5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/TooltipContext.java @@ -0,0 +1,70 @@ +/* + * 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.tooltip; + +import net.kyori.adventure.key.Key; +import org.geysermc.geyser.item.TooltipOptions; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistryKey; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; + +import java.util.Optional; +import java.util.function.Consumer; + +public record TooltipContext(Optional session, boolean advanced, boolean creative, Item item, DataComponents components, + TooltipOptions options) { + + public void getRegistryEntry(JavaRegistryKey registry, Key key, Consumer consumer) { + session.flatMap(session -> Optional.ofNullable(registry.value(session, key))).ifPresent(consumer); + } + + public void getRegistryEntry(JavaRegistryKey registry, Holder holder, Consumer consumer) { + session.flatMap(session -> Optional.ofNullable(registry.value(session, holder))).ifPresent(consumer); + } + + public void withSession(Consumer consumer) { + session.ifPresent(consumer); + } + + public TooltipContext withItemComponents(Item item, DataComponents components) { + return new TooltipContext(session, advanced, creative, item, components, options); + } + + public TooltipContext withFlags(boolean advanced, boolean creative) { + return new TooltipContext(session, advanced, creative, item, components, options); + } + + public static TooltipContext create(GeyserSession session, Item item, DataComponents components) { + return new TooltipContext(Optional.of(session), session.isAdvancedTooltips(), false, item, components, TooltipOptions.fromComponents(components)); + } + + public static TooltipContext createForCreativeMenu(Item item) { + DataComponents components = item.gatherComponents(null); + return new TooltipContext(Optional.empty(), false, true, item, components, TooltipOptions.fromComponents(components)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/TooltipProviders.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/TooltipProviders.java new file mode 100644 index 00000000000..08b3cf6bb29 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/TooltipProviders.java @@ -0,0 +1,223 @@ +/* + * 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.tooltip; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.kyori.adventure.key.InvalidKeyException; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.item.tooltip.providers.ArmorTrimTooltip; +import org.geysermc.geyser.item.tooltip.providers.AttributeModifiersTooltip; +import org.geysermc.geyser.item.tooltip.providers.BannerPatternLayersTooltip; +import org.geysermc.geyser.item.tooltip.providers.BeesTooltip; +import org.geysermc.geyser.item.tooltip.providers.BlockStatePropertiesTooltip; +import org.geysermc.geyser.item.tooltip.providers.ChargedProjectilesTooltip; +import org.geysermc.geyser.item.tooltip.providers.ContainerContentsTooltip; +import org.geysermc.geyser.item.tooltip.providers.ContainerLootTooltip; +import org.geysermc.geyser.item.tooltip.providers.DyedItemColorTooltip; +import org.geysermc.geyser.item.tooltip.providers.FireworkExplosionTooltip; +import org.geysermc.geyser.item.tooltip.providers.FireworksTooltip; +import org.geysermc.geyser.item.tooltip.providers.InstrumentTooltip; +import org.geysermc.geyser.item.tooltip.providers.ItemEnchantmentsTooltip; +import org.geysermc.geyser.item.tooltip.providers.JukeboxPlayableTooltip; +import org.geysermc.geyser.item.tooltip.providers.LoreTooltip; +import org.geysermc.geyser.item.tooltip.providers.MapTooltip; +import org.geysermc.geyser.item.tooltip.providers.OminousBottleTooltip; +import org.geysermc.geyser.item.tooltip.providers.PotDecorationsTooltip; +import org.geysermc.geyser.item.tooltip.providers.PotionContentsTooltip; +import org.geysermc.geyser.item.tooltip.providers.SuspiciousStewTooltip; +import org.geysermc.geyser.item.tooltip.providers.TropicalFishPatternTooltip; +import org.geysermc.geyser.item.tooltip.providers.WrittenBookTooltip; +import org.geysermc.geyser.item.type.BlockItem; +import org.geysermc.geyser.item.type.SpawnEggItem; +import org.geysermc.geyser.registry.Registries; +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 java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +public class TooltipProviders { + private static final Component UNBREAKABLE = Component.translatable("item.unbreakable").color(NamedTextColor.BLUE); + + private static final Map, ComponentTooltipProvider> NAME_PROVIDERS = new Reference2ObjectOpenHashMap<>(); + private static final Map, ComponentTooltipProvider> PROVIDERS = new Reference2ObjectOpenHashMap<>(); + + private static final List DANGEROUS_NBT_WARNING = List.of( + Component.translatable("item.op_warning.line1").color(NamedTextColor.RED).decorate(TextDecoration.BOLD), + Component.translatable("item.op_warning.line2").color(NamedTextColor.RED), + Component.translatable("item.op_warning.line3").color(NamedTextColor.RED) + ); + + private static void register(DataComponentType component, ComponentTooltipProvider provider) { + internalRegister(PROVIDERS, component, provider); + } + + private static void registerName(DataComponentType component, ComponentTooltipProvider provider) { + internalRegister(NAME_PROVIDERS, component, provider); + } + + private static void internalRegister(Map, ComponentTooltipProvider> map, + DataComponentType component, ComponentTooltipProvider provider) { + if (map.containsKey(component)) { + throw new IllegalArgumentException("Component " + component + " already has a tooltip provider registered!"); + } + map.put(component, provider); + } + + static { + register(DataComponentTypes.TROPICAL_FISH_PATTERN, new TropicalFishPatternTooltip()); + register(DataComponentTypes.INSTRUMENT, new InstrumentTooltip()); + register(DataComponentTypes.MAP_ID, new MapTooltip()); + register(DataComponentTypes.BEES, new BeesTooltip()); + register(DataComponentTypes.CONTAINER_LOOT, new ContainerLootTooltip()); + register(DataComponentTypes.CONTAINER, new ContainerContentsTooltip()); + register(DataComponentTypes.BANNER_PATTERNS, new BannerPatternLayersTooltip()); + register(DataComponentTypes.POT_DECORATIONS, new PotDecorationsTooltip()); + register(DataComponentTypes.WRITTEN_BOOK_CONTENT, new WrittenBookTooltip()); + register(DataComponentTypes.CHARGED_PROJECTILES, new ChargedProjectilesTooltip()); + register(DataComponentTypes.FIREWORKS, new FireworksTooltip()); + register(DataComponentTypes.FIREWORK_EXPLOSION, new FireworkExplosionTooltip()); + register(DataComponentTypes.POTION_CONTENTS, new PotionContentsTooltip()); + register(DataComponentTypes.JUKEBOX_PLAYABLE, new JukeboxPlayableTooltip()); + register(DataComponentTypes.TRIM, new ArmorTrimTooltip()); + register(DataComponentTypes.STORED_ENCHANTMENTS, new ItemEnchantmentsTooltip()); + register(DataComponentTypes.ENCHANTMENTS, new ItemEnchantmentsTooltip()); + register(DataComponentTypes.DYED_COLOR, new DyedItemColorTooltip()); + register(DataComponentTypes.LORE, new LoreTooltip()); + register(DataComponentTypes.OMINOUS_BOTTLE_AMPLIFIER, new OminousBottleTooltip()); + register(DataComponentTypes.SUSPICIOUS_STEW_EFFECTS, new SuspiciousStewTooltip()); + register(DataComponentTypes.BLOCK_STATE, new BlockStatePropertiesTooltip()); + register(DataComponentTypes.ATTRIBUTE_MODIFIERS, new AttributeModifiersTooltip()); + } + + @Nullable + public static ComponentTooltipProvider getNameTooltipProvider(DataComponentType component) { + return (ComponentTooltipProvider) NAME_PROVIDERS.get(component); + } + + @Nullable + public static ComponentTooltipProvider getTooltipProvider(DataComponentType component) { + return (ComponentTooltipProvider) PROVIDERS.get(component); + } + + // TODO tooltips for default components? + public static void addNameTooltips(TooltipContext context, DataComponents componentPatch, Consumer adder) { + // TODO + } + + public static void addTooltips(TooltipContext context, Consumer adder) { + tryAddTooltip(context, DataComponentTypes.TROPICAL_FISH_PATTERN, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.INSTRUMENT, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.MAP_ID, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.BEES, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.CONTAINER_LOOT, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.CONTAINER, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.BANNER_PATTERNS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.POT_DECORATIONS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.WRITTEN_BOOK_CONTENT, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.CHARGED_PROJECTILES, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.FIREWORKS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.FIREWORK_EXPLOSION, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.POTION_CONTENTS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.JUKEBOX_PLAYABLE, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.TRIM, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.STORED_ENCHANTMENTS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.ENCHANTMENTS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.DYED_COLOR, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.LORE, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.ATTRIBUTE_MODIFIERS, adder, TooltipProviders::getTooltipProvider); + if (context.components().get(DataComponentTypes.UNBREAKABLE) != null && context.options().showInTooltip(DataComponentTypes.UNBREAKABLE)) { + adder.accept(UNBREAKABLE); + } + + tryAddTooltip(context, DataComponentTypes.OMINOUS_BOTTLE_AMPLIFIER, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.SUSPICIOUS_STEW_EFFECTS, adder, TooltipProviders::getTooltipProvider); + tryAddTooltip(context, DataComponentTypes.BLOCK_STATE, adder, TooltipProviders::getTooltipProvider); + // TODO spawner + // TODO can break/can place + + if (context.advanced()) { + addAdvancedTooltips(context, adder); + } + + if (shouldAddOpWarning(context)) { + DANGEROUS_NBT_WARNING.forEach(adder); + } + } + + private static void tryAddTooltip(TooltipContext context, DataComponentType component, Consumer adder, + Function, ComponentTooltipProvider> providerGetter) { + if (context.options().showInTooltip(component)) { + T value = context.components().get(component); + if (value != null) { + ComponentTooltipProvider provider = providerGetter.apply(component); + if (provider != null) { + provider.addTooltip(context, adder, value); + } + } + } + } + + private static void addAdvancedTooltips(TooltipContext context, Consumer adder) { + int maxDamage = context.components().getOrDefault(DataComponentTypes.MAX_DAMAGE, 0); + int damage = context.components().getOrDefault(DataComponentTypes.DAMAGE, 0); + if (maxDamage > 0 && damage > 0 && context.options().showInTooltip(DataComponentTypes.DAMAGE)) { + adder.accept(Component.translatable("item.durability", Component.text(maxDamage - damage), Component.text(maxDamage))); + } + + adder.accept(Component.text(context.item().javaKey().asString()).color(NamedTextColor.DARK_GRAY)); + + int components = context.components().getDataComponents().size(); + if (components > 0) { + adder.accept(Component.translatable("item.components", Component.text(components)).color(NamedTextColor.DARK_GRAY)); + } + } + + private static boolean shouldAddOpWarning(TooltipContext context) { + NbtMap entityData = null; + if (context.item() instanceof BlockItem) { + entityData = context.components().get(DataComponentTypes.BLOCK_ENTITY_DATA); + } else if (context.item() instanceof SpawnEggItem) { + entityData = context.components().get(DataComponentTypes.ENTITY_DATA); + } + if (entityData != null) { + try { + Key entityId = MinecraftKey.key(entityData.getString("id")); + return Registries.DANGEROUS_BLOCK_ENTITIES.get().contains(entityId) || Registries.DANGEROUS_ENTITIES.get().contains(entityId); + } catch (InvalidKeyException ignored) {} + } + return false; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ArmorTrimTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ArmorTrimTooltip.java new file mode 100644 index 00000000000..7a237682ed5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ArmorTrimTooltip.java @@ -0,0 +1,45 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ArmorTrim; + +import java.util.function.Consumer; + +public class ArmorTrimTooltip implements ComponentTooltipProvider { + private static final Component TITLE = Component.translatable("item.minecraft.smithing_template.upgrade").color(NamedTextColor.GRAY); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull ArmorTrim component) { + adder.accept(TITLE); + // TODO - is the component fully provided by bedrock? even for custom stuff? + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/AttributeModifiersTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/AttributeModifiersTooltip.java new file mode 100644 index 00000000000..c518254ad69 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/AttributeModifiersTooltip.java @@ -0,0 +1,153 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemAttributeModifiers; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class AttributeModifiersTooltip implements ComponentTooltipProvider { + private static final EnumMap SLOT_NAMES = new EnumMap<>(ItemAttributeModifiers.EquipmentSlotGroup.class); + + private static final DecimalFormat ATTRIBUTE_FORMAT = new DecimalFormat("0.#####"); + private static final Key BASE_ATTACK_DAMAGE_ID = MinecraftKey.key("base_attack_damage"); + private static final Key BASE_ATTACK_SPEED_ID = MinecraftKey.key("base_attack_speed"); + + static { + // Maps slot groups to their respective translation names, ordered in their Java edition order in the item tooltip + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.ANY, "any"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.MAIN_HAND, "mainhand"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.OFF_HAND, "offhand"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.HAND, "hand"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.FEET, "feet"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.LEGS, "legs"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.CHEST, "chest"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.HEAD, "head"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.ARMOR, "armor"); + SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.BODY, "body"); + } + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull ItemAttributeModifiers modifiers) { + // maps each slot to the modifiers applied when in such slot + Map> slotsToModifiers = new HashMap<>(); + for (ItemAttributeModifiers.Entry entry : modifiers.getModifiers()) { + // convert the modifier tag to a lore entry + Component loreEntry = attributeToLore(context, entry.getAttribute(), entry.getModifier(), entry.getDisplay()); + if (loreEntry == null) { + continue; // invalid, failed, or hidden + } + + slotsToModifiers.computeIfAbsent(entry.getSlot(), s -> new ArrayList<>()).add(loreEntry); + } + + // iterate through the small array, not the map, so that ordering matches Java Edition + for (ItemAttributeModifiers.EquipmentSlotGroup slot : SLOT_NAMES.keySet()) { + List modifierTooltips = slotsToModifiers.get(slot); + if (modifierTooltips == null || modifierTooltips.isEmpty()) { + continue; + } + + // Declare the slot, e.g. "When in Main Hand" + adder.accept(Component.empty()); + adder.accept(Component.text() + .append(Component.translatable("item.modifiers." + SLOT_NAMES.get(slot))) + .color(NamedTextColor.GRAY) + .build()); + + // Then list all the modifiers when used in this slot + for (Component modifier : modifierTooltips) { + adder.accept(modifier); + } + } + } + + @Nullable + private static Component attributeToLore(TooltipContext context, int attribute, ItemAttributeModifiers.AttributeModifier modifier, + ItemAttributeModifiers.Display display) { + if (display.getType() == ItemAttributeModifiers.DisplayType.HIDDEN) { + return null; + } else if (display.getType() == ItemAttributeModifiers.DisplayType.OVERRIDE) { + return display.getComponent(); + } + + double amount = modifier.getAmount(); + if (amount == 0) { + return null; + } + + String name = AttributeType.Builtin.from(attribute).getIdentifier().asMinimalString(); + // the namespace does not need to be present, but if it is, the java client ignores it as of pre-1.20.5 + + ModifierOperation operation = modifier.getOperation(); + boolean baseModifier = false; + String operationTotal = switch (operation) { + case ADD -> { + if (name.equals("knockback_resistance")) { + amount *= 10.0; + } + + if (modifier.getId().equals(BASE_ATTACK_DAMAGE_ID)) { + amount += context.session().map(session -> session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.ATTACK_DAMAGE)).orElse(1.0F); + baseModifier = true; + } else if (modifier.getId().equals(BASE_ATTACK_SPEED_ID)) { + amount += context.session().map(session -> session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.ATTACK_SPEED)).orElse(4.0F); + baseModifier = true; + } + + yield ATTRIBUTE_FORMAT.format(amount); + } + case ADD_MULTIPLIED_BASE, ADD_MULTIPLIED_TOTAL -> + ATTRIBUTE_FORMAT.format(amount * 100) + "%"; + }; + if (amount > 0 && !baseModifier) { + operationTotal = "+" + operationTotal; + } + + + return Component.text() + .color(baseModifier ? NamedTextColor.DARK_GREEN : amount > 0 ? NamedTextColor.BLUE : NamedTextColor.RED) + .append(Component.text(operationTotal + " "), Component.translatable("attribute.name." + name)) + .build(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BannerPatternLayersTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BannerPatternLayersTooltip.java new file mode 100644 index 00000000000..04787eb0a4a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BannerPatternLayersTooltip.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.item.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.inventory.item.DyeColor; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer; + +import java.util.List; +import java.util.function.Consumer; + +public class BannerPatternLayersTooltip implements ComponentTooltipProvider> { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List patterns) { + for (int i = 0; i < Math.min(6, patterns.size()); i++) { + BannerPatternLayer layer = patterns.get(i); + context.getRegistryEntry(JavaRegistries.BANNER_PATTERN, layer.getPattern(), pattern -> { + DyeColor color = DyeColor.getById(layer.getColorId()); + if (color != null) { + adder.accept(Component.translatable(pattern.translationKey() + "." + color.getJavaIdentifier()).color(NamedTextColor.GRAY)); + } + }); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BeesTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BeesTooltip.java new file mode 100644 index 00000000000..e4319da1755 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BeesTooltip.java @@ -0,0 +1,45 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.BeehiveOccupant; + +import java.util.List; +import java.util.function.Consumer; + +public class BeesTooltip implements ComponentTooltipProvider> { + private static final Component MAX_BEES = Component.text(3); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List bees) { + adder.accept(Component.translatable("container.beehive.bees", Component.text(bees.size()), MAX_BEES).color(NamedTextColor.GRAY)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BlockStatePropertiesTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BlockStatePropertiesTooltip.java new file mode 100644 index 00000000000..cc70a0afe44 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/BlockStatePropertiesTooltip.java @@ -0,0 +1,50 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.BlockStateProperties; + +import java.util.function.Consumer; + +public class BlockStatePropertiesTooltip implements ComponentTooltipProvider { + private static final Component MAX_HONEY_LEVEL = Component.text(5); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull BlockStateProperties state) { + String honeyLevel = state.getProperties().get("honey_level"); + if (honeyLevel != null) { + try { + int level = Integer.parseInt(honeyLevel); + adder.accept(Component.translatable("container.beehive.honey", Component.text(level), MAX_HONEY_LEVEL).color(NamedTextColor.GRAY)); + } catch (NumberFormatException ignored) {} + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ChargedProjectilesTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ChargedProjectilesTooltip.java new file mode 100644 index 00000000000..bf7a4801c08 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ChargedProjectilesTooltip.java @@ -0,0 +1,77 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.item.tooltip.TooltipProviders; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; + +import java.util.List; +import java.util.function.Consumer; + +public class ChargedProjectilesTooltip implements ComponentTooltipProvider> { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List projectiles) { + ItemStack currentStack = null; + int count = 0; + + for (ItemStack stack : projectiles) { + if (currentStack == null) { + currentStack = stack; + count = 1; + } else if (currentStack.equals(stack)) { + count++; + } else { + addProjectile(context, currentStack, count, adder); + currentStack = stack; + count = 1; + } + } + + if (currentStack != null) { + addProjectile(context, currentStack, count, adder); + } + } + + private static void addProjectile(TooltipContext context, ItemStack stack, int count, Consumer adder) { + GeyserItemStack geyserStack = GeyserItemStack.from(stack); + if (count == 1) { + adder.accept(Component.translatable("item.minecraft.crossbow.projectile.single", geyserStack.getName())); + } else { + adder.accept(Component.translatable("item.minecraft.crossbow.projectile.multiple", Component.text(count), Component.text(stack.toString()))); + } + + TooltipProviders.addTooltips(context.withItemComponents(Registries.JAVA_ITEMS.get(stack.getId()), geyserStack.getAllComponents()).withFlags(false, false), + tooltip -> adder.accept(Component.space().append(tooltip).color(NamedTextColor.GRAY))); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ContainerContentsTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ContainerContentsTooltip.java new file mode 100644 index 00000000000..75b3f73ae86 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ContainerContentsTooltip.java @@ -0,0 +1,62 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; + +import java.util.List; +import java.util.function.Consumer; + +public class ContainerContentsTooltip implements ComponentTooltipProvider> { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List stacks) { + int lines = 0; + int total = 0; + + for (ItemStack stack : stacks) { + GeyserItemStack itemStack = GeyserItemStack.from(stack); + if (itemStack.isEmpty()) { + continue; + } + total++; + if (lines < 5) { + lines++; + adder.accept(Component.translatable("item.container.item_count", itemStack.getName(), Component.text(itemStack.getAmount()))); + } + } + + if (total - lines > 0) { + adder.accept(Component.translatable("item.container.more_items", Component.text(total - lines)).decorate(TextDecoration.ITALIC)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ContainerLootTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ContainerLootTooltip.java new file mode 100644 index 00000000000..99497c8b0b6 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ContainerLootTooltip.java @@ -0,0 +1,43 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; + +import java.util.function.Consumer; + +public class ContainerLootTooltip implements ComponentTooltipProvider { + private static final Component TOOLTIP = Component.translatable("item.container.loot_table.unknown"); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull NbtMap component) { + adder.accept(TOOLTIP); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/DyedItemColorTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/DyedItemColorTooltip.java new file mode 100644 index 00000000000..5aaa16255b0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/DyedItemColorTooltip.java @@ -0,0 +1,48 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; + +import java.util.Locale; +import java.util.function.Consumer; + +public class DyedItemColorTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull Integer color) { + if (context.advanced()) { + adder.accept(Component.translatable("item.color", Component.text(String.format(Locale.ROOT, "#%06X", color)).color(NamedTextColor.GRAY))); + } else { + adder.accept(Component.translatable("item.dyed").style(style -> style.color(NamedTextColor.GRAY).decorate(TextDecoration.ITALIC))); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/FireworkExplosionTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/FireworkExplosionTooltip.java new file mode 100644 index 00000000000..4572b994e2b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/FireworkExplosionTooltip.java @@ -0,0 +1,85 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.components.FireworkExplosionShape; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.level.FireworkColor; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Fireworks; + +import java.util.Locale; +import java.util.function.Consumer; + +public class FireworkExplosionTooltip implements ComponentTooltipProvider { + private static final Component CUSTOM_COLOR = Component.translatable("item.minecraft.firework_star.custom_color"); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, Fireworks.@NonNull FireworkExplosion explosion) { + adder.accept(FireworkExplosionShape.values()[explosion.getShapeId()].displayName().color(NamedTextColor.GRAY)); + addDetails(explosion, adder); + } + + public static void addDetails(Fireworks.FireworkExplosion explosion, Consumer adder) { + if (explosion.getColors().length > 0) { + adder.accept(writeColors(Component.translatable(), explosion.getColors()).color(NamedTextColor.GRAY)); + } + if (explosion.getFadeColors().length > 0) { + adder.accept(writeColors(Component.translatable().key("item.minecraft.firework_star.fade_to"), explosion.getFadeColors()).color(NamedTextColor.GRAY)); + } + if (explosion.isHasTrail()) { + adder.accept(Component.translatable("item.minecraft.firework_star.trail").color(NamedTextColor.GRAY)); + } + if (explosion.isHasTwinkle()) { + adder.accept(Component.translatable("item.minecraft.firework_star.flicker").color(NamedTextColor.GRAY)); + } + } + + private static Component writeColors(TranslatableComponent.Builder builder, int[] colors) { + for (int i = 0; i < colors.length; i++) { + if (i > 0) { + builder.append(Component.text(", ")); + } + + int color = colors[i]; + builder.append(getFireworkColorName(color)); + } + + return builder.build(); + } + + private static Component getFireworkColorName(int rgb) { + FireworkColor builtin = FireworkColor.fromJavaRGB(rgb); + if (builtin == null) { + return CUSTOM_COLOR; + } + return Component.translatable("item.minecraft.firework_star." + builtin.name().toLowerCase(Locale.ROOT)); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/FireworksTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/FireworksTooltip.java new file mode 100644 index 00000000000..b4ddff54395 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/FireworksTooltip.java @@ -0,0 +1,80 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.components.FireworkExplosionShape; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Fireworks; + +import java.util.function.Consumer; + +public class FireworksTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull Fireworks fireworks) { + if (fireworks.getFlightDuration() > 0) { + adder.accept(Component.translatable("item.minecraft.firework_rocket.flight") + .append(Component.space()) + .append(Component.text(fireworks.getFlightDuration())) + .color(NamedTextColor.GRAY)); + } + + Fireworks.FireworkExplosion current = null; + int count = 0; + + for (Fireworks.FireworkExplosion explosion : fireworks.getExplosions()) { + if (current == null) { + current = explosion; + count = 1; + } else if (current.equals(explosion)) { + count++; + } else { + addExplosion(current, count, adder); + current = explosion; + count = 1; + } + } + + if (current != null) { + addExplosion(current, count, adder); + } + } + + private static void addExplosion(Fireworks.FireworkExplosion explosion, int count, Consumer adder) { + Component name = FireworkExplosionShape.values()[explosion.getShapeId()].displayName(); + if (count == 1) { + adder.accept(Component.translatable("item.minecraft.firework_rocket.single_star", name).color(NamedTextColor.GRAY)); + } else { + adder.accept(Component.translatable("item.minecraft.firework_rocket.multiple_stars", Component.text(count), name).color(NamedTextColor.GRAY)); + } + + FireworkExplosionTooltip.addDetails(explosion, detail -> adder.accept(Component.space().append(detail))); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/InstrumentTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/InstrumentTooltip.java new file mode 100644 index 00000000000..47913b08fbf --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/InstrumentTooltip.java @@ -0,0 +1,50 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.inventory.item.GeyserInstrument; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; + +import java.util.function.Consumer; + +public class InstrumentTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull InstrumentComponent component) { + Consumer tooltipAdder = instrument -> adder.accept(instrument.description().colorIfAbsent(NamedTextColor.GRAY)); + if (component.instrumentLocation() != null) { + context.getRegistryEntry(JavaRegistries.INSTRUMENT, component.instrumentLocation(), tooltipAdder); + } else { + context.getRegistryEntry(JavaRegistries.INSTRUMENT, component.instrumentHolder(), tooltipAdder); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ItemEnchantmentsTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ItemEnchantmentsTooltip.java new file mode 100644 index 00000000000..09a11eff8a6 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/ItemEnchantmentsTooltip.java @@ -0,0 +1,84 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.enchantment.Enchantment; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.tags.EnchantmentTag; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemEnchantments; + +import java.util.Map; +import java.util.function.Consumer; + +public class ItemEnchantmentsTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull ItemEnchantments component) { + context.withSession(session -> { + int[] tooltipOrder = session.getTagCache().getRaw(EnchantmentTag.TOOLTIP_ORDER); + + Map enchantments = component.getEnchantments(); + for (int orderedEnchantment : tooltipOrder) { + Integer level = enchantments.get(orderedEnchantment); + if (level != null && level > 0) { + adder.accept(enchantmentName(session, orderedEnchantment, level)); + } + } + + for (int enchantment : enchantments.keySet()) { + if (!session.getTagCache().is(EnchantmentTag.TOOLTIP_ORDER, enchantment)) { + adder.accept(enchantmentName(session, enchantment, enchantments.get(enchantment))); + } + } + }); + } + + private static Component enchantmentName(GeyserSession session, int id, int level) { + Enchantment enchantment = JavaRegistries.ENCHANTMENT.value(session, id); + if (enchantment == null) { + return Component.empty(); + } + + Component tooltip = enchantment.description(); + boolean curse = session.getTagCache().is(EnchantmentTag.CURSE, enchantment); + if (curse) { + tooltip = tooltip.colorIfAbsent(NamedTextColor.RED); + } else { + tooltip = tooltip.colorIfAbsent(NamedTextColor.GRAY); + } + + if (level != 1 && enchantment.maxLevel() != 1) { + tooltip = tooltip.append(Component.space()).append(Component.translatable("enchantment.level." + level)); + } + return tooltip; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/JukeboxPlayableTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/JukeboxPlayableTooltip.java new file mode 100644 index 00000000000..436d9070d97 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/JukeboxPlayableTooltip.java @@ -0,0 +1,50 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.level.JukeboxSong; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.JukeboxPlayable; + +import java.util.function.Consumer; + +public class JukeboxPlayableTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull JukeboxPlayable playable) { + Consumer tooltipAdder = song -> adder.accept(song.description().colorIfAbsent(NamedTextColor.GRAY)); + if (playable.songLocation() != null) { + context.getRegistryEntry(JavaRegistries.JUKEBOX_SONG, playable.songLocation(), tooltipAdder); + } else { + context.getRegistryEntry(JavaRegistries.JUKEBOX_SONG, playable.songHolder(), tooltipAdder); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/LoreTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/LoreTooltip.java new file mode 100644 index 00000000000..1bd643dbfdc --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/LoreTooltip.java @@ -0,0 +1,46 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; + +import java.util.List; +import java.util.function.Consumer; + +public class LoreTooltip implements ComponentTooltipProvider> { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List lore) { + for (Component component : lore) { + adder.accept(component.colorIfAbsent(NamedTextColor.DARK_PURPLE).decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.TRUE)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/MapTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/MapTooltip.java new file mode 100644 index 00000000000..22b8ba4fe66 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/MapTooltip.java @@ -0,0 +1,41 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; + +import java.util.function.Consumer; + +public class MapTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull Integer component) { + // TODO + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/OminousBottleTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/OminousBottleTooltip.java new file mode 100644 index 00000000000..6c223d84e81 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/OminousBottleTooltip.java @@ -0,0 +1,46 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectDetails; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectInstance; + +import java.util.List; +import java.util.function.Consumer; + +public class OminousBottleTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull Integer amplifier) { + PotionContentsTooltip.addTooltip(List.of( + new MobEffectInstance(Effect.BAD_OMEN, new MobEffectDetails(amplifier, 120000, false, false, true, null))), 1.0F, adder); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/PotDecorationsTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/PotDecorationsTooltip.java new file mode 100644 index 00000000000..fb1f75c9be0 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/PotDecorationsTooltip.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.item.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.registry.Registries; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class PotDecorationsTooltip implements ComponentTooltipProvider> { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List contents) { + List tooltip = new ArrayList<>(); + + boolean allBrick = true; + for (int item : contents) { + if (item != Items.BRICK.javaId()) { + allBrick = false; + } + // TODO name + tooltip.add(Component.translatable(Registries.JAVA_ITEMS.get(item).translationKey()).color(NamedTextColor.GRAY)); + } + + if (!allBrick) { + adder.accept(Component.empty()); + tooltip.forEach(adder); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/PotionContentsTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/PotionContentsTooltip.java new file mode 100644 index 00000000000..f1be4e09ac2 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/PotionContentsTooltip.java @@ -0,0 +1,94 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.MobEffectInstance; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; + +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +public class PotionContentsTooltip implements ComponentTooltipProvider { + private static final Component NO_EFFECTS = Component.translatable("effect.none").color(NamedTextColor.GRAY); + private static final Component INFINITE_DURATION = Component.translatable("effect.duration.infinite").color(NamedTextColor.GRAY); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull PotionContents potion) { + addTooltip(collectEffects(potion), context.components().getOrDefault(DataComponentTypes.POTION_DURATION_SCALE, 1.0F), adder); + } + + public static void addTooltip(List effects, float durationScale, Consumer adder) { + if (effects.isEmpty()) { + adder.accept(NO_EFFECTS); + return; + } + + for (MobEffectInstance effect : effects) { + Component description = potionDescription(effect.getEffect(), effect.getDetails().getAmplifier()); + if (effect.getDetails().getDuration() == -1 || effect.getDetails().getDuration() <= 20) { + description = Component.translatable("potion.withDuration", description, potionDuration(effect.getDetails().getDuration(), durationScale)); + } + adder.accept(description); + } + + // Java adds the mob effect's attribute modifiers here too. We can't do that, because they aren't sent to us, and we don't have them anywhere. + } + + private static Component potionDescription(Effect effect, int level) { + Component name = Component.translatable("effect.minecraft." + effect.name().toLowerCase(Locale.ROOT)); + return level > 0 ? Component.translatable("potion.withAmplifier", name, Component.translatable("potion.potency." + level)) : name; + } + + private static Component potionDuration(int duration, float durationScale) { + if (duration == -1) { + return INFINITE_DURATION; + } + int ticks = (int) Math.floor(duration * durationScale); + return Component.text(formatDuration(ticks)); + } + + private static String formatDuration(int ticks) { + int seconds = Math.floorDiv(ticks, 20); + int minutes = (seconds / 60); + int hours = (minutes / 60); + return hours > 0 ? String.format("%02d:%02d:%02d", hours, minutes % 60, seconds % 60) : String.format("%02d:%02d", minutes % 60, seconds % 60); + } + + private static List collectEffects(PotionContents potion) { + if (potion.getPotionId() == -1) { // No built-in potion + return potion.getCustomEffects(); + } + return List.of(); // TODO built in effects where? + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/SuspiciousStewTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/SuspiciousStewTooltip.java new file mode 100644 index 00000000000..3e87ef8061b --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/SuspiciousStewTooltip.java @@ -0,0 +1,56 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; +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.SuspiciousStewEffect; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class SuspiciousStewTooltip implements ComponentTooltipProvider> { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull List effects) { + if (context.creative()) { + List mobEffects = new ArrayList<>(); + + for (SuspiciousStewEffect effect : effects) { + mobEffects.add(new MobEffectInstance(Effect.values()[effect.getMobEffectId()], + new MobEffectDetails(0, effect.getDuration(), false, false, false, null))); + } + + PotionContentsTooltip.addTooltip(mobEffects, 1.0F, adder); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/TropicalFishPatternTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/TropicalFishPatternTooltip.java new file mode 100644 index 00000000000..5a7ab3bb7c1 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/TropicalFishPatternTooltip.java @@ -0,0 +1,64 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +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.geysermc.geyser.entity.type.living.animal.TropicalFishEntity; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; + +import java.util.function.Consumer; + +public class TropicalFishPatternTooltip implements ComponentTooltipProvider { + private static final Style STYLE = Style.style(NamedTextColor.GRAY, TextDecoration.ITALIC); + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull Integer pattern) { + int baseColor = context.components().getOrDefault(DataComponentTypes.TROPICAL_FISH_BASE_COLOR, 0); + int patternColor = context.components().getOrDefault(DataComponentTypes.TROPICAL_FISH_PATTERN_COLOR, 0); + + int packedVariant = TropicalFishEntity.getPackedVariant(pattern, baseColor, patternColor); + + int predefinedVariantId = TropicalFishEntity.getPredefinedId(packedVariant); + if (predefinedVariantId != -1) { + adder.accept(Component.translatable("entity.minecraft.tropical_fish.predefined." + predefinedVariantId, STYLE)); + } else { + adder.accept(Component.translatable("entity.minecraft.tropical_fish.type." + TropicalFishEntity.getVariantName(packedVariant), STYLE)); + + Component colorTooltip = Component.translatable("color.minecraft." + TropicalFishEntity.getColorName((byte) baseColor)); + if (baseColor != patternColor) { + colorTooltip = colorTooltip.append(Component.text(", ")) + .append(Component.translatable("color.minecraft." + TropicalFishEntity.getColorName((byte) patternColor))); + } + adder.accept(colorTooltip.style(STYLE)); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/WrittenBookTooltip.java b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/WrittenBookTooltip.java new file mode 100644 index 00000000000..d60b7952edb --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/tooltip/providers/WrittenBookTooltip.java @@ -0,0 +1,46 @@ +/* + * 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.tooltip.providers; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.item.tooltip.ComponentTooltipProvider; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.WrittenBookContent; + +import java.util.function.Consumer; + +public class WrittenBookTooltip implements ComponentTooltipProvider { + + @Override + public void addTooltip(TooltipContext context, Consumer adder, @NonNull WrittenBookContent book) { + if (!book.getAuthor().trim().isEmpty()) { + adder.accept(Component.translatable("book.byAuthor", Component.text(book.getAuthor())).color(NamedTextColor.GRAY)); + } + adder.accept(Component.translatable("book.generation." + book.getGeneration()).color(NamedTextColor.GRAY)); + } +} 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 33a423c50b6..1ba1ba51e86 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 @@ -35,6 +35,7 @@ import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; import org.geysermc.geyser.inventory.item.BannerPattern; +import org.geysermc.geyser.inventory.item.BedrockBannerPattern; import org.geysermc.geyser.inventory.item.DyeColor; import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.level.block.type.Block; @@ -61,19 +62,19 @@ public class BannerItem extends BlockItem { * ominous banners that we set instead. This variable is used to detect Java ominous banner patterns, and apply * the correct ominous banner pattern if Bedrock pulls the item from creative. */ - private static final List> OMINOUS_BANNER_PATTERN; + private static final List> OMINOUS_BANNER_PATTERN; static { // Construct what an ominous banner is supposed to look like OMINOUS_BANNER_PATTERN = List.of( - Pair.of(BannerPattern.RHOMBUS, DyeColor.CYAN), - Pair.of(BannerPattern.STRIPE_BOTTOM, DyeColor.LIGHT_GRAY), - Pair.of(BannerPattern.STRIPE_CENTER, DyeColor.GRAY), - Pair.of(BannerPattern.BORDER, DyeColor.LIGHT_GRAY), - Pair.of(BannerPattern.STRIPE_MIDDLE, DyeColor.BLACK), - Pair.of(BannerPattern.HALF_HORIZONTAL, DyeColor.LIGHT_GRAY), - Pair.of(BannerPattern.CIRCLE, DyeColor.LIGHT_GRAY), - Pair.of(BannerPattern.BORDER, DyeColor.BLACK) + Pair.of(BedrockBannerPattern.RHOMBUS, DyeColor.CYAN), + Pair.of(BedrockBannerPattern.STRIPE_BOTTOM, DyeColor.LIGHT_GRAY), + Pair.of(BedrockBannerPattern.STRIPE_CENTER, DyeColor.GRAY), + Pair.of(BedrockBannerPattern.BORDER, DyeColor.LIGHT_GRAY), + Pair.of(BedrockBannerPattern.STRIPE_MIDDLE, DyeColor.BLACK), + Pair.of(BedrockBannerPattern.HALF_HORIZONTAL, DyeColor.LIGHT_GRAY), + Pair.of(BedrockBannerPattern.CIRCLE, DyeColor.LIGHT_GRAY), + Pair.of(BedrockBannerPattern.BORDER, DyeColor.BLACK) ); } @@ -83,13 +84,13 @@ public static boolean isOminous(GeyserSession session, List } for (int i = 0; i < OMINOUS_BANNER_PATTERN.size(); i++) { BannerPatternLayer patternLayer = patternLayers.get(i); - Pair pair = OMINOUS_BANNER_PATTERN.get(i); + Pair pair = OMINOUS_BANNER_PATTERN.get(i); if (patternLayer.getColorId() != pair.right().ordinal() || !patternLayer.getPattern().isId()) { return false; } - BannerPattern bannerPattern = session.getRegistryCache().registry(JavaRegistries.BANNER_PATTERN).byId(patternLayer.getPattern().id()); - if (bannerPattern != pair.left()) { + BannerPattern bannerPattern = JavaRegistries.BANNER_PATTERN.value(session, patternLayer.getPattern().id()); + if (bannerPattern == null || bannerPattern.bedrockPattern() != pair.left()) { return false; } } @@ -104,13 +105,13 @@ public static boolean isOminous(List blockEntityPatterns) { } for (int i = 0; i < OMINOUS_BANNER_PATTERN.size(); i++) { NbtMap patternLayer = blockEntityPatterns.get(i); - Pair pair = OMINOUS_BANNER_PATTERN.get(i); + Pair pair = OMINOUS_BANNER_PATTERN.get(i); DyeColor color = DyeColor.getByJavaIdentifier(patternLayer.getString("color")); if (color != pair.right()) { return false; } Key id = MinecraftKey.key(patternLayer.getString("pattern")); // Ouch - BannerPattern bannerPattern = BannerPattern.getByJavaIdentifier(id); + BedrockBannerPattern bannerPattern = BedrockBannerPattern.getByJavaIdentifier(id); if (bannerPattern != pair.left()) { return false; } @@ -147,12 +148,14 @@ static void convertBannerPattern(GeyserSession session, List List patternList = new ArrayList<>(patterns.size()); for (BannerPatternLayer patternLayer : patterns) { patternLayer.getPattern().ifId(id -> { - BannerPattern bannerPattern = session.getRegistryCache().registry(JavaRegistries.BANNER_PATTERN).byId(id); - NbtMap tag = NbtMap.builder() - .putString("Pattern", bannerPattern.getBedrockIdentifier()) + BannerPattern bannerPattern = JavaRegistries.BANNER_PATTERN.value(session, id); + if (bannerPattern != null) { + NbtMap tag = NbtMap.builder() + .putString("Pattern", bannerPattern.bedrockPattern().getBedrockIdentifier()) .putInt("Color", 15 - patternLayer.getColorId()) .build(); - patternList.add(tag); + patternList.add(tag); + } }); } builder.putList("Patterns", NbtType.COMPOUND, patternList); @@ -167,7 +170,7 @@ static void convertBannerPattern(GeyserSession session, List */ private static NbtMap getBedrockBannerPattern(NbtMap pattern) { // ViaVersion 1.20.4 -> 1.20.5 can send without the namespace - BannerPattern bannerPattern = BannerPattern.getByJavaIdentifier(MinecraftKey.key(pattern.getString("pattern"))); + BedrockBannerPattern bannerPattern = BedrockBannerPattern.getByJavaIdentifier(MinecraftKey.key(pattern.getString("pattern"))); DyeColor dyeColor = DyeColor.getByJavaIdentifier(pattern.getString("color")); if (bannerPattern == null || dyeColor == null) { return null; @@ -186,14 +189,10 @@ private static NbtMap getBedrockBannerPattern(NbtMap pattern) { * @return The Java edition format pattern layer */ public static BannerPatternLayer getJavaBannerPattern(GeyserSession session, NbtMap pattern) { - JavaRegistry registry = session.getRegistryCache().registry(JavaRegistries.BANNER_PATTERN); - BannerPattern bannerPattern = BannerPattern.getByBedrockIdentifier(pattern.getString("Pattern")); + BedrockBannerPattern bedrockPattern = BedrockBannerPattern.getByBedrockIdentifier(pattern.getString("Pattern")); DyeColor dyeColor = DyeColor.getById(15 - pattern.getInt("Color")); if (dyeColor != null) { - int id = registry.byValue(bannerPattern); - if (id != -1) { - return new BannerPatternLayer(Holder.ofId(id), dyeColor.ordinal()); - } + return new BannerPatternLayer(Holder.ofId(BannerPattern.findNetworkId(session, bedrockPattern)), dyeColor.ordinal()); } return null; } @@ -221,8 +220,7 @@ public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap b List patternLayers = new ArrayList<>(); for (int i = 0; i < OMINOUS_BANNER_PATTERN.size(); i++) { var pair = OMINOUS_BANNER_PATTERN.get(i); - patternLayers.add(new BannerPatternLayer(Holder.ofId(session.getRegistryCache().registry(JavaRegistries.BANNER_PATTERN).byValue(pair.left())), - pair.right().ordinal())); + patternLayers.add(new BannerPatternLayer(Holder.ofId(BannerPattern.findNetworkId(session, pair.left())), pair.right().ordinal())); } components.put(DataComponentTypes.BANNER_PATTERNS, patternLayers); 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 ef1ca52c522..6023fc2b39c 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 @@ -25,6 +25,7 @@ package org.geysermc.geyser.item.type; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -39,10 +40,17 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.LodestoneTracker; public class CompassItem extends Item { + private static final Component LODESTONE_NAME = Component.translatable("item.minecraft.lodestone_compass"); + public CompassItem(String javaIdentifier, Builder builder) { super(javaIdentifier, builder); } + @Override + public Component getName(GeyserItemStack stack) { + return stack.hasComponent(DataComponentTypes.LODESTONE_TRACKER) ? LODESTONE_NAME : super.getName(stack); + } + @Override public ItemData.Builder translateToBedrock(GeyserSession session, int count, DataComponents components, ItemMapping mapping, ItemMappings mappings) { if (isLodestoneCompass(components)) { 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 a0060044f01..507bacc0fbe 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 @@ -52,6 +52,7 @@ public FireworkRocketItem(String javaIdentifier, Builder builder) { public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { super.translateComponentsToBedrock(session, components, tooltip, builder); + // TODO can this be removed? does this have any client side functionality other than tooltip? Fireworks fireworks = components.get(DataComponentTypes.FIREWORKS); if (fireworks == null) { return; @@ -103,7 +104,7 @@ static NbtMap translateExplosionToBedrock(Fireworks.FireworkExplosion explosion) int i = 0; for (int color : oldColors) { - colors[i++] = FireworkColor.fromJavaRGB(color); + colors[i++] = FireworkColor.bedrockIdFromJavaRGB(color); } newExplosionData.putByteArray("FireworkColor", colors); @@ -113,7 +114,7 @@ static NbtMap translateExplosionToBedrock(Fireworks.FireworkExplosion explosion) i = 0; for (int color : oldColors) { - colors[i++] = FireworkColor.fromJavaRGB(color); + colors[i++] = FireworkColor.bedrockIdFromJavaRGB(color); } newExplosionData.putByteArray("FireworkFade", colors); 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 7d0cfa796f5..a798cefa28e 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 @@ -63,19 +63,6 @@ public ItemData.Builder translateToBedrock(GeyserSession session, int count, Dat return builder; } - @Override - 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); - if (component != null && tooltip.showInTooltip(DataComponentTypes.INSTRUMENT)) { - GeyserInstrument instrument = GeyserInstrument.fromComponent(session, component); - if (instrument.bedrockInstrument() == null) { - builder.getOrCreateLore().add(instrument.description()); - } - } - } - @Override public @NonNull GeyserItemStack translateToJava(GeyserSession session, @NonNull ItemData itemData, @NonNull ItemMapping mapping, @NonNull ItemMappings mappings) { GeyserItemStack itemStack = super.translateToJava(session, itemData, mapping, mappings); 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 bf8d4786ef4..84b3ed2904f 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 @@ -134,6 +134,10 @@ public T getComponent(@NonNull DataComponentType type) { return baseComponents.get(type); } + public Component getName(GeyserItemStack stack) { + return baseComponents.getOrDefault(DataComponentTypes.ITEM_NAME, Component.empty()); + } + public String translationKey() { return "item." + javaIdentifier.namespace() + "." + javaIdentifier.value(); } @@ -164,14 +168,6 @@ public ItemMapping toBedrockDefinition(DataComponents components, ItemMappings m * Takes components from Java Edition and map them into Bedrock. */ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { - List loreComponents = components.get(DataComponentTypes.LORE); - 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) { builder.setDamage(damage); @@ -254,8 +250,7 @@ public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap b BedrockEnchantment bedrockEnchantment = enchantment.bedrockEnchantment(); if (bedrockEnchantment == null) { - String enchantmentTranslation = MinecraftLocale.getLocaleString(enchantment.description(), session.locale()); - addJavaOnlyEnchantment(session, builder, enchantmentTranslation, level); + // Java only return null; } @@ -265,12 +260,6 @@ public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap b .build(); } - private void addJavaOnlyEnchantment(GeyserSession session, BedrockItemBuilder builder, String enchantmentName, int level) { - String lvlTranslation = MinecraftLocale.getLocaleString("enchantment.level." + level, session.locale()); - - builder.getOrCreateLore().add(0, ChatColor.RESET + ChatColor.GRAY + enchantmentName + " " + lvlTranslation); - } - protected final void translateDyedColor(DataComponents components, BedrockItemBuilder builder) { Integer dyedItemColor = components.get(DataComponentTypes.DYED_COLOR); if (dyedItemColor != 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 1ead9f2adb0..9d4f019ad0c 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PlayerHeadItem.java @@ -25,7 +25,9 @@ package org.geysermc.geyser.item.type; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.item.components.Rarity; import org.geysermc.geyser.level.block.type.Block; @@ -42,6 +44,14 @@ public PlayerHeadItem(Builder builder, Block block, Block... otherBlocks) { super(builder, block, otherBlocks); } + @Override + public Component getName(GeyserItemStack stack) { + GameProfile profile = stack.getComponent(DataComponentTypes.PROFILE); + return profile != null && profile.getName() != null + ? Component.translatable(translationKey() + ".named", profile.getName()) + : super.getName(stack); + } + @Override public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { super.translateComponentsToBedrock(session, components, tooltip, builder); @@ -49,6 +59,7 @@ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNul // Use the correct color, determined by the rarity of the item char rarity = Rarity.fromId(components.getOrDefault(DataComponentTypes.RARITY, Rarity.COMMON.ordinal())).getColor(); + // TODO name translation where? GameProfile profile = components.get(DataComponentTypes.PROFILE); if (profile != null) { String name = profile.getName(); diff --git a/core/src/main/java/org/geysermc/geyser/item/type/PotionItem.java b/core/src/main/java/org/geysermc/geyser/item/type/PotionItem.java index 2cdd6e4c183..c1fd8715ada 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/PotionItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/PotionItem.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.item.type; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -39,11 +40,20 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import org.geysermc.mcprotocollib.protocol.data.game.item.component.PotionContents; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Function; + public class PotionItem extends Item { public PotionItem(String javaIdentifier, Builder builder) { super(javaIdentifier, builder); } + @Override + public Component getName(GeyserItemStack stack) { + return getName(stack, translationKey(), super::getName); + } + @Override public ItemData.Builder translateToBedrock(GeyserSession session, int count, DataComponents components, ItemMapping mapping, ItemMappings mappings) { if (components == null) return super.translateToBedrock(session, count, components, mapping, mappings); @@ -82,4 +92,17 @@ public ItemData.Builder translateToBedrock(GeyserSession session, int count, Dat public boolean ignoreDamage() { return true; } + + public static Component getName(GeyserItemStack stack, String translationKey, Function fallback) { + PotionContents contents = stack.getComponent(DataComponentTypes.POTION_CONTENTS); + return contents != null ? potionName(contents, translationKey + ".effect.") : fallback.apply(stack); + } + + private static Component potionName(PotionContents contents, String baseTranslation) { + String name = contents.getCustomName() != null + ? contents.getCustomName() + : contents.getPotionId() == -1 ? "empty" + : Objects.requireNonNull(Potion.getByJavaId(contents.getPotionId())).getJavaIdentifier().toLowerCase(Locale.ROOT); + return Component.translatable(baseTranslation + name); + } } 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 9d44920f031..1b7f2397a28 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 @@ -25,7 +25,10 @@ package org.geysermc.geyser.item.type; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.inventory.item.DyeColor; import org.geysermc.geyser.item.TooltipOptions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; @@ -34,12 +37,21 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import java.util.List; +import java.util.Objects; public class ShieldItem extends Item { public ShieldItem(String javaIdentifier, Builder builder) { super(javaIdentifier, builder); } + @Override + public Component getName(GeyserItemStack stack) { + Integer color = stack.getComponent(DataComponentTypes.BASE_COLOR); + return color != null + ? Component.translatable(translationKey() + "." + Objects.requireNonNull(DyeColor.getById(color)).getJavaIdentifier()) + : super.getName(stack); + } + @Override public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { super.translateComponentsToBedrock(session, components, tooltip, builder); 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 e2e910b1744..bf886f40fad 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 @@ -53,7 +53,7 @@ public ShulkerBoxItem(Builder builder, Block block, Block... otherBlocks) { super(builder, block, otherBlocks); } - @Override + /*@Override public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNull DataComponents components, @NonNull TooltipOptions tooltip, @NonNull BedrockItemBuilder builder) { super.translateComponentsToBedrock(session, components, tooltip, builder); @@ -117,5 +117,5 @@ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNul itemsList.add(boxItemNbt.build()); } builder.putList("Items", NbtType.COMPOUND, itemsList); - } + }*/ // TODO can this be removed? is it just for the tooltip? } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/TippedArrowItem.java b/core/src/main/java/org/geysermc/geyser/item/type/TippedArrowItem.java index ae77be6433d..0494276c5ed 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/TippedArrowItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/TippedArrowItem.java @@ -25,8 +25,10 @@ package org.geysermc.geyser.item.type; +import net.kyori.adventure.text.Component; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.item.Potion; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMappings; @@ -40,6 +42,11 @@ public TippedArrowItem(String javaIdentifier, Builder builder) { super(javaIdentifier, builder); } + @Override + public Component getName(GeyserItemStack stack) { + return PotionItem.getName(stack, translationKey(), super::getName); + } + @Override public ItemData.Builder translateToBedrock(GeyserSession session, int count, DataComponents components, ItemMapping mapping, ItemMappings mappings) { if (components != null) { 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 011d5bb2244..372914de938 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 @@ -25,24 +25,14 @@ package org.geysermc.geyser.item.type; -import net.kyori.adventure.text.Component; -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.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; -import org.geysermc.geyser.translator.text.MessageTranslator; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; -import java.util.List; - public class TropicalFishBucketItem extends Item { - private static final Style LORE_STYLE = Style.style(NamedTextColor.GRAY, TextDecoration.ITALIC); public TropicalFishBucketItem(String javaIdentifier, Builder builder) { super(javaIdentifier, builder); @@ -55,48 +45,5 @@ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNul // 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 - 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(packedVariant); - if (predefinedVariantId != -1) { - 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(packedVariant), LORE_STYLE); - lore.add(0, MessageTranslator.convertMessage(typeTooltip, session.locale())); - - 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())); - } - } - } - } - - 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/WrittenBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java index 22064e44cf1..a22f9daa85d 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 @@ -68,6 +68,7 @@ public void translateComponentsToBedrock(@NonNull GeyserSession session, @NonNul } builder.putList("pages", NbtType.COMPOUND, bedrockPages); + // TODO can this go? builder.putString("title", bookContent.getTitle().getRaw()) .putString("author", bookContent.getAuthor()) .putInt("generation", bookContent.getGeneration()); diff --git a/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java b/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java index 2ee8509a0c1..f34983acc44 100644 --- a/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java +++ b/core/src/main/java/org/geysermc/geyser/level/FireworkColor.java @@ -49,9 +49,11 @@ public enum FireworkColor { private static final FireworkColor[] VALUES = values(); + private final int rgbValue; private final TextColor color; FireworkColor(int rgbValue) { + this.rgbValue = rgbValue; this.color = TextColor.color(rgbValue); } @@ -62,7 +64,16 @@ private static HSVLike toHSV(int rgbValue) { return HSVLike.fromRGB(r, g, b); } - public static byte fromJavaRGB(int rgbValue) { + public static FireworkColor fromJavaRGB(int rgbValue) { + for (FireworkColor color : values()) { + if (color.rgbValue == rgbValue) { + return color; + } + } + return null; + } + + public static byte bedrockIdFromJavaRGB(int rgbValue) { HSVLike hsv = toHSV(rgbValue); return (byte) nearestTo(hsv).ordinal(); } diff --git a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java index 1bed4099a74..3e27238c4a2 100644 --- a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java +++ b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java @@ -25,17 +25,23 @@ package org.geysermc.geyser.level; +import net.kyori.adventure.text.Component; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.SoundUtils; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.JukeboxPlayable; -public record JukeboxSong(String soundEvent, String description) { +public record JukeboxSong(String soundEvent, Component description) { public static JukeboxSong read(RegistryEntryContext context) { NbtMap data = context.data(); String soundEvent = SoundUtils.readSoundEvent(data, "jukebox song " + context.id()); - String description = MessageTranslator.deserializeDescription(context.session(), data); + Component description = MessageTranslator.componentFromNbtTag(data.get("description")); return new JukeboxSong(soundEvent, description); } + + public static JukeboxSong fromJukeboxPlayableSong(JukeboxPlayable.JukeboxSong song) { + return new JukeboxSong(song.soundEvent().getName(), song.description()); + } } 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 b05a62894be..634d563001b 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -56,6 +56,8 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.netty.channel.raknet.RakChildChannel; import org.cloudburstmc.netty.handler.codec.raknet.common.RakSessionCodec; import org.cloudburstmc.protocol.bedrock.BedrockDisconnectReasons; @@ -77,6 +79,7 @@ import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType; import org.cloudburstmc.protocol.bedrock.data.definitions.DimensionDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.data.inventory.CreativeItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.CraftingRecipeData; import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket; @@ -151,7 +154,10 @@ import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe; import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.tooltip.TooltipContext; +import org.geysermc.geyser.item.tooltip.TooltipProviders; import org.geysermc.geyser.item.type.BlockItem; +import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.level.BedrockDimension; import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.physics.CollisionManager; @@ -855,7 +861,7 @@ public void connect() { upstream.sendPacket(cameraPresetsPacket); CreativeContentPacket creativePacket = new CreativeContentPacket(); - creativePacket.getContents().addAll(this.itemMappings.getCreativeItems()); + creativePacket.getContents().addAll(addCreativeModeTooltips(this.itemMappings.getCreativeItems())); creativePacket.getGroups().addAll(this.itemMappings.getCreativeItemGroups()); upstream.sendPacket(creativePacket); @@ -2429,4 +2435,33 @@ private void softEnumPacket(String name, SoftEnumUpdateType type, String enums) packet.setSoftEnum(new CommandEnumData(name, Collections.singletonMap(enums, Collections.emptySet()), true)); sendUpstreamPacket(packet); } + + private List addCreativeModeTooltips(List items) { + List withTooltips = new ArrayList<>(); + for (CreativeItemData item : items) { + Item javaItem = Registries.JAVA_ITEM_IDENTIFIERS.get(item.getItem().getDefinition().getIdentifier()); + CreativeItemData withTooltip = item; + + if (javaItem != null) { + List tooltips = new ArrayList<>(); + TooltipProviders.addTooltips(TooltipContext.createForCreativeMenu(javaItem), line -> tooltips.add(MessageTranslator.convertMessage(this, line))); + if (!tooltips.isEmpty()) { + NbtMapBuilder tooltipNbt = NbtMap.builder() + .putCompound("display", NbtMap.builder() + .putList("Lore", NbtType.STRING, tooltips) + .build()); + if (item.getItem().getTag() != null) { + tooltipNbt.putAll(item.getItem().getTag()); + } + withTooltip = new CreativeItemData(item.getItem().toBuilder().tag(tooltipNbt.build()).build(), item.getNetId(), item.getGroupId()); + } + } else { + // Happens for all bedrock items whose identifier does not directly match its Java counterpart + // There's quite a bit of those, but usually the tooltip for them is just empty + geyser.getLogger().debug("Not adding tooltips for item " + item.getItem().getDefinition().getIdentifier() + " because there is no Java item for it!"); + } + withTooltips.add(withTooltip); + } + return withTooltips; + } } 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 6e55096000b..45e34b032bc 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 @@ -73,15 +73,15 @@ * Crafted as of 1.20.5 for easy "add new registry" functionality in the future. */ public final class RegistryCache { - private static final Map, Map> DEFAULTS; - private static final Map, RegistryLoader> READERS = new HashMap<>(); + private static final Map, Map> DEFAULTS; + private static final Map, RegistryLoader> READERS = new HashMap<>(); static { register(JavaRegistries.CHAT_TYPE, ChatDecoration::readChatType); register(JavaRegistries.DIMENSION_TYPE, JavaDimension::read); register(JavaRegistries.BIOME, BiomeTranslator::loadServerBiome); register(JavaRegistries.ENCHANTMENT, Enchantment::read); - register(JavaRegistries.BANNER_PATTERN, context -> BannerPattern.getByJavaIdentifier(context.id())); + register(JavaRegistries.BANNER_PATTERN, BannerPattern::read); register(JavaRegistries.INSTRUMENT, GeyserInstrument::read); register(JavaRegistries.JUKEBOX_SONG, JukeboxSong::read); register(JavaRegistries.PAINTING_VARIANT, context -> PaintingType.getByName(context.id())); @@ -101,7 +101,7 @@ public final class RegistryCache { // Load from MCProtocolLib's classloader NbtMap tag = MinecraftProtocol.loadNetworkCodec(); - Map, Map> defaults = new HashMap<>(); + Map, Map> defaults = new HashMap<>(); // Don't create a keySet - no need to create the cached object in HashMap if we don't use it again READERS.forEach((key, $) -> { List rawValues = tag.getCompound(key.registryKey().asString()).getList("value", NbtType.COMPOUND); @@ -118,12 +118,12 @@ public final class RegistryCache { } private final GeyserSession session; - private final Reference2ObjectMap, JavaRegistry> registries; + private final Reference2ObjectMap, JavaRegistry> registries; public RegistryCache(GeyserSession session) { this.session = session; this.registries = new Reference2ObjectOpenHashMap<>(READERS.size()); - for (JavaRegistryKey registry : READERS.keySet()) { + for (JavaRegistryKey registry : READERS.keySet()) { registries.put(registry, new SimpleJavaRegistry<>()); } } @@ -132,7 +132,7 @@ public RegistryCache(GeyserSession session) { * Loads a registry in, if we are tracking it. */ public void load(ClientboundRegistryDataPacket packet) { - JavaRegistryKey registryKey = JavaRegistries.fromKey(packet.getRegistry()); + JavaRegistryKey registryKey = JavaRegistries.fromKey(packet.getRegistry()); if (registryKey != null) { // Java generic mess - we're sure we're putting the current readers for the correct registry types in the READERS map, so we use raw objects here to let it compile RegistryLoader reader = READERS.get(registryKey); @@ -150,7 +150,7 @@ public void load(ClientboundRegistryDataPacket packet) { } } - public JavaRegistry registry(JavaRegistryKey registryKey) { + public JavaRegistry registry(JavaRegistryKey registryKey) { if (!registries.containsKey(registryKey)) { throw new IllegalArgumentException("The given registry is not data-driven"); } @@ -162,7 +162,7 @@ public JavaRegistry registry(JavaRegistryKey registryKey) { * @param reader converts the RegistryEntry NBT into an object. Should never return null, rather return a default value! * @param the class that represents these entries. */ - private static void register(JavaRegistryKey registryKey, RegistryReader reader) { + private static void register(JavaRegistryKey registryKey, RegistryReader reader) { register(registryKey, (session, registry, entries) -> { Map localRegistry = null; @@ -199,7 +199,7 @@ private static void register(JavaRegistryKey registryKey, RegistryReader< }); } - private static void register(JavaRegistryKey registryKey, RegistryLoader reader) { + private static void register(JavaRegistryKey registryKey, RegistryLoader reader) { READERS.put(registryKey, reader); } diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index 57ce7ecbfc3..cd141d70ba0 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -69,7 +69,7 @@ public void loadPacket(ClientboundUpdateTagsPacket packet) { this.tags.clear(); for (Key registryKey : allTags.keySet()) { - JavaRegistryKey registry = JavaRegistries.fromKey(registryKey); + JavaRegistryKey registry = JavaRegistries.fromKey(registryKey); if (registry == null) { logger.debug("Not loading tags for registry " + registryKey + " (registry not listed in JavaRegistries)"); continue; @@ -98,7 +98,7 @@ public void loadPacket(ClientboundUpdateTagsPacket packet) { } } - private void loadTags(Map packetTags, JavaRegistryKey registry, boolean sort) { + private void loadTags(Map packetTags, JavaRegistryKey registry, boolean sort) { for (Map.Entry tag : packetTags.entrySet()) { int[] value = tag.getValue(); if (sort) { @@ -113,6 +113,13 @@ public boolean is(Tag tag, T object) { return contains(getRaw(tag), tag.registry().networkId(session, object)); } + /** + * @return true if the given tag is present and contains the given Java ID + */ + public boolean is(Tag tag, int id) { + return contains(getRaw(tag), id); + } + /** * @return true if the item tag is present and contains this item stack's Java ID. */ @@ -120,6 +127,7 @@ public boolean is(Tag tag, GeyserItemStack itemStack) { return is(tag, itemStack.asItem()); } + /** * @return true if the specified network ID is in the given holder set. */ @@ -134,7 +142,7 @@ public boolean is(@Nullable GeyserHolderSet holderSet, @Nullable T object * Accessible via the {@link #isItem(HolderSet, Item)} method. * @return true if the specified network ID is in the given {@link HolderSet} set. */ - private boolean is(@Nullable HolderSet holderSet, @NonNull JavaRegistryKey registry, int id) { + private boolean is(@Nullable HolderSet holderSet, @NonNull JavaRegistryKey registry, int id) { if (holderSet == null) { return false; } @@ -172,7 +180,7 @@ public int[] getRaw(Tag tag) { /** * Maps a raw array of network IDs to their respective objects. */ - public static List mapRawArray(GeyserSession session, int[] array, JavaRegistryKey registry) { + public static List mapRawArray(GeyserSession session, int[] array, JavaRegistryKey registry) { return Arrays.stream(array).mapToObj(i -> registry.value(session, i)).toList(); } 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 e6c6a05de4d..1001e5847f2 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 @@ import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; import org.cloudburstmc.protocol.bedrock.data.TrimPattern; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; @@ -34,6 +35,7 @@ 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.BedrockBannerPattern; import org.geysermc.geyser.inventory.item.GeyserInstrument; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.item.type.Item; @@ -48,7 +50,12 @@ import org.geysermc.geyser.session.dialog.Dialog; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.PaintingVariant; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.InstrumentComponent; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.JukeboxPlayable; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; @@ -56,20 +63,21 @@ import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; /** * 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. */ public class JavaRegistries { - private static final List> VALUES = new ArrayList<>(); + private static final List> VALUES = new ArrayList<>(); - public static final JavaRegistryKey BLOCK = createHardcoded("block", BlockRegistries.JAVA_BLOCKS, + public static final JavaRegistryKey BLOCK = createHardcoded("block", BlockRegistries.JAVA_BLOCKS, Block::javaId, Block::javaIdentifier, key -> BlockRegistries.JAVA_BLOCKS.get().stream() .filter(block -> block.javaIdentifier().equals(key)) .findFirst()); - public static final JavaRegistryKey ITEM = createHardcoded("item", Registries.JAVA_ITEMS, + public static final JavaRegistryKey ITEM = createHardcoded("item", Registries.JAVA_ITEMS, Item::javaId, Item::javaKey, key -> Optional.ofNullable(Registries.JAVA_ITEM_IDENTIFIERS.get(key.asString()))); - public static JavaRegistryKey ENTITY_TYPE = createHardcoded("entity_type", Arrays.asList(EntityType.values()), EntityType::ordinal, + public static JavaRegistryKey ENTITY_TYPE = createHardcoded("entity_type", Arrays.asList(EntityType.values()), EntityType::ordinal, type -> MinecraftKey.key(type.name().toLowerCase(Locale.ROOT)), key -> { try { return Optional.of(EntityType.valueOf(key.value().toUpperCase(Locale.ROOT))); @@ -78,51 +86,63 @@ public class JavaRegistries { } }); - public static final JavaRegistryKey CHAT_TYPE = create("chat_type"); - public static final JavaRegistryKey DIMENSION_TYPE = create("dimension_type"); - public static final JavaRegistryKey BIOME = create("worldgen/biome"); - public static final JavaRegistryKey ENCHANTMENT = create("enchantment"); - public static final JavaRegistryKey BANNER_PATTERN = create("banner_pattern"); - public static final JavaRegistryKey INSTRUMENT = create("instrument"); - public static final JavaRegistryKey JUKEBOX_SONG = create("jukebox_song"); - public static final JavaRegistryKey PAINTING_VARIANT = create("painting_variant"); - public static final JavaRegistryKey TRIM_MATERIAL = create("trim_material"); - public static final JavaRegistryKey TRIM_PATTERN = create("trim_pattern"); - public static final JavaRegistryKey DAMAGE_TYPE = create("damage_type"); - public static final JavaRegistryKey DIALOG = create("dialog"); - - public static final JavaRegistryKey CAT_VARIANT = create("cat_variant"); - public static final JavaRegistryKey FROG_VARIANT = create("frog_variant"); - public static final JavaRegistryKey WOLF_VARIANT = create("wolf_variant"); - public static final JavaRegistryKey WOLF_SOUND_VARIANT = create("wolf_sound_variant"); - - public static final JavaRegistryKey PIG_VARIANT = create("pig_variant"); - public static final JavaRegistryKey COW_VARIANT = create("cow_variant"); - public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant"); - - private static JavaRegistryKey create(String key, JavaRegistryKey.RegistryLookup registryLookup) { - JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), registryLookup); + public static final JavaRegistryKey CHAT_TYPE = create("chat_type", Function.identity()); + public static final JavaRegistryKey DIMENSION_TYPE = create("dimension_type"); + public static final JavaRegistryKey BIOME = create("worldgen/biome"); + public static final JavaRegistryKey ENCHANTMENT = create("enchantment"); + + public static final JavaRegistryKey BANNER_PATTERN = create("banner_pattern", + pattern -> new BannerPattern(BedrockBannerPattern.BASE, pattern.getTranslationKey())); + public static final JavaRegistryKey INSTRUMENT = create("instrument", GeyserInstrument::fromInstrument); + public static final JavaRegistryKey JUKEBOX_SONG = create("jukebox_song", JukeboxSong::fromJukeboxPlayableSong); + + public static final JavaRegistryKey PAINTING_VARIANT = create("painting_variant", variant -> PaintingType.KEBAB); // Fallback variant for paintings on Java + public static final JavaRegistryKey TRIM_MATERIAL = create("trim_material"/*, material -> new TrimMaterial()*/); // TODO + public static final JavaRegistryKey TRIM_PATTERN = create("trim_pattern"/*, pattern -> new TrimPattern()*/); // TODO + public static final JavaRegistryKey DAMAGE_TYPE = create("damage_type"); + public static final JavaRegistryKey DIALOG = create("dialog", + (session, registry, map) -> Dialog.readDialogFromNbt(session, map, dialog -> registry.networkId(session, dialog))); + + public static final JavaRegistryKey CAT_VARIANT = create("cat_variant"); + public static final JavaRegistryKey FROG_VARIANT = create("frog_variant"); + public static final JavaRegistryKey WOLF_VARIANT = create("wolf_variant"); + public static final JavaRegistryKey WOLF_SOUND_VARIANT = create("wolf_sound_variant"); + + public static final JavaRegistryKey PIG_VARIANT = create("pig_variant"); + public static final JavaRegistryKey COW_VARIANT = create("cow_variant"); + public static final JavaRegistryKey CHICKEN_VARIANT = create("chicken_variant"); + + private static JavaRegistryKey create(String key, JavaRegistryKey.RegistryLookup registryLookup, JavaRegistryKey.HolderMapper mapper) { + JavaRegistryKey registry = new JavaRegistryKey<>(MinecraftKey.key(key), registryLookup, mapper); VALUES.add(registry); return registry; } - private static JavaRegistryKey createHardcoded(String key, ListRegistry registry, RegistryNetworkMapper networkSerializer, + private static JavaRegistryKey createHardcoded(String key, ListRegistry registry, RegistryNetworkMapper networkSerializer, RegistryObjectIdentifierMapper objectIdentifierMapper, RegistryIdentifierObjectMapper identifierObjectMapper) { return createHardcoded(key, registry.get(), networkSerializer, objectIdentifierMapper, identifierObjectMapper); } - private static JavaRegistryKey createHardcoded(String key, List registry, RegistryNetworkMapper networkSerializer, + private static JavaRegistryKey createHardcoded(String key, List registry, RegistryNetworkMapper networkSerializer, RegistryObjectIdentifierMapper objectIdentifierMapper, RegistryIdentifierObjectMapper identifierObjectMapper) { - return create(key, new HardcodedLookup<>(registry, networkSerializer, objectIdentifierMapper, identifierObjectMapper)); + return create(key, new HardcodedLookup<>(registry, networkSerializer, objectIdentifierMapper, identifierObjectMapper), null); } - private static JavaRegistryKey create(String key) { - return create(key, new RegistryCacheLookup<>()); + private static JavaRegistryKey create(String key, JavaRegistryKey.HolderMapper mapper) { + return create(key, new RegistryCacheLookup<>(), mapper); + } + + private static JavaRegistryKey create(String key, Function mapper) { + return create(key, new RegistryCacheLookup<>(), (session, registry, mcpl) -> mapper.apply(mcpl)); + } + + private static JavaRegistryKey create(String key) { + return create(key, new RegistryCacheLookup<>(), null); } @Nullable - public static JavaRegistryKey fromKey(Key registryKey) { - for (JavaRegistryKey registry : VALUES) { + public static JavaRegistryKey fromKey(Key registryKey) { + for (JavaRegistryKey registry : VALUES) { if (registry.registryKey().equals(registryKey)) { return registry; } @@ -152,19 +172,19 @@ private record HardcodedLookup(List registry, RegistryNetworkMapper net RegistryIdentifierObjectMapper identifierObjectMapper) implements JavaRegistryKey.RegistryLookup { @Override - public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, int networkId) { + public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, int networkId) { return Optional.ofNullable(registry.get(networkId)) .map(value -> new RegistryEntryData<>(networkId, Objects.requireNonNull(objectIdentifierMapper.get(value)), value)); } @Override - public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, Key key) { + public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, @NotNull Key key) { Optional object = identifierObjectMapper.get(key); return object.map(value -> new RegistryEntryData<>(networkMapper.get(value), key, value)); } @Override - public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, T object) { + public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, @NotNull T object) { int id = networkMapper.get(object); return Optional.ofNullable(registry.get(id)) .map(value -> new RegistryEntryData<>(id, Objects.requireNonNull(objectIdentifierMapper.get(value)), value)); @@ -174,21 +194,21 @@ public Optional> entry(GeyserSession session, JavaRegistryK private static class RegistryCacheLookup implements JavaRegistryKey.RegistryLookup { @Override - public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, int networkId) { + public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, int networkId) { return Optional.ofNullable(registry(session, registryKey).entryById(networkId)); } @Override - public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, Key key) { + public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, @NotNull Key key) { return Optional.ofNullable(registry(session, registryKey).entryByKey(key)); } @Override - public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, T object) { + public Optional> entry(GeyserSession session, JavaRegistryKey registryKey, @NotNull T object) { return Optional.ofNullable(registry(session, registryKey).entryByValue(object)); } - private JavaRegistry registry(GeyserSession session, JavaRegistryKey key) { + private JavaRegistry registry(GeyserSession session, JavaRegistryKey key) { return session.getRegistryCache().registry(key); } } 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 47d532603bb..c0939dcab8c 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 org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.Holder; import java.util.Optional; @@ -43,28 +44,30 @@ * * @param registryKey the registry key, as it appears on Java. * @param lookup an implementation of {@link RegistryLookup} that converts an object in this registry to its respective network ID or key, and back. + * @param holderMapper a function that maps the MCPL counterpart of an object in this registry to its Geyser counterpart. Can be null. * @param the object type this registry holds. + * @param the MCPL counterpart of the type this registry holds. Can be {@code ?} when {@code holderMapper} is null. */ -public record JavaRegistryKey(Key registryKey, RegistryLookup lookup) { +public record JavaRegistryKey(Key registryKey, RegistryLookup lookup, @Nullable HolderMapper holderMapper) { /** * Converts an object to its network ID, or -1 if it is not registered. */ - public int networkId(GeyserSession session, T object) { + public int networkId(GeyserSession session, @Nullable T object) { return entry(session, object).map(RegistryEntryData::id).orElse(-1); } /** * Converts a registered key to its network ID, or -1 if it is not registered. */ - public int networkId(GeyserSession session, Key key) { + public int networkId(GeyserSession session, @Nullable Key key) { return entry(session, key).map(RegistryEntryData::id).orElse(-1); } /** * Converts an object to its registered key, or null if it is not registered. */ - public @Nullable Key key(GeyserSession session, T object) { + public @Nullable Key key(GeyserSession session, @Nullable T object) { return entry(session, object).map(RegistryEntryData::key).orElse(null); } @@ -85,11 +88,25 @@ public int networkId(GeyserSession session, Key key) { /** * Converts a key to an object in this registry, or null if it is not registered. */ - public @Nullable T value(GeyserSession session, Key key) { + public @Nullable T value(GeyserSession session, @Nullable Key key) { return entry(session, key).map(RegistryEntryData::data).orElse(null); } - private Optional> entry(GeyserSession session, T object) { + public @Nullable T value(GeyserSession session, @Nullable Holder holder) { + if (holder == null) { + return null; + } else if (holder.isId()) { + return value(session, holder.id()); + } else if (holderMapper == null) { + throw new IllegalArgumentException("Tried to map a custom MCPL holder for a registry that does not have a holder mapper (" + this + ")"); + } + return holderMapper.map(session, this, holder.custom()); + } + + private Optional> entry(GeyserSession session, @Nullable T object) { + if (object == null) { + return Optional.empty(); + } return lookup.entry(session, this, object); } @@ -97,7 +114,10 @@ private Optional> entry(GeyserSession session, int networkI return lookup.entry(session, this, networkId); } - private Optional> entry(GeyserSession session, Key key) { + private Optional> entry(GeyserSession session, @Nullable Key key) { + if (key == null) { + return Optional.empty(); + } return lookup.entry(session, this, key); } @@ -106,11 +126,17 @@ private Optional> entry(GeyserSession session, Key key) { */ public interface RegistryLookup { - Optional> entry(GeyserSession session, JavaRegistryKey registry, int networkId); + Optional> entry(GeyserSession session, JavaRegistryKey registry, int networkId); + + Optional> entry(GeyserSession session, JavaRegistryKey registry, @NonNull Key key); + + Optional> entry(GeyserSession session, JavaRegistryKey registry, @NonNull T object); + } - Optional> entry(GeyserSession session, JavaRegistryKey registry, Key key); + @FunctionalInterface + public interface HolderMapper { - Optional> entry(GeyserSession session, JavaRegistryKey registry, T object); + T map(GeyserSession session, JavaRegistryKey registry, MCPL mcpl); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java b/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java index 00eaf1f44da..c4ba6d588fe 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/tags/GeyserHolderSet.java @@ -58,28 +58,28 @@ @Data public final class GeyserHolderSet { - private final JavaRegistryKey registry; + private final JavaRegistryKey registry; private final @Nullable Tag tag; private final int @Nullable [] holders; private final @Nullable List inline; - private GeyserHolderSet(JavaRegistryKey registry) { + private GeyserHolderSet(JavaRegistryKey registry) { this(registry, IntArrays.EMPTY_ARRAY); } - public GeyserHolderSet(JavaRegistryKey registry, int @NonNull [] holders) { + public GeyserHolderSet(JavaRegistryKey registry, int @NonNull [] holders) { this(registry, null, holders, null); } - public GeyserHolderSet(JavaRegistryKey registry, @NonNull Tag tagId) { + public GeyserHolderSet(JavaRegistryKey registry, @NonNull Tag tagId) { this(registry, tagId, null, null); } - public GeyserHolderSet(JavaRegistryKey registry, @NonNull List inline) { + public GeyserHolderSet(JavaRegistryKey registry, @NonNull List inline) { this(registry, null, null, inline); } - private GeyserHolderSet(JavaRegistryKey registry, @Nullable Tag tag, int @Nullable [] holders, @Nullable List inline) { + private GeyserHolderSet(JavaRegistryKey registry, @Nullable Tag tag, int @Nullable [] holders, @Nullable List inline) { this.registry = registry; this.tag = tag; this.holders = holders; @@ -89,7 +89,7 @@ private GeyserHolderSet(JavaRegistryKey registry, @Nullable Tag tag, int @ /** * Constructs a {@link GeyserHolderSet} from a MCPL HolderSet. */ - public static GeyserHolderSet fromHolderSet(JavaRegistryKey registry, @NonNull HolderSet holderSet) { + public static GeyserHolderSet fromHolderSet(JavaRegistryKey registry, @NonNull HolderSet holderSet) { // MCPL HolderSets don't have to support inline elements... for now (TODO CHECK ME) Tag tag = holderSet.getLocation() == null ? null : new Tag<>(registry, holderSet.getLocation()); return new GeyserHolderSet<>(registry, tag, holderSet.getHolders(), null); @@ -135,7 +135,7 @@ public int[] resolveRaw(TagCache tagCache) { * @param registry the registry the HolderSet contains IDs from. * @param holderSet the HolderSet as a NBT object. */ - public static GeyserHolderSet readHolderSet(GeyserSession session, JavaRegistryKey registry, @Nullable Object holderSet) { + public static GeyserHolderSet readHolderSet(GeyserSession session, JavaRegistryKey registry, @Nullable Object holderSet) { return readHolderSet(registry, holderSet, key -> registry.networkId(session, key)); } @@ -146,7 +146,7 @@ public static GeyserHolderSet readHolderSet(GeyserSession session, JavaRe * @param holderSet the HolderSet as a NBT object. * @param idMapper a function that maps a key in this registry to its respective network ID. */ - public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, @Nullable Object holderSet, ToIntFunction idMapper) { + public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, @Nullable Object holderSet, ToIntFunction idMapper) { return readHolderSet(registry, holderSet, idMapper, null); } @@ -159,7 +159,7 @@ public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, * @param idMapper a function that maps a key in this registry to its respective network ID. * @param reader a function that reads an object in the HolderSet's registry, serialised as NBT. When {@code null}, this method doesn't support reading inline HolderSets. */ - public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, @Nullable Object holderSet, + public static GeyserHolderSet readHolderSet(JavaRegistryKey registry, @Nullable Object holderSet, ToIntFunction idMapper, @Nullable Function reader) { if (holderSet == null) { return new GeyserHolderSet<>(registry); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/tags/Tag.java b/core/src/main/java/org/geysermc/geyser/session/cache/tags/Tag.java index 276e30b9f87..276c51dadc6 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/tags/Tag.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/tags/Tag.java @@ -31,5 +31,5 @@ /** * A tag in any of the registries that tags are loaded for by Geyser. */ -public record Tag(JavaRegistryKey registry, Key tag) { +public record Tag(JavaRegistryKey registry, Key tag) { } diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java b/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java index e7eb7d82fdc..a4ec20a37ee 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/Dialog.java @@ -45,7 +45,6 @@ import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.MinecraftKey; -import org.geysermc.mcprotocollib.protocol.data.game.Holder; import java.util.ArrayList; import java.util.List; @@ -181,14 +180,6 @@ public static Dialog readDialogFromNbt(GeyserSession session, NbtMap map, IdGett throw new UnsupportedOperationException("Unable to read unknown dialog type " + type + "!"); } - public static Dialog getDialogFromHolder(GeyserSession session, Holder holder) { - if (holder.isId()) { - return Objects.requireNonNull(JavaRegistries.DIALOG.value(session, holder.id())); - } else { - return Dialog.readDialogFromNbt(session, holder.custom(), key -> JavaRegistries.DIALOG.networkId(session, key)); - } - } - public static Dialog getDialogFromKey(GeyserSession session, Key key) { return Objects.requireNonNull(JavaRegistries.DIALOG.value(session, key)); } diff --git a/core/src/main/java/org/geysermc/geyser/session/dialog/DialogManager.java b/core/src/main/java/org/geysermc/geyser/session/dialog/DialogManager.java index e61ef53a59c..d43033d580c 100644 --- a/core/src/main/java/org/geysermc/geyser/session/dialog/DialogManager.java +++ b/core/src/main/java/org/geysermc/geyser/session/dialog/DialogManager.java @@ -30,6 +30,7 @@ import net.kyori.adventure.key.Key; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.mcprotocollib.protocol.data.game.Holder; /** @@ -50,7 +51,7 @@ public void openDialog(Key dialog) { } public void openDialog(Holder dialog) { - openDialog(Dialog.getDialogFromHolder(session, dialog)); + openDialog(JavaRegistries.DIALOG.value(session, dialog)); } /** diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java index 74ab1e7e6af..495577a5a25 100644 --- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java @@ -38,8 +38,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -47,10 +49,14 @@ public class MinecraftLocale { public static final Map> LOCALE_MAPPINGS = new HashMap<>(); + private static final List REMOVED_KEYS = new ArrayList<>(); + private static final Map REPLACED_KEYS = new HashMap<>(); + // Check instance availability to avoid exception during testing private static final boolean IN_INSTANCE = GeyserImpl.getInstance() != null; private static final Path LOCALE_FOLDER = (IN_INSTANCE) ? GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales") : null; + private static final Path DEPRECATED = (IN_INSTANCE) ? getPath("deprecated") : null; static { if (IN_INSTANCE) { @@ -75,6 +81,18 @@ public static void ensureEN_US() { })); } + public static void downloadDeprecations() { + if (!loadDeprecations()) { + AssetUtils.addTask(!Files.exists(DEPRECATED), new AssetUtils.ClientJarTask("assets/minecraft/lang/deprecated.json", + stream -> AssetUtils.saveFile(DEPRECATED, stream), + () -> { + if (!loadDeprecations()) { + GeyserImpl.getInstance().getLogger().warning("Failed to load deprecated locale file: it doesn't exist?"); + } + })); + } + } + /** * Downloads a locale from Mojang if it's not already loaded * @@ -183,6 +201,29 @@ private static boolean loadLocale(String locale) { } } + private static boolean loadDeprecations() { + if (Files.exists(DEPRECATED) && Files.isReadable(DEPRECATED)) { + try (InputStream deprecatedStream = Files.newInputStream(DEPRECATED, StandardOpenOption.READ)) { + JsonNode deprecatedObject = GeyserImpl.JSON_MAPPER.readTree(deprecatedStream); + + Iterator removed = deprecatedObject.get("removed").elements(); + while (removed.hasNext()) { + REMOVED_KEYS.add(removed.next().asText()); + } + + Iterator> replaced = deprecatedObject.get("renamed").fields(); + while (replaced.hasNext()) { + Map.Entry replacedString = replaced.next(); + REPLACED_KEYS.put(replacedString.getKey(), replacedString.getValue().asText()); + } + return true; + } catch (IOException exception) { + throw new AssertionError("Failed to read deprecated locale file", exception); + } + } + return false; + } + /** * Load and parse a json lang file. * @@ -201,7 +242,11 @@ public static Map parseLangFile(Path localeFile, String locale) Map langMap = new HashMap<>(); while (localeIterator.hasNext()) { Map.Entry entry = localeIterator.next(); - langMap.put(entry.getKey(), entry.getValue().asText()); + if (REMOVED_KEYS.contains(entry.getKey())) { + continue; + } + + langMap.put(REPLACED_KEYS.getOrDefault(entry.getKey(), entry.getKey()), entry.getValue().asText()); } return langMap; } catch (FileNotFoundException e){ 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 96ee267684d..0f8430caf02 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 @@ -44,6 +44,7 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.SlotType; import org.geysermc.geyser.inventory.item.BannerPattern; +import org.geysermc.geyser.inventory.item.BedrockBannerPattern; import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; import org.geysermc.geyser.item.type.BannerItem; import org.geysermc.geyser.item.type.DyeItem; @@ -100,7 +101,7 @@ public ItemStackResponse translateSpecialRequest(GeyserSession session, Containe String bedrockPattern = ((CraftLoomAction) headerData).getPatternId(); - BannerPattern requestedPattern = BannerPattern.getByBedrockIdentifier(bedrockPattern); + BannerPattern requestedPattern = BannerPattern.fromBedrockPattern(session, BedrockBannerPattern.getByBedrockIdentifier(bedrockPattern)); if (requestedPattern == null) { GeyserImpl.getInstance().getLogger().warning("Unknown Bedrock pattern id: " + bedrockPattern); return rejectRequest(request); 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 412e9982941..a8dc68b4aff 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -25,11 +25,9 @@ package org.geysermc.geyser.translator.item; -import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtList; @@ -40,12 +38,13 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.block.custom.CustomBlockData; -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.tooltip.TooltipContext; +import org.geysermc.geyser.item.tooltip.TooltipProviders; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.item.type.PotionItem; import org.geysermc.geyser.level.block.type.Block; @@ -58,64 +57,28 @@ import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.InventoryUtils; -import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.auth.GameProfile; import org.geysermc.mcprotocollib.auth.GameProfile.Texture; import org.geysermc.mcprotocollib.auth.GameProfile.TextureType; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; -import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; -import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate; 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.HolderSet; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.ItemAttributeModifiers; 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.WrittenBookContent; -import java.text.DecimalFormat; import java.util.ArrayList; -import java.util.EnumMap; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; public final class ItemTranslator { - /** - * The order of these slots is their display order on Java Edition clients - */ - private static final EnumMap SLOT_NAMES; - private static final ItemAttributeModifiers.EquipmentSlotGroup[] ARMOR_SLOT_NAMES = new ItemAttributeModifiers.EquipmentSlotGroup[] { - ItemAttributeModifiers.EquipmentSlotGroup.HEAD, - ItemAttributeModifiers.EquipmentSlotGroup.CHEST, - ItemAttributeModifiers.EquipmentSlotGroup.LEGS, - ItemAttributeModifiers.EquipmentSlotGroup.FEET - }; - private static final DecimalFormat ATTRIBUTE_FORMAT = new DecimalFormat("0.#####"); - private static final Key BASE_ATTACK_DAMAGE_ID = MinecraftKey.key("base_attack_damage"); - private static final Key BASE_ATTACK_SPEED_ID = MinecraftKey.key("base_attack_speed"); - - static { - // Maps slot groups to their respective translation names, ordered in their Java edition order in the item tooltip - SLOT_NAMES = new EnumMap<>(ItemAttributeModifiers.EquipmentSlotGroup.class); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.ANY, "any"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.MAIN_HAND, "mainhand"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.OFF_HAND, "offhand"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.HAND, "hand"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.FEET, "feet"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.LEGS, "legs"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.CHEST, "chest"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.HEAD, "head"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.ARMOR, "armor"); - SLOT_NAMES.put(ItemAttributeModifiers.EquipmentSlotGroup.BODY, "body"); - } - + // TODO this should be done over default item components private final static List GLINT_PRESENT = List.of(Items.ENCHANTED_GOLDEN_APPLE, Items.EXPERIENCE_BOTTLE, Items.WRITTEN_BOOK, Items.NETHER_STAR, Items.ENCHANTED_BOOK, Items.END_CRYSTAL); @@ -190,6 +153,7 @@ public static ItemData translateToBedrock(GeyserSession session, @NonNull Geyser // Populates default components that aren't sent over the network DataComponents components = javaItem.gatherComponents(customComponents); + // TODO remove this, is in context now TooltipOptions tooltip = TooltipOptions.fromComponents(components); // Translate item-specific components @@ -207,15 +171,8 @@ public static ItemData translateToBedrock(GeyserSession session, @NonNull Geyser nbtBuilder.setCustomName(customName); } - ItemAttributeModifiers attributeModifiers = components.get(DataComponentTypes.ATTRIBUTE_MODIFIERS); - 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() && !TooltipOptions.hideTooltip(components)) { - addAdvancedTooltips(components, nbtBuilder, javaItem, session.locale()); - } + TooltipProviders.addTooltips(TooltipContext.create(session, javaItem, components), + line -> nbtBuilder.getOrCreateLore().add(MessageTranslator.convertMessageForTooltip(line, session.locale()))); // Add enchantment override. We can't remove it - enchantments would stop showing - but we can add it. if (components.getOrDefault(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, false) && !GLINT_PRESENT.contains(javaItem)) { @@ -257,102 +214,6 @@ public static ItemData translateToBedrock(GeyserSession session, @NonNull Geyser return builder; } - /** - * Bedrock Edition does not see attribute modifiers like Java Edition does, - * so we add them as lore instead. - * - * @param modifiers the attribute modifiers of the ItemStack - * @param language the locale of the player - */ - private static void addAttributeLore(GeyserSession session, ItemAttributeModifiers modifiers, BedrockItemBuilder builder, String language) { - // maps each slot to the modifiers applied when in such slot - Map> slotsToModifiers = new HashMap<>(); - for (ItemAttributeModifiers.Entry entry : modifiers.getModifiers()) { - // convert the modifier tag to a lore entry - String loreEntry = attributeToLore(session, entry.getAttribute(), entry.getModifier(), entry.getDisplay(), language); - if (loreEntry == null) { - continue; // invalid, failed, or hidden - } - - slotsToModifiers.computeIfAbsent(entry.getSlot(), s -> new ArrayList<>()).add(loreEntry); - } - - // iterate through the small array, not the map, so that ordering matches Java Edition - for (var slot : SLOT_NAMES.keySet()) { - List modifierStrings = slotsToModifiers.get(slot); - if (modifierStrings == null || modifierStrings.isEmpty()) { - continue; - } - - // Declare the slot, e.g. "When in Main Hand" - Component slotComponent = Component.text() - .resetStyle() - .color(NamedTextColor.GRAY) - .append(Component.newline(), Component.translatable("item.modifiers." + SLOT_NAMES.get(slot))) - .build(); - builder.getOrCreateLore().add(MessageTranslator.convertMessage(slotComponent, language)); - - // Then list all the modifiers when used in this slot - for (String modifier : modifierStrings) { - builder.getOrCreateLore().add(modifier); - } - } - } - - @Nullable - private static String attributeToLore(GeyserSession session, int attribute, ItemAttributeModifiers.AttributeModifier modifier, - ItemAttributeModifiers.Display display, String language) { - if (display.getType() == ItemAttributeModifiers.DisplayType.HIDDEN) { - return null; - } else if (display.getType() == ItemAttributeModifiers.DisplayType.OVERRIDE) { - return MessageTranslator.convertMessage(Objects.requireNonNull(display.getComponent()) - .colorIfAbsent(NamedTextColor.WHITE) - .decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE), language); - } - - double amount = modifier.getAmount(); - if (amount == 0) { - return null; - } - - String name = AttributeType.Builtin.from(attribute).getIdentifier().asMinimalString(); - // the namespace does not need to be present, but if it is, the java client ignores it as of pre-1.20.5 - - ModifierOperation operation = modifier.getOperation(); - boolean baseModifier = false; - String operationTotal = switch (operation) { - case ADD -> { - if (name.equals("knockback_resistance")) { - amount *= 10; - } - - if (modifier.getId().equals(BASE_ATTACK_DAMAGE_ID)) { - amount += session.getPlayerEntity().attributeOrDefault(GeyserAttributeType.ATTACK_DAMAGE); - baseModifier = true; - } else if (modifier.getId().equals(BASE_ATTACK_SPEED_ID)) { - amount += session.getAttackSpeed(); - baseModifier = true; - } - - yield ATTRIBUTE_FORMAT.format(amount); - } - case ADD_MULTIPLIED_BASE, ADD_MULTIPLIED_TOTAL -> - ATTRIBUTE_FORMAT.format(amount * 100) + "%"; - }; - if (amount > 0 && !baseModifier) { - operationTotal = "+" + operationTotal; - } - - - Component attributeComponent = Component.text() - .resetStyle() - .color(baseModifier ? NamedTextColor.DARK_GREEN : amount > 0 ? NamedTextColor.BLUE : NamedTextColor.RED) - .append(Component.text(operationTotal + " "), Component.translatable("attribute.name." + name)) - .build(); - - return MessageTranslator.convertMessage(attributeComponent, language); - } - private static final List negativeEffectList = List.of( Effect.SLOWNESS, Effect.MINING_FATIGUE, 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 6c290a287d7..f2f21151b74 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 @@ -46,6 +46,7 @@ import org.geysermc.geyser.translator.level.event.LevelEventTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.DimensionUtils; import org.geysermc.geyser.util.SoundUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; @@ -127,7 +128,7 @@ public void translate(GeyserSession session, ClientboundLevelEventPacket packet) textPacket.setPlatformChatId(""); textPacket.setSourceName(null); textPacket.setMessage("record.nowPlaying"); - textPacket.setParameters(Collections.singletonList(jukeboxSong.description())); + textPacket.setParameters(Collections.singletonList(MessageTranslator.convertMessage(session, jukeboxSong.description()))); session.sendUpstreamPacket(textPacket); return; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java index 3361ad539a9..fff089ecaef 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/text/MessageTranslator.java @@ -186,7 +186,7 @@ public static String convertMessage(Component message, String locale) { * @return Parsed and formatted message for bedrock, in gray color */ public static String convertMessageForTooltip(Component message, String locale) { - return RESET + ChatColor.GRAY + convertMessageRaw(message, locale); + return RESET + convertMessageRaw(message, locale); } /** @@ -391,7 +391,7 @@ public static String convertToPlainTextLenient(String message, String locale) { return PlainTextComponentSerializer.plainText().serialize(messageComponent); } - public static void handleChatPacket(GeyserSession session, Component message, Holder chatTypeHolder, Component targetName, Component sender, @Nullable UUID senderUuid) { + public static void handleChatPacket(GeyserSession session, Component message, Holder holder, Component targetName, Component sender, @Nullable UUID senderUuid) { TextPacket textPacket = new TextPacket(); textPacket.setPlatformChatId(""); textPacket.setSourceName(""); @@ -414,7 +414,7 @@ public static void handleChatPacket(GeyserSession session, Component message, Ho textPacket.setNeedsTranslation(false); - ChatType chatType = chatTypeHolder.getOrCompute(session.getRegistryCache().registry(JavaRegistries.CHAT_TYPE)::byId); + ChatType chatType = JavaRegistries.CHAT_TYPE.value(session, holder); if (chatType != null && chatType.chat() != null) { var chat = chatType.chat(); // As of 1.19 - do this to apply all the styling for signed messages @@ -495,24 +495,6 @@ public static String normalizeSpace(String string) { return new String(newChars, 0, count - (whitespacesCount > 0 ? 1 : 0)).trim(); } - /** - * Deserialize an NbtMap with a description text component (usually provided from a registry) into a Bedrock-formatted string. - */ - public static String deserializeDescription(GeyserSession session, NbtMap tag) { - Object description = tag.get("description"); - Component parsed = componentFromNbtTag(description); - return convertMessage(session, parsed); - } - - /** - * Deserialize an NbtMap with a description text component (usually provided from a registry) into a Bedrock-formatted string. - */ - public static String deserializeDescriptionForTooltip(GeyserSession session, NbtMap tag) { - Object description = tag.get("description"); - Component parsed = componentFromNbtTag(description); - return convertMessageForTooltip(parsed, session.locale()); - } - public static @Nullable String convertFromNullableNbtTag(GeyserSession session, @Nullable Object nbtTag) { if (nbtTag == null) { return null;