Add comparator output, tab-completion, soundlist/soundstopall, and OpenAL low-pass occlusion/underwater filter

This commit is contained in:
rardog
2026-09-24 13:37:01 +03:00
parent cd3672a274
commit 9bee71c98b
5 changed files with 170 additions and 5 deletions
@@ -58,6 +58,7 @@ import javazoom.jl.decoder.SampleBuffer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.player.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.tags.FluidTags;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.util.math.RayTraceContext;
@@ -65,6 +66,8 @@ import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.vector.Vector3d;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.EXTEfx;
import org.lwjgl.stb.STBVorbis;
import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.MemoryStack;
@@ -200,6 +203,14 @@ public final class AudioPlaybackService {
return;
}
SoundDebug.log(SoundDebug.Level.L2, "PLAYBACK", sourceId, "stop source={} buffer={}", src.source, src.buffer);
if (src.directFilter > 0) {
try {
AL10.alSourcei(src.source, EXTEfx.AL_DIRECT_FILTER, EXTEfx.AL_FILTER_NULL);
EXTEfx.alDeleteFilters(src.directFilter);
} catch (Throwable ignored) {
}
src.directFilter = 0;
}
AL10.alSourceStop((int)src.source);
AL10.alDeleteSources((int)src.source);
if (src.reverbSource > 0) {
@@ -209,6 +220,13 @@ public final class AudioPlaybackService {
AL10.alDeleteBuffers((int)src.buffer);
}
public synchronized void stopAll() {
ArrayList<String> all = new ArrayList<>(this.active.keySet());
for (String id : all) {
this.stop(id);
}
}
public synchronized boolean isPlaying(String sourceId) {
return this.active.containsKey(sourceId);
}
@@ -223,6 +241,7 @@ public final class AudioPlaybackService {
return;
}
Vector3d playerPos = player.position();
boolean underwater = player.isEyeInFluid(FluidTags.WATER);
ArrayList<String> remove = new ArrayList<String>();
int topPriority = Integer.MIN_VALUE;
String dominantCategory = "ambient";
@@ -248,9 +267,13 @@ public final class AudioPlaybackService {
float gain = this.computeAttenuatedGain(src, playerPos);
float occlusionFactor = this.computeOcclusionFactor(src, playerPos);
gain *= occlusionFactor;
if (underwater) {
gain *= 0.72f;
}
gain = this.applyFades(src, gain);
gain = this.applyPriorityDucking(src, gain, topPriority, dominantCategory);
AL10.alSourcef((int)src.source, (int)4106, (float)Math.max(0.0f, gain));
this.updateDirectFilter(src, occlusionFactor, underwater);
float pitch = src.settings.pitch;
pitch *= this.computePitchModulation(src);
if (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f && !"voice".equals(src.settings.category)) {
@@ -276,6 +299,63 @@ public final class AudioPlaybackService {
}
}
private static boolean efxSupported = false;
private static boolean efxChecked = false;
private static boolean isEfxSupported() {
if (!efxChecked) {
efxChecked = true;
try {
long context = ALC10.alcGetCurrentContext();
long device = ALC10.alcGetContextsDevice(context);
efxSupported = ALC10.alcIsExtensionPresent(device, "ALC_EXT_EFX");
} catch (Throwable t) {
efxSupported = false;
}
}
return efxSupported;
}
private void updateDirectFilter(PlayingSource src, float occlusionFactor, boolean underwater) {
if (!isEfxSupported()) {
return;
}
boolean needFilter = (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f) || underwater;
if (!needFilter) {
if (src.directFilter > 0) {
try {
AL10.alSourcei(src.source, EXTEfx.AL_DIRECT_FILTER, EXTEfx.AL_FILTER_NULL);
} catch (Throwable ignored) {}
}
return;
}
try {
if (src.directFilter <= 0) {
src.directFilter = EXTEfx.alGenFilters();
if (src.directFilter > 0) {
EXTEfx.alFilteri(src.directFilter, EXTEfx.AL_FILTER_TYPE, EXTEfx.AL_FILTER_LOWPASS);
}
}
if (src.directFilter > 0) {
float gainHF = 1.0f;
float gainLF = 1.0f;
if (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f) {
float str = Math.max(0.0f, Math.min(1.0f, src.settings.occlusionStrength));
gainHF = Math.max(0.08f, 1.0f - str * 0.82f);
}
if (underwater) {
gainHF *= 0.18f;
gainLF *= 0.75f;
}
EXTEfx.alFilterf(src.directFilter, EXTEfx.AL_LOWPASS_GAIN, gainLF);
EXTEfx.alFilterf(src.directFilter, EXTEfx.AL_LOWPASS_GAINHF, gainHF);
AL10.alSourcei(src.source, EXTEfx.AL_DIRECT_FILTER, src.directFilter);
}
} catch (Throwable ignored) {
}
}
private float computePitchModulation(PlayingSource src) {
if (src.settings.pitchMode == null || src.settings.pitchMode == PitchModulationMode.STATIC) {
return 1.0f;
@@ -653,6 +733,7 @@ public final class AudioPlaybackService {
int fadeOutTicks;
int reverbSource;
int elapsedTicks;
int directFilter;
long lastLiveUpdateTick = Long.MIN_VALUE;
PlayingSource(String sourceId, int source, int buffer, BlockPos pos, SoundSettings settings, long startedAtTick) {
@@ -40,9 +40,12 @@ import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@@ -60,18 +63,32 @@ import net.minecraftforge.fml.network.PacketDistributor;
public final class ModCommands {
private static final Map<String, DiagRequest> DIAG_REQUESTS = new ConcurrentHashMap<String, DiagRequest>();
private static final SuggestionProvider<CommandSource> SOUND_SUGGESTIONS = (ctx, builder) -> {
ServerSoundRepository repo = ServerSoundRepository.get();
if (repo != null) {
for (SoundManifestEntry entry : repo.getManifest()) {
if (entry.flags == 0) {
builder.suggest(entry.soundId);
}
}
}
return builder.buildFuture();
};
private ModCommands() {
}
public static void register(CommandDispatcher<CommandSource> dispatcher) {
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"playsoundcustom").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).then(Commands.argument((String)"pos", (ArgumentType)Vec3Argument.vec3()).executes(ctx -> ModCommands.playCustom((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound"), Vec3Argument.getVec3((CommandContext)ctx, (String)"pos"))))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"playsoundcustom").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).then(Commands.argument((String)"pos", (ArgumentType)Vec3Argument.vec3()).executes(ctx -> ModCommands.playCustom((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound"), Vec3Argument.getVec3((CommandContext)ctx, (String)"pos"))))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"stopsoundcustom").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.stopCustom((CommandContext<CommandSource>)ctx)));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundstopall").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.stopAllCustom((CommandContext<CommandSource>)ctx)));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundlist").requires(src -> src.hasPermission(0))).executes(ctx -> ModCommands.listSounds((CommandContext<CommandSource>)ctx)));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddebug").requires(src -> src.hasPermission(2))).then(Commands.literal((String)"on").executes(ctx -> ModCommands.setDebug((CommandContext<CommandSource>)ctx, true)))).then(Commands.literal((String)"off").executes(ctx -> ModCommands.setDebug((CommandContext<CommandSource>)ctx, false)))).then(Commands.literal((String)"level").then(Commands.argument((String)"level", (ArgumentType)IntegerArgumentType.integer((int)1, (int)3)).executes(ctx -> ModCommands.setDebugLevel((CommandContext<CommandSource>)ctx, IntegerArgumentType.getInteger((CommandContext)ctx, (String)"level"))))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundreload").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.reload((CommandContext<CommandSource>)ctx)));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundsync").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).executes(ctx -> ModCommands.syncSound((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundsync").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).executes(ctx -> ModCommands.syncSound((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundcacheclear").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.clearCache((CommandContext<CommandSource>)ctx)));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiag").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).executes(ctx -> ModCommands.soundDiag((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound")))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiagclient").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).executes(ctx -> ModCommands.soundDiagClient((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiag").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).executes(ctx -> ModCommands.soundDiag((CommandContext<CommandSource>)ctx, StringArgumentType.getString((CommandContext)ctx, (String)"sound")))));
dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"sounddiagclient").requires(src -> src.hasPermission(2))).then(Commands.argument((String)"player", (ArgumentType)EntityArgument.player()).then(Commands.argument((String)"sound", (ArgumentType)StringArgumentType.word()).suggests(SOUND_SUGGESTIONS).executes(ctx -> ModCommands.soundDiagClient((CommandContext<CommandSource>)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound"))))));
}
private static int playCustom(CommandContext<CommandSource> ctx, String sound, Vector3d pos) {
@@ -96,6 +113,32 @@ public final class ModCommands {
return 1;
}
private static int stopAllCustom(CommandContext<CommandSource> ctx) {
for (ServerPlayerEntity player : ((CommandSource)ctx.getSource()).getLevel().players()) {
NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), (Object)new S2CStopSoundPacket("*"));
}
((CommandSource)ctx.getSource()).sendSuccess((ITextComponent)new StringTextComponent("§a[SoundMod] Все пользовательские звуки принудительно остановлены."), true);
return 1;
}
private static int listSounds(CommandContext<CommandSource> ctx) {
CommandSource src = (CommandSource) ctx.getSource();
List<SoundManifestEntry> list = ServerSoundRepository.get().getManifest();
if (list.isEmpty()) {
src.sendSuccess((ITextComponent)new StringTextComponent("§7[SoundMod] Список звуков пуст. Добавьте .ogg, .wav или .mp3 в customsounds/"), false);
return 0;
}
src.sendSuccess((ITextComponent)new StringTextComponent("§6=== Список доступных звуков (" + list.size() + ") ==="), false);
for (SoundManifestEntry e : list) {
String sizeStr = (e.size >= 1024 * 1024)
? String.format(Locale.ROOT, "%.2f MB", e.size / (1024.0 * 1024.0))
: String.format(Locale.ROOT, "%.1f KB", e.size / 1024.0);
String status = e.flags == 0 ? "§a[OK]" : "§c[Ошибка]";
src.sendSuccess((ITextComponent)new StringTextComponent("§e• §f" + e.soundId + " §7(" + e.relativePath + ", " + sizeStr + ") " + status), false);
}
return list.size();
}
private static int setDebug(CommandContext<CommandSource> ctx, boolean enabled) {
SoundDebug.setEnabled(enabled);
int levelCode = SoundDebug.getLevel().code();
@@ -80,6 +80,28 @@ public class SoundPlayerBlock extends Block {
}
}
@Override
public boolean hasAnalogOutputSignal(BlockState state) {
return true;
}
@Override
public int getAnalogOutputSignal(BlockState state, World world, BlockPos pos) {
TileEntity te = world.getBlockEntity(pos);
if (te instanceof SoundPlayerTileEntity) {
SoundPlayerTileEntity soundTe = (SoundPlayerTileEntity) te;
if (soundTe.isPlaying()) {
if (soundTe.getScenario() != null && !soundTe.getScenario().steps.isEmpty()) {
int total = soundTe.getScenario().steps.size();
int cur = soundTe.getScenarioStepIndex();
return Math.max(1, Math.min(15, (int) Math.ceil(((double) (cur + 1) / total) * 15.0)));
}
return 15;
}
}
return 0;
}
@Override
@OnlyIn(Dist.CLIENT)
public void animateTick(BlockState state, World world, BlockPos pos, Random rand) {
@@ -292,6 +292,7 @@ ITickableTileEntity {
SoundDebug.log(SoundDebug.Level.L2, "TRIGGER", this.debugPosId(), "stop requested sourceId=sound_player:{}", this.worldPosition.asLong());
NetworkHandler.CHANNEL.send(PacketDistributor.TRACKING_CHUNK.with(() -> this.level.getChunkAt(this.worldPosition)), (Object)new S2CBlockStateSyncPacket(this.worldPosition, this.save(new CompoundNBT()), false));
this.setChanged();
this.notifyNeighbors();
}
}
@@ -543,6 +544,7 @@ ITickableTileEntity {
this.playing = true;
SoundDebug.log(SoundDebug.Level.L1, "TRIGGER", this.debugPosId(), "play requested sourceId=sound_player:{} soundId={}", this.worldPosition.asLong(), this.settings.soundId);
NetworkHandler.CHANNEL.send(PacketDistributor.TRACKING_CHUNK.with(() -> this.level.getChunkAt(this.worldPosition)), (Object)new S2CBlockStateSyncPacket(this.worldPosition, this.save(new CompoundNBT()), true));
this.notifyNeighbors();
}
private void emitScenarioStepPlay(String soundId) {
@@ -551,6 +553,17 @@ ITickableTileEntity {
this.playing = true;
SoundDebug.log(SoundDebug.Level.L1, "TRIGGER", this.debugPosId(), "scenario step play sourceId=sound_player:{} stepSoundId={} nextDelay={}", this.worldPosition.asLong(), soundId, this.scenarioWaitTicks);
NetworkHandler.CHANNEL.send(PacketDistributor.TRACKING_CHUNK.with(() -> this.level.getChunkAt(this.worldPosition)), (Object)new S2CPlaySoundPacket("sound_player:" + this.worldPosition.asLong(), this.worldPosition, stepSettings));
this.notifyNeighbors();
}
public int getScenarioStepIndex() {
return this.scenarioStepIndex;
}
private void notifyNeighbors() {
if (this.level != null && !this.level.isClientSide) {
this.level.updateNeighborsAt(this.worldPosition, this.getBlockState().getBlock());
}
}
private boolean isSourceRuntimeActive() {
@@ -28,7 +28,13 @@ public class S2CStopSoundPacket {
}
public static void handle(S2CStopSoundPacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> AudioPlaybackService.get().stop(msg.sourceId));
ctx.get().enqueueWork(() -> {
if (msg.sourceId == null || msg.sourceId.isEmpty() || "*".equals(msg.sourceId)) {
AudioPlaybackService.get().stopAll();
} else {
AudioPlaybackService.get().stop(msg.sourceId);
}
});
ctx.get().setPacketHandled(true);
}
}