From df69410e675e6db3283e03e193a726ffe1510600 Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Sat, 30 Aug 2025 14:15:26 -0400 Subject: [PATCH] (Improvement) Implement playing sound by string ID (#5201) * (improvement) implement playing sound by string ID I've replaced enum-based sound playing events with string-based equivalents, which should open the door for server customization and other enhancements in the future - Added SoundLookup class with different registry lookup methods depending on server version. - Added the ability to configure what sounds are played depending on event, with a fallback built into SoundType. - Removed getCrippleSound as SoundLookup can now fall back to the original default sound if the mace sound doesn't exist on the server's Minecraft version. - Added a EnableCustomSounds config variable that will skip SoundLookup ID checking and just pass the sound string directly to the client, mainly due to the fact that it isn't possible to verify if resource pack values exist. - Cleaned up a few switch statements to match how the original getSound had it formatted. I'd love to see/do a further expansion of sound configuration for each ability now that we can just fall back to generic, but that may be for another PR. * Fix getIsEnabled using wrong key * always use registry, simplify custom sound enabling logic, optimize reflection calls * forgot we need this for legacy versions --------- Co-authored-by: nossr50 --- .../com/gmail/nossr50/config/SoundConfig.java | 13 ++- .../nossr50/util/sounds/SoundManager.java | 88 +++++++++++++++--- .../util/sounds/SoundRegistryUtils.java | 92 +++++++++++++++++++ .../gmail/nossr50/util/sounds/SoundType.java | 60 ++++++------ src/main/resources/sounds.yml | 21 ++++- 5 files changed, 229 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/gmail/nossr50/util/sounds/SoundRegistryUtils.java diff --git a/src/main/java/com/gmail/nossr50/config/SoundConfig.java b/src/main/java/com/gmail/nossr50/config/SoundConfig.java index 3ad71fa98..7a17e6fbc 100644 --- a/src/main/java/com/gmail/nossr50/config/SoundConfig.java +++ b/src/main/java/com/gmail/nossr50/config/SoundConfig.java @@ -29,7 +29,7 @@ public class SoundConfig extends BukkitConfig { @Override protected boolean validateKeys() { for (SoundType soundType : SoundType.values()) { - if (config.getDouble("Sounds." + soundType.toString() + ".Volume") < 0) { + if (config.getDouble("Sounds." + soundType + ".Volume") < 0) { LogUtils.debug(mcMMO.p.getLogger(), "[mcMMO] Sound volume cannot be below 0 for " + soundType); return false; @@ -52,17 +52,22 @@ public class SoundConfig extends BukkitConfig { } public float getVolume(SoundType soundType) { - String key = "Sounds." + soundType.toString() + ".Volume"; + String key = "Sounds." + soundType + ".Volume"; return (float) config.getDouble(key, 1.0); } public float getPitch(SoundType soundType) { - String key = "Sounds." + soundType.toString() + ".Pitch"; + String key = "Sounds." + soundType + ".Pitch"; return (float) config.getDouble(key, 1.0); } + public String getSound(SoundType soundType) { + final String key = "Sounds." + soundType + ".CustomSoundId"; + return config.getString(key); + } + public boolean getIsEnabled(SoundType soundType) { - String key = "Sounds." + soundType.toString() + ".Enabled"; + String key = "Sounds." + soundType + ".Enable"; return config.getBoolean(key, true); } } diff --git a/src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java b/src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java index a345f1bc7..89460bc5b 100644 --- a/src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java +++ b/src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java @@ -4,6 +4,8 @@ import com.gmail.nossr50.config.SoundConfig; import com.gmail.nossr50.util.Misc; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.SoundCategory; @@ -11,12 +13,12 @@ import org.bukkit.World; import org.bukkit.entity.Player; public class SoundManager { + + private static final Map soundCache = new ConcurrentHashMap<>(); + private static final String NULL_FALLBACK_ID = null; private static Sound CRIPPLE_SOUND; - private static final String ITEM_MACE_SMASH_GROUND = "ITEM_MACE_SMASH_GROUND"; - private static final String VALUE_OF = "valueOf"; - private static final String ORG_BUKKIT_SOUND = "org.bukkit.Sound"; /** @@ -98,16 +100,78 @@ public class SoundManager { } private static float getPitch(SoundType soundType) { - if (soundType == SoundType.FIZZ) { - return getFizzPitch(); - } else if (soundType == SoundType.POP) { - return getPopPitch(); - } else { - return SoundConfig.getInstance().getPitch(soundType); - } + return switch (soundType) + { + case FIZZ -> getFizzPitch(); + case POP -> getPopPitch(); + default -> SoundConfig.getInstance().getPitch(soundType); + }; } private static Sound getSound(SoundType soundType) { + final String soundId = SoundConfig.getInstance().getSound(soundType); + + // Legacy versions use a different lookup method + if (SoundRegistryUtils.useLegacyLookup()) { + return getSoundLegacyCustom(soundId, soundType); + } + + if (soundCache.containsKey(soundType)) { + return soundCache.get(soundType); + } + + Sound sound; + if (soundId != null && !soundId.isEmpty()) { + sound = SoundRegistryUtils.getSound(soundId, soundType.id()); + } else { + sound = SoundRegistryUtils.getSound(soundType.id(), NULL_FALLBACK_ID); + } + + if (sound != null) { + soundCache.putIfAbsent(soundType, sound); + return sound; + } + + throw new RuntimeException("Could not find Sound for SoundType: " + soundType); + } + + private static Sound getSoundLegacyCustom(String id, SoundType soundType) { + if (soundCache.containsKey(soundType)) { + return soundCache.get(soundType); + } + + // Try to look up a custom legacy sound + if (id != null && !id.isEmpty()) { + Sound sound; + if (Sound.class.isEnum()) { + // Sound is only an ENUM in legacy versions + // Use reflection to loop through the values, finding the first enum matching our ID + try { + Method method = Sound.class.getMethod("getKey"); + for (Object legacyEnumEntry : Sound.class.getEnumConstants()) { + // This enum extends Keyed which adds the getKey() method + // we need to invoke this method to get the NamespacedKey and compare to our ID + if (method.invoke(legacyEnumEntry).toString().equals(id)) { + sound = (Sound) legacyEnumEntry; + soundCache.putIfAbsent(soundType, sound); + return sound; + } + } + } catch (NoSuchMethodException | InvocationTargetException | + IllegalAccessException e) { + // Ignore + } + } + throw new RuntimeException("Unable to find legacy sound by ID %s for SoundType %s" + .formatted(id, soundType)); + } + // Failsafe -- we haven't found a matching sound + final Sound sound = getSoundLegacyFallBack(soundType); + soundCache.putIfAbsent(soundType, sound); + return sound; + } + + private static Sound getSoundLegacyFallBack(SoundType soundType) { return switch (soundType) { case ANVIL -> Sound.BLOCK_ANVIL_PLACE; case ITEM_BREAK -> Sound.ENTITY_ITEM_BREAK; @@ -153,8 +217,4 @@ public class SoundManager { public static float getPopPitch() { return ((Misc.getRandom().nextFloat() - Misc.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F; } - - public static float getKrakenPitch() { - return (Misc.getRandom().nextFloat() - Misc.getRandom().nextFloat()) * 0.2F + 1.0F; - } } diff --git a/src/main/java/com/gmail/nossr50/util/sounds/SoundRegistryUtils.java b/src/main/java/com/gmail/nossr50/util/sounds/SoundRegistryUtils.java new file mode 100644 index 000000000..4051051cf --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/sounds/SoundRegistryUtils.java @@ -0,0 +1,92 @@ +package com.gmail.nossr50.util.sounds; + +import static java.lang.String.format; + +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.AttributeMapper; +import com.gmail.nossr50.util.LogUtils; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.jetbrains.annotations.Nullable; + +public final class SoundRegistryUtils { + + private static Method registryLookup; + private static Object soundReg; + + public static final String PAPER_SOUND_REGISTRY_FIELD = "SOUND_EVENT"; + public static final String SPIGOT_SOUND_REGISTRY_FIELD = "SOUNDS"; + public static final String METHOD_GET_OR_THROW_NAME = "getOrThrow"; + public static final String METHOD_GET_NAME = "get"; + + static { + boolean foundRegistry = false; + Class registry; + try { + registry = Class.forName(AttributeMapper.ORG_BUKKIT_REGISTRY); + try { + // First check for Paper's sound registry, held by field SOUND_EVENT + soundReg = registry.getField(PAPER_SOUND_REGISTRY_FIELD).get(null); + foundRegistry = true; + } catch (NoSuchFieldException | IllegalAccessException e) { + try { + soundReg = registry.getField(SPIGOT_SOUND_REGISTRY_FIELD); + foundRegistry = true; + } catch (NoSuchFieldException ex) { + // ignored + } + } + } catch (ClassNotFoundException e) { + // ignored + } + + if (foundRegistry) { + try { + // getOrThrow isn't in all API versions, but we use it if it exists + registryLookup = soundReg.getClass().getMethod(METHOD_GET_OR_THROW_NAME, + NamespacedKey.class); + } catch (NoSuchMethodException e) { + try { + registryLookup = soundReg.getClass().getMethod(METHOD_GET_NAME, + NamespacedKey.class); + } catch (NoSuchMethodException ex) { + // ignored exception + registryLookup = null; + } + } + } + } + + public static boolean useLegacyLookup() { + return registryLookup == null; + } + + public static @Nullable Sound getSound(String id, String fallBackId) { + if (registryLookup != null) { + try { + return (Sound) registryLookup.invoke(soundReg, NamespacedKey.fromString(id)); + } catch(InvocationTargetException | IllegalAccessException + | IllegalArgumentException e) { + if (fallBackId != null) { + LogUtils.debug(mcMMO.p.getLogger(), + format("Could not find sound with ID '%s', trying fallback ID '%s'", id, + fallBackId)); + try { + return (Sound) registryLookup.invoke(soundReg, + NamespacedKey.fromString(fallBackId)); + } catch (IllegalAccessException | InvocationTargetException ex) { + mcMMO.p.getLogger().severe(format("Could not find sound with ID %s," + + " fallback ID of %s also failed.", id, fallBackId)); + } + } else { + mcMMO.p.getLogger().severe(format("Could not find sound with ID %s.", id)); + } + throw new RuntimeException(e); + } + } + return null; + } +} diff --git a/src/main/java/com/gmail/nossr50/util/sounds/SoundType.java b/src/main/java/com/gmail/nossr50/util/sounds/SoundType.java index 7a0056537..0354afc73 100644 --- a/src/main/java/com/gmail/nossr50/util/sounds/SoundType.java +++ b/src/main/java/com/gmail/nossr50/util/sounds/SoundType.java @@ -1,31 +1,39 @@ package com.gmail.nossr50.util.sounds; public enum SoundType { - ANVIL, - LEVEL_UP, - FIZZ, - ITEM_BREAK, - POP, - CHIMAERA_WING, - ROLL_ACTIVATED, - SKILL_UNLOCKED, - DEFLECT_ARROWS, - TOOL_READY, - ABILITY_ACTIVATED_GENERIC, - ABILITY_ACTIVATED_BERSERK, - BLEED, - GLASS, - ITEM_CONSUMED, - CRIPPLE, - TIRED; + ANVIL("minecraft:block.anvil.place"), + ITEM_BREAK("minecraft:entity.item.break"), + POP("minecraft:entity.item.pickup"), + CHIMAERA_WING("minecraft:entity.bat.takeoff"), + LEVEL_UP("minecraft:entity.player.levelup"), + FIZZ("minecraft:block.fire.extinguish"), + TOOL_READY("minecraft:item.armor.equip_gold"), + ROLL_ACTIVATED("minecraft:entity.llama.swag"), + SKILL_UNLOCKED("minecraft:ui.toast.challenge_complete"), + ABILITY_ACTIVATED_BERSERK("minecraft:block.conduit.ambient"), + TIRED("minecraft:block.conduit.ambient"), + ABILITY_ACTIVATED_GENERIC("minecraft:item.trident.riptide_3"), + DEFLECT_ARROWS("minecraft:entity.ender_eye.death"), + BLEED("minecraft:entity.ender_eye.death"), + GLASS("minecraft:block.glass.break"), + ITEM_CONSUMED("minecraft:item.bottle.empty"), + CRIPPLE("minecraft:block.anvil.place"); + + private final String soundRegistryId; - public boolean usesCustomPitch() { - switch (this) { - case POP: - case FIZZ: - return true; - default: - return false; - } + SoundType(String soundRegistryId) { + this.soundRegistryId = soundRegistryId; } -} + + public String id() { + return soundRegistryId; + } + + public boolean usesCustomPitch() + { + return switch (this) { + case POP, FIZZ -> true; + default -> false; + }; + } +} \ No newline at end of file diff --git a/src/main/resources/sounds.yml b/src/main/resources/sounds.yml index b6d4fcba4..4330f3e6f 100644 --- a/src/main/resources/sounds.yml +++ b/src/main/resources/sounds.yml @@ -4,71 +4,90 @@ Sounds: # 1.0 = Max volume # 0.0 = No Volume MasterVolume: 1.0 + # If you want to use custom sounds, provide an ID for CustomSoundId + # Sound IDs are strings, such as minecraft:entity.player.levelup ITEM_CONSUMED: Enable: true Volume: 1.0 Pitch: 1.0 + CustomSoundId: null GLASS: Enable: true Volume: 1.0 Pitch: 1.0 + CustomSoundId: null ANVIL: Enable: true Volume: 1.0 Pitch: 0.3 + CustomSoundId: null #Fizz, and Pop make use of a adding and multiplying random numbers together to make a unique pitch everytime they are heard FIZZ: Enable: true Volume: 0.5 + CustomSoundId: null LEVEL_UP: Enable: true Volume: 0.3 Pitch: 0.5 + CustomSoundId: null ITEM_BREAK: Enable: true Volume: 1.0 Pitch: 1.0 + CustomSoundId: null #Fizz, and Pop make use of a adding and multiplying random numbers together to make a unique pitch everytime they are heard POP: Enable: true Volume: 0.2 + CustomSoundId: null CHIMAERA_WING: Enable: true Volume: 1.0 Pitch: 0.6 + CustomSoundId: null ROLL_ACTIVATED: Enable: true Volume: 1.0 Pitch: 0.7 + CustomSoundId: null SKILL_UNLOCKED: Enable: true Volume: 1.0 Pitch: 1.4 + CustomSoundId: null DEFLECT_ARROWS: Enable: true Volume: 1.0 Pitch: 2.0 + CustomSoundId: null TOOL_READY: Enable: true Volume: 1.0 Pitch: 0.4 + CustomSoundId: null ABILITY_ACTIVATED_GENERIC: Enable: true Volume: 1.0 Pitch: 0.1 + CustomSoundId: null ABILITY_ACTIVATED_BERSERK: Enable: true Volume: 0.5 Pitch: 1.7 + CustomSoundId: null TIRED: Enable: true Volume: 1.0 Pitch: 1.7 + CustomSoundId: null BLEED: Enable: true Volume: 2.0 Pitch: 2.0 + CustomSoundId: null CRIPPLE: Enable: true Volume: 1.0 - Pitch: 0.5 \ No newline at end of file + Pitch: 0.5 + CustomSoundId: null