package ar.com.companeros.horror; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.Arrays; import java.util.Objects; import java.util.UUID; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.*; import net.minecraftforge.network.simple.SimpleChannel; /** Preferencias y voz enviada sólo a los observadores autorizados. */ public final class HorrorNetwork { public static Consumer CLIENT_RECEIVER = enabled -> { }; public static Consumer CLIENT_SPEECH = speech -> { }; public static final int MAX_SPEECH_BYTES = 192 * 1024; private static final String VERSION = "3"; private static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( new ResourceLocation("companeros", "horror"), () -> VERSION, VERSION::equals, VERSION::equals); private static boolean registered; private HorrorNetwork() { } public static synchronized void register() { if (registered) return; registered = true; CHANNEL.messageBuilder(Sensitivity.class, 0, NetworkDirection.PLAY_TO_CLIENT) .encoder(Sensitivity::encode).decoder(Sensitivity::decode).consumerMainThread(Sensitivity::handle).add(); CHANNEL.messageBuilder(Speech.class, 1, NetworkDirection.PLAY_TO_CLIENT) .encoder(Speech::encode).decoder(Speech::decode).consumerMainThread(Speech::handle).add(); } public static void send(ServerPlayer player, boolean enabled) { CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), new Sensitivity(enabled)); } // VOZ: no transmite rutas, servicios ni URLs; únicamente un OGG pequeño en memoria. public static void sendSpeech(ServerPlayer player, ParasiteEntity body, byte[] audio) { var server = player.getServer(); if (server == null || !HorrorModule.mayTarget(server, player) || player.level() != body.level() || !body.isAlive() || player.distanceToSqr(body) > 48 * 48) return; CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), new Speech(body.getUUID(), body.getX(), body.getEyeY(), body.getZ(), audio)); } public static boolean validSpeechBytes(byte[] audio) { if (audio == null || audio.length < 36 || audio.length > MAX_SPEECH_BYTES || audio[0] != 'O' || audio[1] != 'g' || audio[2] != 'g' || audio[3] != 'S' || audio[4] != 0) return false; int firstPacket = 27 + Byte.toUnsignedInt(audio[26]); if (firstPacket + 30 > audio.length || audio[firstPacket] != 1) return false; byte[] vorbis = { 'v', 'o', 'r', 'b', 'i', 's' }; for (int index = 0; index < vorbis.length; index++) if (audio[firstPacket + 1 + index] != vorbis[index]) return false; int channels = Byte.toUnsignedInt(audio[firstPacket + 11]); int rate = Byte.toUnsignedInt(audio[firstPacket + 12]) | Byte.toUnsignedInt(audio[firstPacket + 13]) << 8 | Byte.toUnsignedInt(audio[firstPacket + 14]) << 16 | Byte.toUnsignedInt(audio[firstPacket + 15]) << 24; return (channels == 1 || channels == 2) && rate >= 8000 && rate <= 48000; } public record Speech(UUID body, double x, double y, double z, byte[] audio) { public Speech { Objects.requireNonNull(body); if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z) || Math.abs(x) > 30_000_000 || Math.abs(z) > 30_000_000 || Math.abs(y) > 4096 || !validSpeechBytes(audio)) throw new IllegalArgumentException("Invalid parasite speech packet"); audio = Arrays.copyOf(audio, audio.length); } @Override public byte[] audio() { return Arrays.copyOf(audio, audio.length); } private void encode(FriendlyByteBuf buffer) { buffer.writeUUID(body); buffer.writeDouble(x); buffer.writeDouble(y); buffer.writeDouble(z); buffer.writeByteArray(audio); } private static Speech decode(FriendlyByteBuf buffer) { return new Speech(buffer.readUUID(), buffer.readDouble(), buffer.readDouble(), buffer.readDouble(), buffer.readByteArray(MAX_SPEECH_BYTES)); } private void handle(Supplier context) { CLIENT_SPEECH.accept(this); context.get().setPacketHandled(true); } } public record Sensitivity(boolean enabled) { private void encode(FriendlyByteBuf buffer) { buffer.writeBoolean(enabled); } private static Sensitivity decode(FriendlyByteBuf buffer) { return new Sensitivity(buffer.readBoolean()); } private void handle(Supplier context) { CLIENT_RECEIVER.accept(enabled); context.get().setPacketHandled(true); } } }