diff --git a/src/main/java/ch/njol/skript/bukkitutil/AdventureSoundReceiver.java b/src/main/java/ch/njol/skript/bukkitutil/AdventureSoundReceiver.java
new file mode 100644
index 00000000000..fd596a324a4
--- /dev/null
+++ b/src/main/java/ch/njol/skript/bukkitutil/AdventureSoundReceiver.java
@@ -0,0 +1,108 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.bukkitutil;
+
+import java.util.Locale;
+import java.util.OptionalLong;
+
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Sound;
+import org.bukkit.SoundCategory;
+import org.bukkit.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+
+import ch.njol.skript.effects.EffPlaySound;
+
+/**
+ * A utility interface to access the Player::playSound while also providing the same arguments to World::playSound
+ * Used in EffPlaySound. Separated due to static versioning.
+ */
+@FunctionalInterface
+public interface AdventureSoundReceiver {
+
+ void play(
+ @NotNull T receiver, @NotNull E emitter, @NotNull String sound,
+ @NotNull SoundCategory category, float volume, float pitch
+ );
+
+ static void play(
+ @NotNull AdventureEmitterSoundReceiver adventureLocationReceiver,
+ @NotNull AdventureEntitySoundReceiver adventureEmitterReceiver,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String[] sounds,
+ @NotNull SoundCategory category, float volume, float pitch, OptionalLong seed
+ ) {
+ for (String sound : sounds) {
+ NamespacedKey key = null;
+ try {
+ Sound enumSound = Sound.valueOf(sound.toUpperCase(Locale.ENGLISH));
+ key = enumSound.getKey();
+ } catch (IllegalArgumentException alternative) {
+ sound = sound.toLowerCase(Locale.ENGLISH);
+ if (!EffPlaySound.KEY_PATTERN.matcher(sound).matches())
+ continue;
+ try {
+ key = NamespacedKey.fromString(sound);
+ } catch (IllegalArgumentException argument) {
+ // The user input invalid characters
+ }
+ }
+
+ if (key == null)
+ continue;
+ net.kyori.adventure.sound.Sound adventureSound = net.kyori.adventure.sound.Sound.sound()
+ .source(category)
+ .volume(volume)
+ .pitch(pitch)
+ .seed(seed)
+ .type(key)
+ .build();
+ AdventureEmitterSoundReceiver.play(adventureLocationReceiver, adventureEmitterReceiver, receiver, adventureSound, emitter);
+ }
+ }
+
+ @FunctionalInterface
+ public interface AdventureEmitterSoundReceiver {
+ void play(
+ @NotNull T receiver, @NotNull net.kyori.adventure.sound.Sound sound, double x, double y, double z
+ );
+
+ static void play(
+ @NotNull AdventureEmitterSoundReceiver locationReceiver,
+ @NotNull AdventureEntitySoundReceiver emitterReceiver,
+ @NotNull T receiver, @NotNull net.kyori.adventure.sound.Sound sound, @NotNull E emitter
+ ) {
+ if (emitter instanceof Location) {
+ Location location = (Location) emitter;
+ locationReceiver.play(receiver, sound, location.getX(), location.getY(), location.getZ());
+ } else if (emitter instanceof Entity) {
+ Entity entity = (Entity) emitter;
+ emitterReceiver.play(receiver, sound, entity);
+ }
+ }
+ }
+
+ @FunctionalInterface
+ public interface AdventureEntitySoundReceiver {
+ void play(
+ @NotNull T receiver, @NotNull net.kyori.adventure.sound.Sound sound, net.kyori.adventure.sound.Sound.Emitter emitter
+ );
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/effects/EffPlaySound.java b/src/main/java/ch/njol/skript/effects/EffPlaySound.java
index 2d0d5cc0e5b..07eb31c59fe 100644
--- a/src/main/java/ch/njol/skript/effects/EffPlaySound.java
+++ b/src/main/java/ch/njol/skript/effects/EffPlaySound.java
@@ -19,112 +19,164 @@
package ch.njol.skript.effects;
import ch.njol.skript.Skript;
+import ch.njol.skript.bukkitutil.AdventureSoundReceiver;
+import ch.njol.skript.bukkitutil.AdventureSoundReceiver.AdventureEmitterSoundReceiver;
+import ch.njol.skript.bukkitutil.AdventureSoundReceiver.AdventureEntitySoundReceiver;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
import ch.njol.skript.doc.Since;
import ch.njol.skript.lang.Effect;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.util.Kleenean;
import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
+import java.util.OptionalLong;
import java.util.regex.Pattern;
@Name("Play Sound")
-@Description({"Plays a sound at given location for everyone or just for given players, or plays a sound to specified players. " +
- "Both Minecraft sound names and " +
- "Spigot sound names " +
- "are supported. Playing resource pack sounds are supported too. The sound category is 'master' by default. ",
- "",
- "Please note that sound names can get changed in any Minecraft or Spigot version, or even removed from Minecraft itself."})
+@Description({
+ "Plays a sound at given location for everyone or just for given players, or plays a sound to specified players. " +
+ "Both Minecraft sound names and " +
+ "Spigot sound names " +
+ "are supported. Playing resource pack sounds are supported too. The sound category is 'master' by default. ",
+ "",
+ "Playing a sound from an entity directly will result in the sound coming from said entity, even while moving.",
+ "If the sound is custom, a location emitter will follow the entity. Do note that pitch and volume ",
+ "are reflected based on the entity, and Minecraft may not use the values from this syntax.",
+ "",
+ "If using Paper 1.19.4+ or Adventure API 4.12.0+ you can utilize sound seeds. Minecraft sometimes have a set of sounds under one sound ID ",
+ "that will randomly play, to counter this, you can directly state which seed to use.",
+ "",
+ "Please note that sound names can get changed in any Minecraft or Spigot version, or even removed from Minecraft itself.",
+})
@Examples({
"play sound \"block.note_block.pling\" # It is block.note.pling in 1.12.2",
"play sound \"entity.experience_orb.pickup\" with volume 0.5 to the player",
- "play sound \"custom.music.1\" in jukebox category at {speakerBlock}"
+ "play sound \"custom.music.1\" in jukebox category at {speakerBlock}",
+ "play sound \"BLOCK_AMETHYST_BLOCK_RESONATE\" with seed 1 on target entity for the player #1.20.1+"
})
-@Since("2.2-dev28, 2.4 (sound categories)")
+@RequiredPlugins("Paper 1.19.4+ or Adventure API 4.12.0+ (sound seed)")
+@Since("2.2-dev28, 2.4 (sound categories), INSERT VERSION (sound seed & entity emitter)")
public class EffPlaySound extends Effect {
- private static final Pattern KEY_PATTERN = Pattern.compile("([a-z0-9._-]+:)?[a-z0-9/._-]+");
-
+ private static final boolean ADVENTURE_API = Skript.classExists("net.kyori.adventure.sound.Sound$Builder");
+ public static final Pattern KEY_PATTERN = Pattern.compile("([a-z0-9._-]+:)?[a-z0-9/._-]+");
+
static {
+ String additional = "";
+ if (ADVENTURE_API)
+ additional = "[[with] seed %-number%] ";
Skript.registerEffect(EffPlaySound.class,
- "play sound[s] %strings% [(in|from) %-soundcategory%] " +
- "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] at %locations% [(to|for) %-players%]",
- "play sound[s] %strings% [(in|from) %-soundcategory%] " +
- "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] [(to|for) %players%] [(at|from) %-locations%]"
+ "play sound[s] %strings% " + additional + "[(in|from) %-soundcategory%] " +
+ "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] (at|on|from) %locations/entities% [(to|for) %-players%]",
+ "play sound[s] %strings% " + additional + "[(in|from) %-soundcategory%] " +
+ "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] [(to|for) %players%] [(at|on|from) %-locations/entities%]"
);
}
@SuppressWarnings("NotNullFieldNotInitialized")
private Expression sounds;
+
@Nullable
private Expression category;
+
+ @Nullable
+ private Expression players;
+
@Nullable
private Expression volume;
+
@Nullable
private Expression pitch;
+
@Nullable
- private Expression locations;
+ private Expression seed;
+
@Nullable
- private Expression players;
+ private Expression> emitters;
@Override
@SuppressWarnings("unchecked")
public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
sounds = (Expression) exprs[0];
- category = (Expression) exprs[1];
- volume = (Expression) exprs[2];
- pitch = (Expression) exprs[3];
+ int index = 1;
+ if (ADVENTURE_API)
+ seed = (Expression) exprs[index++];
+ category = (Expression) exprs[index++];
+ volume = (Expression) exprs[index++];
+ pitch = (Expression) exprs[index++];
if (matchedPattern == 0) {
- locations = (Expression) exprs[4];
- players = (Expression) exprs[5];
+ emitters = exprs[index++];
+ players = (Expression) exprs[index];
} else {
- players = (Expression) exprs[4];
- locations = (Expression) exprs[5];
+ players = (Expression) exprs[index++];
+ emitters = exprs[index];
}
-
return true;
}
@Override
protected void execute(Event event) {
+ OptionalLong seed = OptionalLong.empty();
+ if (this.seed != null) {
+ Number number = this.seed.getSingle(event);
+ if (number != null)
+ seed = OptionalLong.of(number.longValue());
+ }
SoundCategory category = this.category == null ? SoundCategory.MASTER : this.category.getOptionalSingle(event)
- .orElse(SoundCategory.MASTER);
+ .orElse(SoundCategory.MASTER);
float volume = this.volume == null ? 1 : this.volume.getOptionalSingle(event)
- .orElse(1)
- .floatValue();
+ .orElse(1)
+ .floatValue();
float pitch = this.pitch == null ? 1 : this.pitch.getOptionalSingle(event)
- .orElse(1)
- .floatValue();
-
+ .orElse(1)
+ .floatValue();
+
if (players != null) {
- if (locations == null) {
+ if (emitters == null) {
for (Player player : players.getArray(event)) {
- SoundReceiver.play(Player::playSound, Player::playSound, player,
- player.getLocation(), sounds.getArray(event), category, volume, pitch);
+ play(Player::playSound, Player::playSound, ADVENTURE_API ? Player::playSound : null, ADVENTURE_API ? Player::playSound : null,
+ player, player.getLocation(), sounds.getArray(event), category, volume, pitch, seed);
}
} else {
for (Player player : players.getArray(event)) {
- for (Location location : locations.getArray(event)) {
- SoundReceiver.play(Player::playSound, Player::playSound, player,
- location, sounds.getArray(event), category, volume, pitch);
+ for (Object emitter : emitters.getArray(event)) {
+ if (emitter instanceof Entity) {
+ Entity entity = (Entity) emitter;
+ play(Player::playSound, Player::playSound, ADVENTURE_API ? Player::playSound : null, ADVENTURE_API ? Player::playSound : null,
+ player, entity, sounds.getArray(event), category, volume, pitch, seed);
+ } else if (emitter instanceof Location) {
+ Location location = (Location) emitter;
+ play(Player::playSound, Player::playSound, ADVENTURE_API ? Player::playSound : null, ADVENTURE_API ? Player::playSound : null,
+ player, location, sounds.getArray(event), category, volume, pitch, seed);
+ }
}
}
}
- } else if (locations != null) {
- for (Location location : locations.getArray(event)) {
- SoundReceiver.play(World::playSound, World::playSound, location.getWorld(),
- location, sounds.getArray(event), category, volume, pitch);
+ } else if (emitters != null) {
+ for (Object emitter : emitters.getArray(event)) {
+ if (emitter instanceof Entity) {
+ Entity entity = (Entity) emitter;
+ play(World::playSound, World::playSound, ADVENTURE_API ? World::playSound : null, ADVENTURE_API ? World::playSound : null,
+ entity.getWorld(), entity, sounds.getArray(event), category, volume, pitch, seed);
+ } else if (emitter instanceof Location) {
+ Location location = (Location) emitter;
+ play(World::playSound, World::playSound, ADVENTURE_API ? World::playSound : null, ADVENTURE_API ? World::playSound : null,
+ location.getWorld(), location, sounds.getArray(event), category, volume, pitch, seed);
+ }
}
}
}
@@ -132,53 +184,77 @@ protected void execute(Event event) {
@Override
public String toString(@Nullable Event event, boolean debug) {
StringBuilder builder = new StringBuilder()
- .append("play sound ")
- .append(sounds.toString(event, debug));
-
+ .append("play sound ")
+ .append(sounds.toString(event, debug));
+
+ if (seed != null)
+ builder.append(" with seed ").append(seed.toString(event, debug));
if (category != null)
builder.append(" in ").append(category.toString(event, debug));
-
if (volume != null)
builder.append(" with volume ").append(volume.toString(event, debug));
-
if (pitch != null)
builder.append(" with pitch ").append(pitch.toString(event, debug));
-
- if (locations != null)
- builder.append(" at ").append(locations.toString(event, debug));
-
+ if (emitters != null)
+ builder.append(" from ").append(emitters.toString(event, debug));
if (players != null)
builder.append(" to ").append(players.toString(event, debug));
return builder.toString();
}
-
+
+ private void play(@NotNull SoundReceiver entityReceiver,
+ @NotNull SoundReceiver locationReceiver,
+ @Nullable AdventureEmitterSoundReceiver adventureLocationReceiver,
+ @Nullable AdventureEntitySoundReceiver adventureEmitterReceiver,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String[] sounds,
+ @NotNull SoundCategory category, float volume, float pitch, OptionalLong seed) {
+ if (!ADVENTURE_API || adventureLocationReceiver == null || adventureEmitterReceiver == null) {
+ SoundReceiver.play(entityReceiver, locationReceiver, receiver, emitter, sounds, category, volume, pitch, seed);
+ return;
+ }
+ AdventureSoundReceiver.play(adventureLocationReceiver, adventureEmitterReceiver, receiver, emitter, sounds, category, volume, pitch, seed);
+ }
+
@FunctionalInterface
- private interface SoundReceiver {
+ private interface SoundReceiver {
void play(
- @NotNull T receiver, @NotNull Location location, @NotNull S sound,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String sound,
@NotNull SoundCategory category, float volume, float pitch
);
-
- static void play(
- @NotNull SoundReceiver stringReceiver,
- @NotNull SoundReceiver soundReceiver,
- @NotNull T receiver, @NotNull Location location, @NotNull String[] sounds,
- @NotNull SoundCategory category, float volume, float pitch
- ) {
+
+ static void play(
+ @NotNull SoundReceiver entityReceiver,
+ @NotNull SoundReceiver locationReceiver,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String[] sounds,
+ @NotNull SoundCategory category, float volume, float pitch, OptionalLong seed
+ ) {
for (String sound : sounds) {
+ NamespacedKey key = null;
try {
Sound enumSound = Sound.valueOf(sound.toUpperCase(Locale.ENGLISH));
- soundReceiver.play(receiver, location, enumSound, category, volume, pitch);
- continue;
- } catch (IllegalArgumentException ignored) {}
-
- sound = sound.toLowerCase(Locale.ENGLISH);
- if (!KEY_PATTERN.matcher(sound).matches())
+ key = enumSound.getKey();
+ } catch (IllegalArgumentException alternative) {
+ sound = sound.toLowerCase(Locale.ENGLISH);
+ if (!KEY_PATTERN.matcher(sound).matches())
+ continue;
+ try {
+ key = NamespacedKey.fromString(sound);
+ } catch (IllegalArgumentException argument) {
+ // The user input invalid characters
+ }
+ }
+
+ if (key == null)
continue;
-
- stringReceiver.play(receiver, location, sound, category, volume, pitch);
+ if (emitter instanceof Location) {
+ locationReceiver.play(receiver, (Location) emitter, key.getKey(), category, volume, pitch);
+ } else if (emitter instanceof Entity) {
+ entityReceiver.play(receiver, (Entity) emitter, key.getKey(), category, volume, pitch);
+ }
+ return;
}
}
}
+
}