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 8d1d92f6c..468b88e87 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 @@ -27,13 +27,15 @@ package org.geysermc.geyser.item.enchantment; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; -import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.item.BedrockEnchantment; import org.geysermc.geyser.session.cache.tags.EnchantmentTag; import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; -import java.util.*; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * @param description only populated if {@link #bedrockEnchantment()} is not null. @@ -59,7 +61,7 @@ public record Enchantment(String identifier, String exclusiveSet = data.getString("exclusive_set", null); EnchantmentTag exclusiveSetTag = exclusiveSet == null ? null : EnchantmentTag.ALL_ENCHANTMENT_TAGS.get(exclusiveSet.substring(1)); BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(entry.getId()); - String description = bedrockEnchantment == null ? readDescription(data) : null; + String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null; return new Enchantment(entry.getId(), effects, ItemTag.ALL_ITEM_TAGS.get(supportedItems), maxLevel, description, anvilCost, exclusiveSetTag, bedrockEnchantment); @@ -74,14 +76,4 @@ public record Enchantment(String identifier, } return Set.copyOf(components); // Also ensures any empty sets are consolidated } - - private static String readDescription(NbtMap tag) { - NbtMap description = tag.getCompound("description"); - String translate = description.getString("translate", null); - if (translate == null) { - GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag); - return ""; - } - return translate; - } } diff --git a/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java new file mode 100644 index 000000000..fd6ce693d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/level/JukeboxSong.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.level; + +import org.cloudburstmc.nbt.NbtMap; +import org.geysermc.geyser.translator.text.MessageTranslator; +import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry; + +public record JukeboxSong(String soundEvent, String description) { + + public static JukeboxSong read(RegistryEntry entry) { + NbtMap data = entry.getData(); + String soundEvent = data.getString("sound_event"); + String description = MessageTranslator.deserializeDescription(data); + return new JukeboxSong(soundEvent, description); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java index fe970ee2b..266a0a418 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/RegistryCache.java @@ -40,6 +40,7 @@ import org.geysermc.geyser.inventory.item.BannerPattern; import org.geysermc.geyser.inventory.recipe.TrimRecipe; import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.level.JavaDimension; +import org.geysermc.geyser.level.JukeboxSong; import org.geysermc.geyser.level.PaintingType; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.registry.JavaRegistry; @@ -76,6 +77,7 @@ public final class RegistryCache { register("chat_type", cache -> cache.chatTypes, ($, entry) -> TextDecoration.readChatType(entry)); register("dimension_type", cache -> cache.dimensions, ($, entry) -> JavaDimension.read(entry)); register("enchantment", cache -> cache.enchantments, ($, entry) -> Enchantment.read(entry)); + register("jukebox_song", cache -> cache.jukeboxSongs, ($, entry) -> JukeboxSong.read(entry)); register("painting_variant", cache -> cache.paintings, ($, entry) -> PaintingType.getByName(entry.getId())); register("trim_material", cache -> cache.trimMaterials, TrimRecipe::readTrimMaterial); register("trim_pattern", cache -> cache.trimPatterns, TrimRecipe::readTrimPattern); @@ -115,6 +117,7 @@ public final class RegistryCache { */ private final JavaRegistry dimensions = new SimpleJavaRegistry<>(); private final JavaRegistry enchantments = new SimpleJavaRegistry<>(); + private final JavaRegistry jukeboxSongs = new SimpleJavaRegistry<>(); private final JavaRegistry paintings = new SimpleJavaRegistry<>(); private final JavaRegistry trimMaterials = new SimpleJavaRegistry<>(); private final JavaRegistry trimPatterns = new SimpleJavaRegistry<>(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java index 44ec7a6b9..8eb715560 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java @@ -28,8 +28,10 @@ package org.geysermc.geyser.session.cache; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMaps; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; import lombok.Setter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; import org.geysermc.geyser.scoreboard.Scoreboard; @@ -39,6 +41,7 @@ import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.mcprotocollib.protocol.data.game.setting.Difficulty; import java.util.Iterator; +import java.util.Map; public final class WorldCache { private final GeyserSession session; @@ -61,6 +64,8 @@ public final class WorldCache { private int currentSequence; private final Object2IntMap unverifiedPredictions = new Object2IntOpenHashMap<>(1); + private final Map activeRecords = new Object2ObjectOpenHashMap<>(1); // Assume the average player won't be listening to many records + @Getter @Setter private boolean editingSignOnFront; @@ -185,4 +190,15 @@ public final class WorldCache { } } } + + public void addActiveRecord(Vector3i pos, String bedrockPlaySound) { + this.activeRecords.put(pos, bedrockPlaySound); + } + + // Implementation note: positions aren't removed unless the server calls, but this seems to match 1.21 Java + // client behavior. + @Nullable + public String removeActiveRecord(Vector3i pos) { + return this.activeRecords.remove(pos); + } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java index cb8a8e60f..ef0329209 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 @@ -25,28 +25,27 @@ package org.geysermc.geyser.translator.protocol.java.level; -import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; -import org.geysermc.mcprotocollib.protocol.data.game.level.event.*; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelEventPacket; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.ParticleType; import org.cloudburstmc.protocol.bedrock.data.SoundEvent; -import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; -import org.cloudburstmc.protocol.bedrock.packet.TextPacket; +import org.cloudburstmc.protocol.bedrock.packet.*; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.level.JukeboxSong; import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.SoundMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.translator.level.event.LevelEventTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.geyser.util.SoundUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; +import org.geysermc.mcprotocollib.protocol.data.game.level.event.*; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelEventPacket; import java.util.Collections; -import java.util.Locale; import java.util.Set; @Translator(packet = ClientboundLevelEventPacket.class) @@ -60,23 +59,48 @@ public class JavaLevelEventTranslator extends PacketTranslator { - LevelSoundEventPacket levelSoundEvent = new LevelSoundEventPacket(); - levelSoundEvent.setIdentifier(""); - levelSoundEvent.setSound(SoundEvent.STOP_RECORD); - levelSoundEvent.setPosition(pos); - levelSoundEvent.setRelativeVolumeDisabled(false); - levelSoundEvent.setExtraData(-1); - levelSoundEvent.setBabySound(false); - session.sendUpstreamPacket(levelSoundEvent); + String bedrockSound = session.getWorldCache().removeActiveRecord(origin); + if (bedrockSound == null) { + // Vanilla record + LevelSoundEventPacket levelSoundEvent = new LevelSoundEventPacket(); + levelSoundEvent.setIdentifier(""); + levelSoundEvent.setSound(SoundEvent.STOP_RECORD); + levelSoundEvent.setPosition(pos); + levelSoundEvent.setRelativeVolumeDisabled(false); + levelSoundEvent.setExtraData(-1); + levelSoundEvent.setBabySound(false); + session.sendUpstreamPacket(levelSoundEvent); + } else { + // Custom record + StopSoundPacket stopSound = new StopSoundPacket(); + stopSound.setSoundName(bedrockSound); + session.sendUpstreamPacket(stopSound); + } return; } default -> { 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 3507567be..152bf4160 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 @@ -35,6 +35,7 @@ import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.packet.TextPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.session.GeyserSession; @@ -424,6 +425,20 @@ public class MessageTranslator { return new String(newChars, 0, count - (whitespacesCount > 0 ? 1 : 0)).trim(); } + /** + * Deserialize an NbtMap provided from a registry into a string. + */ + // This may be a Component in the future. + public static String deserializeDescription(NbtMap tag) { + NbtMap description = tag.getCompound("description"); + String translate = description.getString("translate", null); + if (translate == null) { + GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag); + return ""; + } + return translate; + } + public static void init() { // no-op } diff --git a/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java b/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java index 7559347e9..524d241db 100644 --- a/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/SoundUtils.java @@ -33,7 +33,6 @@ import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket; import org.cloudburstmc.protocol.bedrock.packet.PlaySoundPacket; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.level.block.BlockStateValues; import org.geysermc.geyser.level.block.type.Block; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; @@ -52,7 +51,7 @@ public final class SoundUtils { * @param sound the sound name * @return a sound event from the given sound */ - private static @Nullable SoundEvent toSoundEvent(String sound) { + public static @Nullable SoundEvent toSoundEvent(String sound) { try { return SoundEvent.valueOf(sound.toUpperCase(Locale.ROOT).replace(".", "_")); } catch (Exception ex) {