Skip to content

Commit df69410

Browse files
JeBobsnossr50
andauthored
(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 <[email protected]>
1 parent 99f7437 commit df69410

File tree

5 files changed

+228
-44
lines changed

5 files changed

+228
-44
lines changed

src/main/java/com/gmail/nossr50/config/SoundConfig.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected void loadKeys() {
2929
@Override
3030
protected boolean validateKeys() {
3131
for (SoundType soundType : SoundType.values()) {
32-
if (config.getDouble("Sounds." + soundType.toString() + ".Volume") < 0) {
32+
if (config.getDouble("Sounds." + soundType + ".Volume") < 0) {
3333
LogUtils.debug(mcMMO.p.getLogger(),
3434
"[mcMMO] Sound volume cannot be below 0 for " + soundType);
3535
return false;
@@ -52,17 +52,22 @@ public float getMasterVolume() {
5252
}
5353

5454
public float getVolume(SoundType soundType) {
55-
String key = "Sounds." + soundType.toString() + ".Volume";
55+
String key = "Sounds." + soundType + ".Volume";
5656
return (float) config.getDouble(key, 1.0);
5757
}
5858

5959
public float getPitch(SoundType soundType) {
60-
String key = "Sounds." + soundType.toString() + ".Pitch";
60+
String key = "Sounds." + soundType + ".Pitch";
6161
return (float) config.getDouble(key, 1.0);
6262
}
6363

64+
public String getSound(SoundType soundType) {
65+
final String key = "Sounds." + soundType + ".CustomSoundId";
66+
return config.getString(key);
67+
}
68+
6469
public boolean getIsEnabled(SoundType soundType) {
65-
String key = "Sounds." + soundType.toString() + ".Enabled";
70+
String key = "Sounds." + soundType + ".Enable";
6671
return config.getBoolean(key, true);
6772
}
6873
}

src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44
import com.gmail.nossr50.util.Misc;
55
import java.lang.reflect.InvocationTargetException;
66
import java.lang.reflect.Method;
7+
import java.util.Map;
8+
import java.util.concurrent.ConcurrentHashMap;
79
import org.bukkit.Location;
810
import org.bukkit.Sound;
911
import org.bukkit.SoundCategory;
1012
import org.bukkit.World;
1113
import org.bukkit.entity.Player;
1214

1315
public class SoundManager {
14-
private static Sound CRIPPLE_SOUND;
1516

17+
private static final Map<SoundType, Sound> soundCache = new ConcurrentHashMap<>();
18+
private static final String NULL_FALLBACK_ID = null;
19+
private static Sound CRIPPLE_SOUND;
1620
private static final String ITEM_MACE_SMASH_GROUND = "ITEM_MACE_SMASH_GROUND";
17-
1821
private static final String VALUE_OF = "valueOf";
19-
2022
private static final String ORG_BUKKIT_SOUND = "org.bukkit.Sound";
2123

2224
/**
@@ -98,16 +100,78 @@ private static float getVolume(SoundType soundType) {
98100
}
99101

100102
private static float getPitch(SoundType soundType) {
101-
if (soundType == SoundType.FIZZ) {
102-
return getFizzPitch();
103-
} else if (soundType == SoundType.POP) {
104-
return getPopPitch();
103+
return switch (soundType)
104+
{
105+
case FIZZ -> getFizzPitch();
106+
case POP -> getPopPitch();
107+
default -> SoundConfig.getInstance().getPitch(soundType);
108+
};
109+
}
110+
111+
private static Sound getSound(SoundType soundType) {
112+
final String soundId = SoundConfig.getInstance().getSound(soundType);
113+
114+
// Legacy versions use a different lookup method
115+
if (SoundRegistryUtils.useLegacyLookup()) {
116+
return getSoundLegacyCustom(soundId, soundType);
117+
}
118+
119+
if (soundCache.containsKey(soundType)) {
120+
return soundCache.get(soundType);
121+
}
122+
123+
Sound sound;
124+
if (soundId != null && !soundId.isEmpty()) {
125+
sound = SoundRegistryUtils.getSound(soundId, soundType.id());
105126
} else {
106-
return SoundConfig.getInstance().getPitch(soundType);
127+
sound = SoundRegistryUtils.getSound(soundType.id(), NULL_FALLBACK_ID);
107128
}
129+
130+
if (sound != null) {
131+
soundCache.putIfAbsent(soundType, sound);
132+
return sound;
133+
}
134+
135+
throw new RuntimeException("Could not find Sound for SoundType: " + soundType);
108136
}
109137

110-
private static Sound getSound(SoundType soundType) {
138+
private static Sound getSoundLegacyCustom(String id, SoundType soundType) {
139+
if (soundCache.containsKey(soundType)) {
140+
return soundCache.get(soundType);
141+
}
142+
143+
// Try to look up a custom legacy sound
144+
if (id != null && !id.isEmpty()) {
145+
Sound sound;
146+
if (Sound.class.isEnum()) {
147+
// Sound is only an ENUM in legacy versions
148+
// Use reflection to loop through the values, finding the first enum matching our ID
149+
try {
150+
Method method = Sound.class.getMethod("getKey");
151+
for (Object legacyEnumEntry : Sound.class.getEnumConstants()) {
152+
// This enum extends Keyed which adds the getKey() method
153+
// we need to invoke this method to get the NamespacedKey and compare to our ID
154+
if (method.invoke(legacyEnumEntry).toString().equals(id)) {
155+
sound = (Sound) legacyEnumEntry;
156+
soundCache.putIfAbsent(soundType, sound);
157+
return sound;
158+
}
159+
}
160+
} catch (NoSuchMethodException | InvocationTargetException |
161+
IllegalAccessException e) {
162+
// Ignore
163+
}
164+
}
165+
throw new RuntimeException("Unable to find legacy sound by ID %s for SoundType %s"
166+
.formatted(id, soundType));
167+
}
168+
// Failsafe -- we haven't found a matching sound
169+
final Sound sound = getSoundLegacyFallBack(soundType);
170+
soundCache.putIfAbsent(soundType, sound);
171+
return sound;
172+
}
173+
174+
private static Sound getSoundLegacyFallBack(SoundType soundType) {
111175
return switch (soundType) {
112176
case ANVIL -> Sound.BLOCK_ANVIL_PLACE;
113177
case ITEM_BREAK -> Sound.ENTITY_ITEM_BREAK;
@@ -153,8 +217,4 @@ public static float getFizzPitch() {
153217
public static float getPopPitch() {
154218
return ((Misc.getRandom().nextFloat() - Misc.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F;
155219
}
156-
157-
public static float getKrakenPitch() {
158-
return (Misc.getRandom().nextFloat() - Misc.getRandom().nextFloat()) * 0.2F + 1.0F;
159-
}
160220
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.gmail.nossr50.util.sounds;
2+
3+
import static java.lang.String.format;
4+
5+
import com.gmail.nossr50.mcMMO;
6+
import com.gmail.nossr50.util.AttributeMapper;
7+
import com.gmail.nossr50.util.LogUtils;
8+
import java.lang.reflect.InvocationTargetException;
9+
import java.lang.reflect.Method;
10+
import java.util.Locale;
11+
import org.bukkit.NamespacedKey;
12+
import org.bukkit.Sound;
13+
import org.jetbrains.annotations.Nullable;
14+
15+
public final class SoundRegistryUtils {
16+
17+
private static Method registryLookup;
18+
private static Object soundReg;
19+
20+
public static final String PAPER_SOUND_REGISTRY_FIELD = "SOUND_EVENT";
21+
public static final String SPIGOT_SOUND_REGISTRY_FIELD = "SOUNDS";
22+
public static final String METHOD_GET_OR_THROW_NAME = "getOrThrow";
23+
public static final String METHOD_GET_NAME = "get";
24+
25+
static {
26+
boolean foundRegistry = false;
27+
Class<?> registry;
28+
try {
29+
registry = Class.forName(AttributeMapper.ORG_BUKKIT_REGISTRY);
30+
try {
31+
// First check for Paper's sound registry, held by field SOUND_EVENT
32+
soundReg = registry.getField(PAPER_SOUND_REGISTRY_FIELD).get(null);
33+
foundRegistry = true;
34+
} catch (NoSuchFieldException | IllegalAccessException e) {
35+
try {
36+
soundReg = registry.getField(SPIGOT_SOUND_REGISTRY_FIELD);
37+
foundRegistry = true;
38+
} catch (NoSuchFieldException ex) {
39+
// ignored
40+
}
41+
}
42+
} catch (ClassNotFoundException e) {
43+
// ignored
44+
}
45+
46+
if (foundRegistry) {
47+
try {
48+
// getOrThrow isn't in all API versions, but we use it if it exists
49+
registryLookup = soundReg.getClass().getMethod(METHOD_GET_OR_THROW_NAME,
50+
NamespacedKey.class);
51+
} catch (NoSuchMethodException e) {
52+
try {
53+
registryLookup = soundReg.getClass().getMethod(METHOD_GET_NAME,
54+
NamespacedKey.class);
55+
} catch (NoSuchMethodException ex) {
56+
// ignored exception
57+
registryLookup = null;
58+
}
59+
}
60+
}
61+
}
62+
63+
public static boolean useLegacyLookup() {
64+
return registryLookup == null;
65+
}
66+
67+
public static @Nullable Sound getSound(String id, String fallBackId) {
68+
if (registryLookup != null) {
69+
try {
70+
return (Sound) registryLookup.invoke(soundReg, NamespacedKey.fromString(id));
71+
} catch(InvocationTargetException | IllegalAccessException
72+
| IllegalArgumentException e) {
73+
if (fallBackId != null) {
74+
LogUtils.debug(mcMMO.p.getLogger(),
75+
format("Could not find sound with ID '%s', trying fallback ID '%s'", id,
76+
fallBackId));
77+
try {
78+
return (Sound) registryLookup.invoke(soundReg,
79+
NamespacedKey.fromString(fallBackId));
80+
} catch (IllegalAccessException | InvocationTargetException ex) {
81+
mcMMO.p.getLogger().severe(format("Could not find sound with ID %s,"
82+
+ " fallback ID of %s also failed.", id, fallBackId));
83+
}
84+
} else {
85+
mcMMO.p.getLogger().severe(format("Could not find sound with ID %s.", id));
86+
}
87+
throw new RuntimeException(e);
88+
}
89+
}
90+
return null;
91+
}
92+
}
Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
package com.gmail.nossr50.util.sounds;
22

33
public enum SoundType {
4-
ANVIL,
5-
LEVEL_UP,
6-
FIZZ,
7-
ITEM_BREAK,
8-
POP,
9-
CHIMAERA_WING,
10-
ROLL_ACTIVATED,
11-
SKILL_UNLOCKED,
12-
DEFLECT_ARROWS,
13-
TOOL_READY,
14-
ABILITY_ACTIVATED_GENERIC,
15-
ABILITY_ACTIVATED_BERSERK,
16-
BLEED,
17-
GLASS,
18-
ITEM_CONSUMED,
19-
CRIPPLE,
20-
TIRED;
4+
ANVIL("minecraft:block.anvil.place"),
5+
ITEM_BREAK("minecraft:entity.item.break"),
6+
POP("minecraft:entity.item.pickup"),
7+
CHIMAERA_WING("minecraft:entity.bat.takeoff"),
8+
LEVEL_UP("minecraft:entity.player.levelup"),
9+
FIZZ("minecraft:block.fire.extinguish"),
10+
TOOL_READY("minecraft:item.armor.equip_gold"),
11+
ROLL_ACTIVATED("minecraft:entity.llama.swag"),
12+
SKILL_UNLOCKED("minecraft:ui.toast.challenge_complete"),
13+
ABILITY_ACTIVATED_BERSERK("minecraft:block.conduit.ambient"),
14+
TIRED("minecraft:block.conduit.ambient"),
15+
ABILITY_ACTIVATED_GENERIC("minecraft:item.trident.riptide_3"),
16+
DEFLECT_ARROWS("minecraft:entity.ender_eye.death"),
17+
BLEED("minecraft:entity.ender_eye.death"),
18+
GLASS("minecraft:block.glass.break"),
19+
ITEM_CONSUMED("minecraft:item.bottle.empty"),
20+
CRIPPLE("minecraft:block.anvil.place");
21+
22+
private final String soundRegistryId;
2123

22-
public boolean usesCustomPitch() {
23-
switch (this) {
24-
case POP:
25-
case FIZZ:
26-
return true;
27-
default:
28-
return false;
29-
}
24+
SoundType(String soundRegistryId) {
25+
this.soundRegistryId = soundRegistryId;
3026
}
31-
}
27+
28+
public String id() {
29+
return soundRegistryId;
30+
}
31+
32+
public boolean usesCustomPitch()
33+
{
34+
return switch (this) {
35+
case POP, FIZZ -> true;
36+
default -> false;
37+
};
38+
}
39+
}

0 commit comments

Comments
 (0)