浏览代码

(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 <nossr50@gmail.com>
Nathan V. 4 天之前
父节点
当前提交
df69410e67

+ 9 - 4
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);
     }
 }

+ 73 - 13
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 Sound CRIPPLE_SOUND;
 
+    private static final Map<SoundType, Sound> 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();
+        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 {
-            return SoundConfig.getInstance().getPitch(soundType);
+            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 getSound(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;
-    }
 }

+ 92 - 0
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;
+    }
+}

+ 34 - 26
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;
+        };
+    }
+}

+ 20 - 1
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
+        Pitch: 0.5
+        CustomSoundId: null