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