Skip to content

Commit

Permalink
XSound Async Fix - IllegalStateException: Asynchronous play sound!
Browse files Browse the repository at this point in the history
Since paper now blocks async location-based sound plays (PaperMC/Paper#10021), I decided to use my own system.
  • Loading branch information
CryptoMorin committed Dec 30, 2023
1 parent dc23b5c commit b3fae10
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 30 deletions.
22 changes: 15 additions & 7 deletions src/main/java/com/cryptomorin/xseries/SkullUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@
* I don't know if this cache system works across other servers or is just specific to one server.
*
* @author Crypto Morin
* @version 6.0.0.1
* @version 6.0.1
* @see XMaterial
* @see ReflectionUtils
*/
public class SkullUtils {
public final class SkullUtils {
protected static final MethodHandle
CRAFT_META_SKULL_PROFILE_GETTER, CRAFT_META_SKULL_PROFILE_SETTER,
CRAFT_META_SKULL_BLOCK_SETTER, PROPERTY_GETVALUE;
Expand All @@ -86,7 +86,7 @@ public class SkullUtils {
* We'll just return an x shaped hardcoded skull.
* https://minecraft-heads.com/custom-heads/miscellaneous/58141-cross
*/
private static final String INVALID_BASE64 =
private static final String INVALID_SKULL_VALUE =
"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzEwNTkxZTY5MDllNmEyODFiMzcxODM2ZTQ2MmQ2N2EyYzc4ZmEwOTUyZTkxMGYzMmI0MWEyNmM0OGMxNzU3YyJ9fX0=";

/**
Expand Down Expand Up @@ -239,7 +239,7 @@ public static SkullMeta applySkin(@Nonnull ItemMeta head, @Nonnull String identi
case BASE64: return setSkullBase64(meta, identifier, extractMojangSHAFromBase64((String) result.object));
case TEXTURE_URL: return setSkullBase64(meta, encodeTexturesURL(identifier), extractMojangSHAFromBase64(identifier));
case TEXTURE_HASH: return setSkullBase64(meta, encodeTexturesURL(TEXTURES + identifier), identifier);
case UNKNOWN: return setSkullBase64(meta, INVALID_BASE64, INVALID_BASE64);
case UNKNOWN: return setSkullBase64(meta, INVALID_SKULL_VALUE, INVALID_SKULL_VALUE);
default: throw new AssertionError("Unknown skull value");
}
// @formatter:on
Expand All @@ -249,14 +249,22 @@ public static SkullMeta applySkin(@Nonnull ItemMeta head, @Nonnull String identi
public static SkullMeta setSkullBase64(@Nonnull SkullMeta head, @Nonnull String value, String MojangSHA) {
if (value == null || value.isEmpty()) throw new IllegalArgumentException("Skull value cannot be null or empty");
GameProfile profile = profileFromBase64(value, MojangSHA);
setProfile(head, profile);
return head;
}

/**
* Setting the profile directly is not compatible with {@link SkullMeta#setOwningPlayer(OfflinePlayer)}
* and should be reset with {@code setProfile(head, null)} before anything.
* <p>
* It seems like the Profile is prioritized over UUID/name.
*/
public static void setProfile(SkullMeta head, GameProfile profile) {
try {
CRAFT_META_SKULL_PROFILE_SETTER.invoke(head, profile);
} catch (Throwable ex) {
ex.printStackTrace();
}

return head;
}

@Nonnull
Expand Down Expand Up @@ -289,7 +297,7 @@ public static GameProfile detectProfileFromString(String identifier) {
case BASE64: return profileFromBase64( identifier, extractMojangSHAFromBase64((String) result.object));
case TEXTURE_URL: return profileFromBase64(encodeTexturesURL( identifier), extractMojangSHAFromBase64(identifier));
case TEXTURE_HASH: return profileFromBase64(encodeTexturesURL(TEXTURES + identifier), identifier);
case UNKNOWN: return profileFromBase64(INVALID_BASE64, INVALID_BASE64); // This can't be cached because the caller might change it.
case UNKNOWN: return profileFromBase64(INVALID_SKULL_VALUE, INVALID_SKULL_VALUE); // This can't be cached because the caller might change it.
default: throw new AssertionError("Unknown skull value");
}
// @formatter:on
Expand Down
83 changes: 60 additions & 23 deletions src/main/java/com/cryptomorin/xseries/XSound.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
* play command: <a href="https://minecraft.wiki/w/Commands/play">minecraft.wiki/w</a>
*
* @author Crypto Morin
* @version 9.3.0
* @version 9.4.0
* @see Sound
*/
public enum XSound {
Expand Down Expand Up @@ -2101,7 +2101,7 @@ public void play(@Nonnull Location location, float volume, float pitch) {
/**
* Used for data that need to be accessed during enum initialization.
*
* @since 5.0.0
* @since 6.0.0
*/
private static final class Data {
/**
Expand Down Expand Up @@ -2133,7 +2133,7 @@ public static class Record implements Cloneable {
public final float volume, pitch;
public boolean playAtLocation;
@Nullable
public Player player;
public Set<Player> players = new HashSet<>(10);
@Nullable
public Location location;

Expand All @@ -2147,18 +2147,23 @@ public Record(@Nonnull XSound sound, float volume, float pitch) {

public Record(@Nonnull XSound sound, @Nullable Player player, @Nullable Location location, float volume, float pitch, boolean playAtLocation) {
this.sound = Objects.requireNonNull(sound, "Sound cannot be null");
this.player = player;
addSinglePlayer(player);
this.location = location;
this.volume = volume;
this.pitch = pitch;
this.playAtLocation = playAtLocation;
}

private void addSinglePlayer(Player player) {
this.players.clear();
if (player != null) this.players.add(player);
}

/**
* Plays the sound only for a single player and no one else can hear it.
*/
public Record forPlayer(@Nullable Player player) {
this.player = player;
addSinglePlayer(player);
return this;
}

Expand All @@ -2170,26 +2175,51 @@ public Record atLocation(@Nullable Location location) {
return this;
}

/**
* Plays the sound only for a single player and no on else can hear it.
* The source of the sound is different and players using headphones may
* hear the sound with a <a href="https://en.wikipedia.org/wiki/3D_audio_effect">3D audio effect</a>.
*/
public Record forPlayerAtLocation(@Nullable Player player, @Nullable Location location) {
this.player = player;
this.location = location;
public Record forPlayers(@Nullable Collection<Player> players) {
this.players.clear();
this.players.addAll(players);
return this;
}

public Collection<Player> getHearingPlayers() {
if (location == null) return players;
return getHearingPlayers(location, volume);
}

public static Collection<Player> getHearingPlayers(Location location, double volume) {
// Increase the amount of blocks for volumes higher than 1
volume = volume > 1.0F ? (16.0F * volume) : 16.0;
double powerVolume = volume * volume;

List<Player> playersInWorld = location.getWorld().getPlayers();
List<Player> hearing = new ArrayList<>(playersInWorld.size());

double x = location.getX();
double y = location.getY();
double z = location.getZ();

for (Player player : playersInWorld) {
Location loc = player.getLocation();
double deltaX = x - loc.getX();
double deltaY = y - loc.getY();
double deltaZ = z - loc.getZ();

double length = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
if (length < powerVolume) hearing.add(player);
}

return hearing;
}

/**
* Plays the sound with the given options and updating the player's location.
*
* @since 3.0.0
*/
public void play() {
if (player == null && location == null)
if (players.isEmpty() && location == null)
throw new IllegalStateException("Cannot play sound when there is no location available");
play(player == null ? location : player.getLocation());
play(players.size() != 1 ? location : players.iterator().next().getLocation());
}

/**
Expand All @@ -2200,9 +2230,14 @@ public void play() {
*/
public void play(@Nonnull Location updatedLocation) {
Objects.requireNonNull(updatedLocation, "Cannot play sound at null location");
if (playAtLocation || player == null) {
location.getWorld().playSound(updatedLocation, sound.parseSound(), volume, pitch);
} else {
if (playAtLocation || players.isEmpty()) {
if (players.size() != 1) {
players.clear();
players.addAll(getHearingPlayers());
}
}

for (Player player : players) {
player.playSound(updatedLocation, sound.parseSound(), volume, pitch);
}
}
Expand All @@ -2219,11 +2254,11 @@ public void play(@Nonnull Location updatedLocation) {
*/
public void stopSound() {
if (playAtLocation) {
for (Entity entity : location.getWorld().getNearbyEntities(location, volume, volume, volume)) {
if (entity instanceof Player) ((Player) entity).stopSound(sound.parseSound());
for (Player player : getHearingPlayers()) {
player.stopSound(sound.parseSound());
}
}
if (player != null) player.stopSound(sound.parseSound());
players.forEach(x -> x.stopSound(sound.parseSound()));
}

public String rebuild() {
Expand All @@ -2233,14 +2268,16 @@ public String rebuild() {
@SuppressWarnings("MethodDoesntCallSuperMethod")
@Override
public Record clone() {
return new Record(
Record record = new Record(
sound,
player,
null,
location,
volume,
pitch,
playAtLocation
);
record.players.addAll(this.players);
return record;
}
}
}

0 comments on commit b3fae10

Please sign in to comment.