(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>
This commit is contained in:
Nathan V.
2025-08-30 14:15:26 -04:00
committed by GitHub
parent 99f7437d9d
commit df69410e67
5 changed files with 229 additions and 45 deletions

View File

@@ -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);
}
}

View File

@@ -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<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();
} 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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");
public boolean usesCustomPitch() {
switch (this) {
case POP:
case FIZZ:
return true;
default:
return false;
}
private final String soundRegistryId;
SoundType(String soundRegistryId) {
this.soundRegistryId = soundRegistryId;
}
public String id() {
return soundRegistryId;
}
public boolean usesCustomPitch()
{
return switch (this) {
case POP, FIZZ -> true;
default -> false;
};
}
}

View File

@@ -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
CustomSoundId: null