Compare commits

...

5 Commits

Author SHA1 Message Date
nossr50
8e049822a3 better phrasing 2025-08-30 12:08:14 -07:00
nossr50
06f979d7bf Reduce Blast Mining PVP damage Fixes #5213 2025-08-30 12:03:50 -07:00
nossr50
f78586675d different default for CustomSoundId in sounds.yml 2025-08-30 11:40:23 -07:00
nossr50
46bcd29998 update changelog with info about custom sound playing 2025-08-30 11:20:31 -07:00
Nathan V.
df69410e67
(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>
2025-08-30 11:15:26 -07:00
8 changed files with 254 additions and 52 deletions

View File

@ -1,3 +1,15 @@
Version 2.2.042
You can now define custom sounds to be played in sounds.yml (Thank you JeBobs, see notes)
Added a cap to how much Blast Mining PVP damage can do to other players
Notes:
The new sounds.yml config file allows you to override the sounds played by mcMMO.
Simply define the ID of the sound you want to play for each sound.
For example, add an entry for Sounds.TOOL_READY.CustomSoundId into sounds.yml to override the sound played when a tool is "readied".
If you are on older versions (1.18, 1.19, etc), instead of registering an ID, you specify the Sound enum.
Those who are on newer versions, can define the ID of any sound registered with Paper/Spigot, this allows you to even play custom sounds so long as they are properly loaded on the server.
Vanilla minecraft sounds tend to have IDs like "minecraft:ui.toast.challenge_complete", you can google what these keys are.
Version 2.2.041
Fixed Berserk failing to crack blocks
Added 'Skills.Unarmed.Block_Cracker.Allow_Block_Cracker' to config.yml

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

@ -385,8 +385,8 @@ public class EntityListener implements Listener {
if (animalTamer != null && ((OfflinePlayer) animalTamer).isOnline()) {
attacker = (Entity) animalTamer;
}
} else if (attacker instanceof TNTPrimed && defender instanceof Player) {
if (BlastMining.processBlastMiningExplosion(event, (TNTPrimed) attacker,
} else if (attacker instanceof TNTPrimed tntAttacker && defender instanceof Player) {
if (BlastMining.processBlastMiningExplosion(event, tntAttacker,
(Player) defender)) {
return;
}

View File

@ -10,7 +10,8 @@ import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
public class BlastMining {
public final static int MAXIMUM_REMOTE_DETONATION_DISTANCE = 100;
public static final int MAXIMUM_REMOTE_DETONATION_DISTANCE = 100;
private static final double BLAST_MINING_PVP_DAMAGE_CAP = 24D;
public static double getBlastRadiusModifier(int rank) {
return mcMMO.p.getAdvancedConfig().getBlastRadiusModifier(rank);
@ -41,17 +42,22 @@ public class BlastMining {
}
public static boolean processBlastMiningExplosion(EntityDamageByEntityEvent event,
TNTPrimed tnt, Player defender) {
if (!tnt.hasMetadata(MetadataConstants.METADATA_KEY_TRACKED_TNT)
TNTPrimed tntAttacker, Player defender) {
if (!tntAttacker.hasMetadata(MetadataConstants.METADATA_KEY_TRACKED_TNT)
|| !UserManager.hasPlayerDataKey(defender)) {
return false;
}
// We can make this assumption because we (should) be the only ones using this exact metadata
Player player = mcMMO.p.getServer().getPlayerExact(
tnt.getMetadata(MetadataConstants.METADATA_KEY_TRACKED_TNT).get(0).asString());
tntAttacker.getMetadata(MetadataConstants.METADATA_KEY_TRACKED_TNT).get(0).asString());
if (!(player != null && player.equals(defender))) {
double cappedDamage = Math.min(event.getDamage(), BLAST_MINING_PVP_DAMAGE_CAP);
event.setDamage(Math.max(cappedDamage, 0D));
if (event.getFinalDamage() <= 0) {
event.setCancelled(true);
}
return false;
}
@ -67,7 +73,7 @@ public class BlastMining {
event.setDamage(miningManager.processDemolitionsExpertise(event.getDamage()));
if (event.getFinalDamage() == 0) {
if (event.getFinalDamage() <= 0) {
event.setCancelled(true);
return false;
}

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

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: ''
GLASS:
Enable: true
Volume: 1.0
Pitch: 1.0
CustomSoundId: ''
ANVIL:
Enable: true
Volume: 1.0
Pitch: 0.3
CustomSoundId: ''
#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: ''
LEVEL_UP:
Enable: true
Volume: 0.3
Pitch: 0.5
CustomSoundId: ''
ITEM_BREAK:
Enable: true
Volume: 1.0
Pitch: 1.0
CustomSoundId: ''
#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: ''
CHIMAERA_WING:
Enable: true
Volume: 1.0
Pitch: 0.6
CustomSoundId: ''
ROLL_ACTIVATED:
Enable: true
Volume: 1.0
Pitch: 0.7
CustomSoundId: ''
SKILL_UNLOCKED:
Enable: true
Volume: 1.0
Pitch: 1.4
CustomSoundId: ''
DEFLECT_ARROWS:
Enable: true
Volume: 1.0
Pitch: 2.0
CustomSoundId: ''
TOOL_READY:
Enable: true
Volume: 1.0
Pitch: 0.4
CustomSoundId: ''
ABILITY_ACTIVATED_GENERIC:
Enable: true
Volume: 1.0
Pitch: 0.1
CustomSoundId: ''
ABILITY_ACTIVATED_BERSERK:
Enable: true
Volume: 0.5
Pitch: 1.7
CustomSoundId: ''
TIRED:
Enable: true
Volume: 1.0
Pitch: 1.7
CustomSoundId: ''
BLEED:
Enable: true
Volume: 2.0
Pitch: 2.0
CustomSoundId: ''
CRIPPLE:
Enable: true
Volume: 1.0
Pitch: 0.5
Pitch: 0.5
CustomSoundId: ''