Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel sounds #170

Merged
merged 6 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions common/src/main/java/org/figuramc/figura/avatar/Avatar.java
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,10 @@ public boolean itemRenderEvent(ItemStackAPI item, String mode, FiguraVec3 pos, F
return rendered;
}

public void playSoundEvent(String id, FiguraVec3 pos, float vol, float pitch, boolean loop, String category, String file) {
if (loaded) run("ON_PLAY_SOUND", tick, id, pos, vol, pitch, loop, category, file);
public boolean playSoundEvent(String id, FiguraVec3 pos, float vol, float pitch, boolean loop, String category, String file) {
Varargs result = null;
if (loaded) result = run("ON_PLAY_SOUND", tick, id, pos, vol, pitch, loop, category, file);
return isCancelled(result);
}

public void resourceReloadEvent() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundSource;
import org.figuramc.figura.avatar.Avatar;
import org.figuramc.figura.avatar.AvatarManager;
import org.figuramc.figura.lua.LuaWhitelist;
import org.figuramc.figura.lua.docs.LuaMethodDoc;
import org.figuramc.figura.lua.docs.LuaMethodOverload;
Expand Down Expand Up @@ -88,20 +89,62 @@ public LuaSound play() {

owner.noPermissions.remove(Permissions.SOUNDS);

// if handle exists, the sound was previously played. Unpause it
if (handle != null) {
handle.execute(Channel::unpause);
this.playing = true;
} else if (buffer != null) {
float vol = calculateVolume();
if (vol <= 0)
return this;
return this;
}

// if there is no sound data, exit early
if (buffer == null && sound == null)
return this;

float vol = calculateVolume();
// if nobody can hear you scream, are you really screaming? No.
if (vol <= 0)
return this;

// Technically I am setting playing to true earlier than I am supposed to,
// but the function cannot exit early past here (other than crashing) so its fine
this.playing = true;
AvatarManager.executeAll("playSoundEvent", avatar -> {
boolean cancel = avatar.playSoundEvent(
this.getId(),
this.getPos(),
this.getVolume(), this.getPitch(),
this.isLooping(),
SoundSource.PLAYERS.name(),
null
);
if (avatar.permissions.get(Permissions.CANCEL_SOUNDS) >= 1) {
avatar.noPermissions.remove(Permissions.CANCEL_SOUNDS);
if (cancel)
this.playing = false;
}
else {
avatar.noPermissions.add(Permissions.CANCEL_SOUNDS);
}
});
// If the sound was cancelled, exit.
if (!this.playing) {
return this;
}

if (buffer != null)
this.handle = SoundAPI.getSoundEngine().figura$createHandle(owner.owner, id, Library.Pool.STATIC);
if (handle == null)
return this;
else
this.handle = SoundAPI.getSoundEngine().figura$createHandle(owner.owner, id, sound.shouldStream() ? Library.Pool.STREAMING : Library.Pool.STATIC);

SoundAPI.getSoundEngine().figura$addSound(this);
// I dunno why this check was here in the first place and I aint about to ruin things by removing it.
if (handle == null) {
this.playing = false; // explicit set just incase
return this;
}

SoundAPI.getSoundEngine().figura$addSound(this);

if (buffer != null) {
handle.execute(channel -> {
channel.setPitch(pitch);
channel.setVolume(volume * vol);
Expand All @@ -112,31 +155,18 @@ public LuaSound play() {
channel.attachStaticBuffer(buffer);
channel.play();
});

this.playing = true;
} else if (sound != null) {
float vol = calculateVolume();
if (vol <= 0)
return this;

boolean shouldStream = sound.shouldStream();
this.handle = SoundAPI.getSoundEngine().figura$createHandle(owner.owner, id, shouldStream ? Library.Pool.STREAMING : Library.Pool.STATIC);
if (handle == null)
return this;

SoundAPI.getSoundEngine().figura$addSound(this);

} else {
handle.execute(channel -> {
channel.setPitch(pitch);
channel.setVolume(volume * vol);
channel.linearAttenuation(attenuation * 16f);
channel.setLooping(loop && !shouldStream);
channel.setLooping(loop && !sound.shouldStream());
channel.setSelfPosition(pos.asVec3());
channel.setRelative(false);
});

SoundBufferLibrary lib = SoundAPI.getSoundEngine().figura$getSoundBuffers();
if (!shouldStream) {
if (!sound.shouldStream()) {
lib.getCompleteBuffer(sound.getPath()).thenAccept(buffer -> handle.execute(channel -> {
channel.attachStaticBuffer(buffer);
channel.play();
Expand All @@ -147,8 +177,6 @@ public LuaSound play() {
channel.play();
}));
}

this.playing = true;
}

return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@

import com.mojang.blaze3d.audio.Channel;
import com.mojang.blaze3d.audio.Library;
import com.mojang.blaze3d.audio.Listener;

import net.minecraft.client.Options;
import net.minecraft.client.gui.components.SubtitleOverlay;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.client.resources.sounds.SoundInstance.Attenuation;
import net.minecraft.client.sounds.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceProvider;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.phys.Vec3;

import org.figuramc.figura.avatar.AvatarManager;
import org.figuramc.figura.ducks.ChannelHandleAccessor;
import org.figuramc.figura.ducks.SoundEngineAccessor;
import org.figuramc.figura.ducks.SubtitleOverlayAccessor;
import org.figuramc.figura.lua.api.sound.FiguraSoundListener;
import org.figuramc.figura.lua.api.sound.LuaSound;
import org.figuramc.figura.math.vector.FiguraVec3;
import org.figuramc.figura.permissions.Permissions;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
Expand All @@ -28,6 +36,7 @@ public abstract class SoundEngineMixin implements SoundEngineAccessor {
@Shadow @Final private SoundEngineExecutor executor;
@Shadow @Final private SoundBufferLibrary soundBuffers;
@Shadow private boolean loaded;
@Shadow private Listener listener;

@Shadow protected abstract float getVolume(@Nullable SoundSource category);
@Shadow @Final private List<SoundEventListener> listeners;
Expand All @@ -41,7 +50,6 @@ public abstract class SoundEngineMixin implements SoundEngineAccessor {
@Inject(at = @At("RETURN"), method = "<init>")
private void soundEngineInit(SoundManager soundManager, Options options, ResourceProvider resourceProvider, CallbackInfo ci) {
figuraChannel = new ChannelAccess(this.library, this.executor);
addEventListener(new FiguraSoundListener());
}

@Inject(at = @At("RETURN"), method = "tick")
Expand Down Expand Up @@ -96,14 +104,43 @@ private void stop(ResourceLocation id, SoundSource category, CallbackInfo ci) {
figura$stopAllSounds();
}

// targeting getGain is more likely to target more versions of Minecraft, but targeting the `listeners` list blocks subtitles. I believe blocking subtitles was more important.
// If we end up targeting a version of minecraft with no subtitles, use getGain. In vanilla, `listeners` is only used for subtitles and may not be in versions without subtitles.
//@Inject(at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/audio/Listener;getGain()Z"), method = "play", cancellable = true)
@Inject(at = @At(value = "INVOKE", target = "Ljava/util/List;isEmpty()Z"), method = "play", cancellable = true)
public void play(SoundInstance sound, CallbackInfo c) {
// "Can hear sound" check stolen from 381 of SoundEngine
float g = Math.max(sound.getVolume(), 1.0F) * (float)sound.getSound().getAttenuationDistance();
Vec3 pos = new Vec3(sound.getX(), sound.getY(), sound.getZ());
if (sound.isRelative() || sound.getAttenuation() == Attenuation.NONE || this.listener.getListenerPosition().distanceToSqr(pos) < (double)(g * g)){
// Run sound event
AvatarManager.executeAll("playSoundEvent", avatar -> {
boolean cancel = avatar.playSoundEvent(
sound.getLocation().toString(),
FiguraVec3.fromVec3(pos),
sound.getVolume(), sound.getPitch(),
sound.isLooping(),
sound.getSource().name(),
sound.getSound().getLocation().toString()
);
if (avatar.permissions.get(Permissions.CANCEL_SOUNDS) >= 1) {
avatar.noPermissions.remove(Permissions.CANCEL_SOUNDS);
if (cancel)
c.cancel(); // calling cancel multple times is fine, right?
}
else {
avatar.noPermissions.add(Permissions.CANCEL_SOUNDS);
}
});
}
}

@Override @Intrinsic
public void figura$addSound(LuaSound sound) {
figuraHandlers.add(sound);
for (SoundEventListener listener : this.listeners) {
if (listener instanceof SubtitleOverlay overlay)
((SubtitleOverlayAccessor) overlay).figura$PlaySound(sound);
else if (listener instanceof FiguraSoundListener figuraListener)
figuraListener.figuraPlaySound(sound);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public boolean checkInfinity(int value) {
OFFSCREEN_RENDERING = new Permissions("OFFSCREEN_RENDERING", 0, 0, 0, 1, 1),
// CUSTOM_SHADERS = new Permissions("CUSTOM_SHADERS", 0, 0, 1, 1, 1),
CUSTOM_SOUNDS = new Permissions("CUSTOM_SOUNDS", 0, 0, 1, 1, 1),
CANCEL_SOUNDS = new Permissions("CANCEL_SOUNDS", 0, 0, 0, 1, 1),
CUSTOM_SKULL = new Permissions("CUSTOM_SKULL", 0, 0, 1, 1, 1),
BUFFER_SIZE = new Permissions("BUFFER_SIZE", 0, 3072000, 0, 128000, 1024000, 2048000, Integer.MAX_VALUE),
BUFFERS_COUNT = new Permissions("BUFFERS_COUNT", 0, 32, 0, 2, 4, 16, 32),
Expand All @@ -57,6 +58,7 @@ public boolean checkInfinity(int value) {
NAMEPLATE_EDIT,
OFFSCREEN_RENDERING,
CUSTOM_SOUNDS,
CANCEL_SOUNDS,
CUSTOM_SKULL,
BUFFER_SIZE,
BUFFERS_COUNT,
Expand Down
5 changes: 4 additions & 1 deletion common/src/main/resources/assets/figura/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"figura.permissions.value.nameplate_edit.tooltip": "Toggles if the Avatar can change its nameplate, allowing for completely custom names, positioning and even disabling its rendering",
"figura.permissions.value.offscreen_rendering": "Render Offscreen",
"figura.permissions.value.offscreen_rendering.tooltip": "Toggles if the Avatar should render even when you (the viewer) are not looking at them, ie behind you",
"figura.permissions.value.cancel_sounds": "Cancel Sounds",
"figura.permissions.value.cancel_sounds.tooltip": "Toggles if the Avatar can stop any sound from playing via the ON_PLAY_SOUND event",
"figura.permissions.value.custom_render_layer": "Custom Render Layers",
"figura.permissions.value.custom_render_layer.tooltip": "Toggles if the Avatar can create its own Render Layers, allowing for custom GLSL code (shaders)",
"figura.permissions.value.custom_sounds": "Custom Sounds",
Expand Down Expand Up @@ -136,6 +138,7 @@
"figura.badges.no_permissions.texture_size": "Custom Textures too huge",
"figura.badges.no_permissions.particles": "Reached Particles limit",
"figura.badges.no_permissions.sounds": "Reached Sounds limit",
"figura.badges.no_permissions.cancel_sounds": "Could not cancel a sound",
"figura.badges.no_permissions.custom_sounds": "Could not use Custom Sounds",
"figura.badges.no_permissions.vanilla_model_edit": "Tried to change the Vanilla Model",
"figura.emoji.face_angry": "Angry",
Expand Down Expand Up @@ -1051,7 +1054,7 @@
"figura.docs.events.use_item": "The USE_ITEM event is run every time the entity uses an item\nThe item, action and amount of particles this item would produce is given as argument\nIf returned true, the event cancels its vanilla function",
"figura.docs.events.arrow_render": "The ARROW_RENDER event is run for every arrow entity shot by the Avatar owner\nIt takes two arguments, the tick delta, and the arrow entity\nReturning \"true\" stops this arrow from rendering, including the Arrow parent parts\nRequires the \"Vanilla Model Change\" permission",
"figura.docs.events.item_render": "Called on every one of your items that is being rendered\nIt takes six arguments: the item being rendered, the rendering mode, the position, rotation, and scale that would be applied to the item, and if it's being rendered in the left hand\nReturning a ModelPart parented to Item stops the rendering of this item and will render the returned part instead",
"figura.docs.events.on_play_sound": "Called every time a new sound is played\nTakes the following as arguments: the sound's ID, its world position, volume, pitch, if the sound should loop, the sound's category, and the sound's file path",
"figura.docs.events.on_play_sound": "Called every time a new sound is played\nTakes the following as arguments: the sound's ID, its world position, volume, pitch, if the sound should loop, the sound's category, and the sound's file path\nReturn true to prevent this sound from playing",
"figura.docs.events.resource_reload": "Called every time that the client resources are reloaded, allowing you to re-create or update resource texture references",
"figura.docs.events.get_events": "Returns a table with all events types",
"figura.docs.event": "A hook for a certain event in Minecraft\nYou may register functions to one, and those functions will be called when the event occurs",
Expand Down
Loading