Volver al índice

src/main/java/ar/com/companeros/horror/client/HorrorSpeechClient.java

package ar.com.companeros.horror.client;

import ar.com.companeros.Companeros;
import ar.com.companeros.horror.HorrorModule;
import ar.com.companeros.horror.HorrorNetwork;
import ar.com.companeros.horror.ParasiteEntity;
import com.mojang.blaze3d.audio.OggAudioStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance;
import net.minecraft.client.resources.sounds.Sound;
import net.minecraft.client.sounds.AudioStream;
import net.minecraft.client.sounds.SoundBufferLibrary;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

/** Reproduce voz generada en memoria; jamás abre archivos ni contacta servicios. */
@Mod.EventBusSubscriber(modid = Companeros.ID, value = Dist.CLIENT)
public final class HorrorSpeechClient {
    private static final Map<UUID, GeneratedSpeech> ACTIVE = new LinkedHashMap<>();
    private static long lastPacketTime;
    private HorrorSpeechClient() {}

    // RECEPCIÓN: una voz por cuerpo y sólo para un jugador que acepta el modo de terror.
    public static void play(HorrorNetwork.Speech speech) {
        Minecraft minecraft = Minecraft.getInstance();
        if (HorrorClientSafety.isEnabled() || minecraft.level == null || minecraft.player == null
            || minecraft.player.isCreative() || minecraft.player.isSpectator()) return;
        long now = Util.getMillis();
        if (now - lastPacketTime < 500) return;
        ParasiteEntity body = null;
        for (Entity entity : minecraft.level.entitiesForRendering()) {
            if (entity instanceof ParasiteEntity parasite && entity.getUUID().equals(speech.body())) {
                body = parasite;
                break;
            }
        }
        if (body == null || !body.isAlive() || minecraft.player.distanceToSqr(body) > 48 * 48) return;
        // El relato fijo también usa este evento: se corta antes de la nueva frase.
        stopAll();
        minecraft.getSoundManager().stop(HorrorModule.PARASITE_LORE.get().getLocation(), SoundSource.HOSTILE);
        lastPacketTime = now;
        GeneratedSpeech sound = new GeneratedSpeech(speech, body);
        ACTIVE.put(speech.body(), sound);
        minecraft.getSoundManager().play(sound);
    }

    public static void stopAll() {
        for (GeneratedSpeech sound : ACTIVE.values().toArray(GeneratedSpeech[]::new)) sound.cancel();
        ACTIVE.clear();
        lastPacketTime = 0;
    }

    @SubscribeEvent public static void disconnected(ClientPlayerNetworkEvent.LoggingOut event) { stopAll(); }

    // LIMPIEZA: StopSound del servidor y fin del audio también liberan sus bytes locales.
    @SubscribeEvent public static void clientTick(TickEvent.ClientTickEvent event) {
        if (event.phase != TickEvent.Phase.END || ACTIVE.isEmpty()) return;
        Minecraft minecraft = Minecraft.getInstance();
        if (HorrorClientSafety.isEnabled() || minecraft.level == null) { stopAll(); return; }
        for (GeneratedSpeech sound : ACTIVE.values().toArray(GeneratedSpeech[]::new)) {
            if (sound.age > 20 && !minecraft.getSoundManager().isActive(sound)) sound.cancel();
        }
    }

    private static final class GeneratedSpeech extends AbstractTickableSoundInstance {
        private final UUID bodyId;
        private final ParasiteEntity body;
        private byte[] audio;
        private volatile boolean cancelled;
        private int age;

        private GeneratedSpeech(HorrorNetwork.Speech speech, ParasiteEntity body) {
            super(HorrorModule.PARASITE_LORE.get(), SoundSource.HOSTILE, RandomSource.create());
            this.bodyId = speech.body();
            this.body = body;
            this.audio = speech.audio();
            x = speech.x(); y = speech.y(); z = speech.z();
            volume = .95F; pitch = 1;
            looping = false; relative = false;
            attenuation = Attenuation.LINEAR;
        }

        @Override public boolean canPlaySound() { return !cancelled && !HorrorClientSafety.isEnabled(); }

        @Override public void tick() {
            Minecraft minecraft = Minecraft.getInstance();
            if (++age > 20 * 75 || cancelled || HorrorClientSafety.isEnabled() || !body.isAlive()
                || minecraft.level != body.level() || minecraft.player == null || minecraft.player.isCreative()
                || minecraft.player.isSpectator() || minecraft.player.distanceToSqr(body) > 64 * 64) {
                cancel();
                return;
            }
            x = body.getX(); y = body.getEyeY(); z = body.getZ();
        }

        // El constructor del decodificador corre fuera del hilo de renderizado.
        @Override public CompletableFuture<AudioStream> getStream(SoundBufferLibrary buffers, Sound sound, boolean loop) {
            byte[] bytes = audio;
            return CompletableFuture.supplyAsync(() -> {
                try {
                    if (cancelled || HorrorClientSafety.isEnabled()) throw new IOException("Speech cancelled");
                    OggAudioStream stream = new OggAudioStream(new ByteArrayInputStream(bytes));
                    if (cancelled || HorrorClientSafety.isEnabled()) {
                        stream.close();
                        throw new IOException("Speech cancelled");
                    }
                    return stream;
                } catch (IOException error) { throw new CompletionException(error); }
            }, Util.backgroundExecutor());
        }

        private void cancel() {
            if (cancelled) return;
            cancelled = true;
            stop();
            Minecraft.getInstance().getSoundManager().stop(this);
            ACTIVE.remove(bodyId, this);
            audio = new byte[0];
        }
    }
}