From 9bee71c98bcefd5085e6ba1aed04bdbc33691a0e Mon Sep 17 00:00:00 2001 From: rardog Date: Thu, 24 Sep 2026 13:37:01 +0300 Subject: [PATCH] Add comparator output, tab-completion, soundlist/soundstopall, and OpenAL low-pass occlusion/underwater filter --- .../client/audio/AudioPlaybackService.java | 81 +++++++++++++++++++ .../example/soundmod/command/ModCommands.java | 51 +++++++++++- .../gameplay/block/SoundPlayerBlock.java | 22 +++++ .../gameplay/tile/SoundPlayerTileEntity.java | 13 +++ .../net/packet/S2CStopSoundPacket.java | 8 +- 5 files changed, 170 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java b/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java index 9dc04b1..54d5ccd 100644 --- a/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java +++ b/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java @@ -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 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 remove = new ArrayList(); 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) { diff --git a/src/main/java/com/example/soundmod/command/ModCommands.java b/src/main/java/com/example/soundmod/command/ModCommands.java index 5deeef7..2783d59 100644 --- a/src/main/java/com/example/soundmod/command/ModCommands.java +++ b/src/main/java/com/example/soundmod/command/ModCommands.java @@ -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 DIAG_REQUESTS = new ConcurrentHashMap(); + private static final SuggestionProvider 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 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)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)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)ctx))); + dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundstopall").requires(src -> src.hasPermission(2))).executes(ctx -> ModCommands.stopAllCustom((CommandContext)ctx))); + dispatcher.register((LiteralArgumentBuilder)((LiteralArgumentBuilder)Commands.literal((String)"soundlist").requires(src -> src.hasPermission(0))).executes(ctx -> ModCommands.listSounds((CommandContext)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)ctx, true)))).then(Commands.literal((String)"off").executes(ctx -> ModCommands.setDebug((CommandContext)ctx, false)))).then(Commands.literal((String)"level").then(Commands.argument((String)"level", (ArgumentType)IntegerArgumentType.integer((int)1, (int)3)).executes(ctx -> ModCommands.setDebugLevel((CommandContext)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)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)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)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)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)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)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)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)ctx, EntityArgument.getPlayer((CommandContext)ctx, (String)"player"), StringArgumentType.getString((CommandContext)ctx, (String)"sound")))))); } private static int playCustom(CommandContext ctx, String sound, Vector3d pos) { @@ -96,6 +113,32 @@ public final class ModCommands { return 1; } + private static int stopAllCustom(CommandContext 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 ctx) { + CommandSource src = (CommandSource) ctx.getSource(); + List 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 ctx, boolean enabled) { SoundDebug.setEnabled(enabled); int levelCode = SoundDebug.getLevel().code(); diff --git a/src/main/java/com/example/soundmod/gameplay/block/SoundPlayerBlock.java b/src/main/java/com/example/soundmod/gameplay/block/SoundPlayerBlock.java index 8ca28b6..4e27091 100644 --- a/src/main/java/com/example/soundmod/gameplay/block/SoundPlayerBlock.java +++ b/src/main/java/com/example/soundmod/gameplay/block/SoundPlayerBlock.java @@ -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) { diff --git a/src/main/java/com/example/soundmod/gameplay/tile/SoundPlayerTileEntity.java b/src/main/java/com/example/soundmod/gameplay/tile/SoundPlayerTileEntity.java index 65093c5..6c22c4a 100644 --- a/src/main/java/com/example/soundmod/gameplay/tile/SoundPlayerTileEntity.java +++ b/src/main/java/com/example/soundmod/gameplay/tile/SoundPlayerTileEntity.java @@ -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() { diff --git a/src/main/java/com/example/soundmod/net/packet/S2CStopSoundPacket.java b/src/main/java/com/example/soundmod/net/packet/S2CStopSoundPacket.java index f5f5b01..0e99d8b 100644 --- a/src/main/java/com/example/soundmod/net/packet/S2CStopSoundPacket.java +++ b/src/main/java/com/example/soundmod/net/packet/S2CStopSoundPacket.java @@ -28,7 +28,13 @@ public class S2CStopSoundPacket { } public static void handle(S2CStopSoundPacket msg, Supplier 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); } }