diff options
Diffstat (limited to 'src/main/java/eu')
7 files changed, 537 insertions, 11 deletions
diff --git a/src/main/java/eu/projnull/spelis/svci/Intercom.java b/src/main/java/eu/projnull/spelis/svci/Intercom.java index 4f40019..b7808ef 100644 --- a/src/main/java/eu/projnull/spelis/svci/Intercom.java +++ b/src/main/java/eu/projnull/spelis/svci/Intercom.java @@ -3,6 +3,7 @@ package eu.projnull.spelis.svci; import de.maxhenkel.voicechat.api.BukkitVoicechatService; import eu.projnull.spelis.svci.commands.IntercomCommand; import eu.projnull.spelis.svci.misc.BroadcastHudTask; +import eu.projnull.spelis.svci.voice.SpeakerManager; import eu.projnull.spelis.svci.voice.VoicePlugin; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import org.apache.logging.log4j.LogManager; @@ -23,6 +24,9 @@ public final class Intercom extends JavaPlugin { @Override public void onEnable() { + // Initialize speaker system + SpeakerManager.inst().initialize(this.getDataFolder()); + BukkitVoicechatService service = getServer().getServicesManager().load(BukkitVoicechatService.class); if (service != null) { voicechatPlugin = new VoicePlugin(); diff --git a/src/main/java/eu/projnull/spelis/svci/commands/IntercomCommand.java b/src/main/java/eu/projnull/spelis/svci/commands/IntercomCommand.java index 418408e..cb1972d 100644 --- a/src/main/java/eu/projnull/spelis/svci/commands/IntercomCommand.java +++ b/src/main/java/eu/projnull/spelis/svci/commands/IntercomCommand.java @@ -19,6 +19,7 @@ public class IntercomCommand { registerHandler(new LiveCommand()); registerHandler(new FileCommand()); registerHandler(new AboutCommand()); + registerHandler(new SpeakerCommand()); } public void registerHandler(Handler handler) { diff --git a/src/main/java/eu/projnull/spelis/svci/commands/handlers/FileCommand.java b/src/main/java/eu/projnull/spelis/svci/commands/handlers/FileCommand.java index 3d7fb4c..5a871a3 100644 --- a/src/main/java/eu/projnull/spelis/svci/commands/handlers/FileCommand.java +++ b/src/main/java/eu/projnull/spelis/svci/commands/handlers/FileCommand.java @@ -83,18 +83,46 @@ public class FileCommand implements Handler { BroadcasterState.Broadcaster broadcaster = new BroadcasterState.Broadcaster(null, world.getUID(), BroadcasterState.Broadcaster.BroadcastType.FILE, durationMillis, filename); BroadcasterState.inst().startBroadcast(broadcaster); - for (Player p : world.getPlayers()) { - VoicechatConnection connection = api.getConnectionOf(p.getUniqueId()); - if (connection == null) continue; + // Get all speakers in this world + java.util.List<eu.projnull.spelis.svci.voice.Speaker> speakers = eu.projnull.spelis.svci.voice.SpeakerManager.inst().getSpeakers(world.getUID()); + + if (speakers.isEmpty()) { + // Fallback to old behavior if no speakers are defined + for (Player p : world.getPlayers()) { + VoicechatConnection connection = api.getConnectionOf(p.getUniqueId()); + if (connection == null) continue; - StaticAudioChannel channel = api.createStaticAudioChannel(UUID.randomUUID(), level, connection); - if (channel == null) continue; + StaticAudioChannel channel = api.createStaticAudioChannel(UUID.randomUUID(), level, connection); + if (channel == null) continue; - AudioPlayer audioPlayer = api.createAudioPlayer(channel, api.createEncoder(), pcm); - audioPlayer.startPlaying(); - } + AudioPlayer audioPlayer = api.createAudioPlayer(channel, api.createEncoder(), pcm); + audioPlayer.startPlaying(); + } + sender.sendMessage("§aFile broadcast started: §f" + filename + " §ain §f" + world.getName() + " §7(no speakers, using global audio)"); + } else { + // Use positional audio from speakers + for (eu.projnull.spelis.svci.voice.Speaker speaker : speakers) { + org.bukkit.Location speakerLoc = speaker.getLocation(); + if (speakerLoc == null) continue; + + de.maxhenkel.voicechat.api.Position position = api.createPosition( + speakerLoc.getX(), + speakerLoc.getY(), + speakerLoc.getZ() + ); + + de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel channel = + api.createLocationalAudioChannel(UUID.randomUUID(), level, position); + + if (channel == null) continue; - sender.sendMessage("§aFile broadcast started: §f" + filename + " §ain §f" + world.getName()); + channel.setDistance((float) speaker.getRange()); + + AudioPlayer audioPlayer = api.createAudioPlayer(channel, api.createEncoder(), pcm); + audioPlayer.startPlaying(); + } + sender.sendMessage("§aFile broadcast started: §f" + filename + " §ain §f" + world.getName() + " §7(" + speakers.size() + " speakers)"); + } Bukkit.getScheduler().runTaskLaterAsynchronously(Intercom.getPlugin(Intercom.class), () -> BroadcasterState.inst().stopBroadcastWithMessage(world.getUID(), "§aThe broadcast has ended."), durationMillis / 50L); }); diff --git a/src/main/java/eu/projnull/spelis/svci/commands/handlers/SpeakerCommand.java b/src/main/java/eu/projnull/spelis/svci/commands/handlers/SpeakerCommand.java new file mode 100644 index 0000000..b175f4c --- /dev/null +++ b/src/main/java/eu/projnull/spelis/svci/commands/handlers/SpeakerCommand.java @@ -0,0 +1,204 @@ +package eu.projnull.spelis.svci.commands.handlers; + +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import eu.projnull.spelis.svci.commands.Handler; +import eu.projnull.spelis.svci.commands.Helpers; +import eu.projnull.spelis.svci.voice.Speaker; +import eu.projnull.spelis.svci.voice.SpeakerManager; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.UUID; + +public class SpeakerCommand implements Handler { + + @Override + public ArgumentBuilder<CommandSourceStack, ?> buildCommand() { + return Commands.literal("speaker") + .requires(cs -> cs.getSender().hasPermission("svcintercom.speaker")) + .then(buildAddCommand()) + .then(buildRemoveCommand()) + .then(buildListCommand()); + } + + private ArgumentBuilder<CommandSourceStack, ?> buildAddCommand() { + return Commands.literal("add") + .requires(cs -> cs.getSender().hasPermission("svcintercom.speaker.add")) + .then(Commands.argument("name", StringArgumentType.word()) + .then(Commands.argument("world", StringArgumentType.word()) + .suggests((ctx, builder) -> { + Helpers.getAllWorldsSuggestion(builder); + return builder.buildFuture(); + }) + .then(Commands.argument("x", DoubleArgumentType.doubleArg()) + .then(Commands.argument("y", DoubleArgumentType.doubleArg()) + .then(Commands.argument("z", DoubleArgumentType.doubleArg()) + .then(Commands.argument("range", DoubleArgumentType.doubleArg(1.0, 1000.0)) + .executes(ctx -> { + CommandSender sender = ctx.getSource().getSender(); + String name = StringArgumentType.getString(ctx, "name"); + String worldName = StringArgumentType.getString(ctx, "world"); + double x = DoubleArgumentType.getDouble(ctx, "x"); + double y = DoubleArgumentType.getDouble(ctx, "y"); + double z = DoubleArgumentType.getDouble(ctx, "z"); + double range = DoubleArgumentType.getDouble(ctx, "range"); + + World world = Bukkit.getWorld(worldName); + if (world == null) { + sender.sendMessage("§cWorld not found: " + worldName); + return 0; + } + + UUID worldId = world.getUID(); + if (SpeakerManager.inst().speakerExists(worldId, name)) { + sender.sendMessage("§cA speaker with that name already exists in this world"); + return 0; + } + + Speaker speaker = new Speaker(worldId, x, y, z, range, name); + SpeakerManager.inst().addSpeaker(speaker); + + sender.sendMessage("§aAdded speaker: §f" + speaker.toString()); + return 1; + }) + ) + ) + ) + ) + ) + // Add shorthand for current location (player only) + .then(Commands.argument("range", DoubleArgumentType.doubleArg(1.0, 1000.0)) + .executes(ctx -> { + CommandSender sender = ctx.getSource().getSender(); + if (!(sender instanceof Player player)) { + sender.sendMessage("§cYou must be a player to use this command without coordinates"); + return 0; + } + + String name = StringArgumentType.getString(ctx, "name"); + double range = DoubleArgumentType.getDouble(ctx, "range"); + + Location loc = player.getLocation(); + World world = loc.getWorld(); + UUID worldId = world.getUID(); + + if (SpeakerManager.inst().speakerExists(worldId, name)) { + sender.sendMessage("§cA speaker with that name already exists in this world"); + return 0; + } + + Speaker speaker = new Speaker(worldId, loc.getX(), loc.getY(), loc.getZ(), range, name); + SpeakerManager.inst().addSpeaker(speaker); + + sender.sendMessage("§aAdded speaker: §f" + speaker.toString()); + return 1; + }) + ) + ); + } + + private ArgumentBuilder<CommandSourceStack, ?> buildRemoveCommand() { + return Commands.literal("remove") + .requires(cs -> cs.getSender().hasPermission("svcintercom.speaker.remove")) + .then(Commands.argument("world", StringArgumentType.word()) + .suggests((ctx, builder) -> { + Helpers.getAllWorldsSuggestion(builder); + return builder.buildFuture(); + }) + .then(Commands.argument("name", StringArgumentType.word()) + .suggests((ctx, builder) -> { + String worldName = StringArgumentType.getString(ctx, "world"); + World world = Bukkit.getWorld(worldName); + if (world != null) { + List<Speaker> speakers = SpeakerManager.inst().getSpeakers(world.getUID()); + for (Speaker speaker : speakers) { + builder.suggest(speaker.getName()); + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + CommandSender sender = ctx.getSource().getSender(); + String worldName = StringArgumentType.getString(ctx, "world"); + String name = StringArgumentType.getString(ctx, "name"); + + World world = Bukkit.getWorld(worldName); + if (world == null) { + sender.sendMessage("§cWorld not found: " + worldName); + return 0; + } + + boolean removed = SpeakerManager.inst().removeSpeaker(world.getUID(), name); + if (removed) { + sender.sendMessage("§aRemoved speaker: §f" + name + " §afrom §f" + worldName); + return 1; + } else { + sender.sendMessage("§cSpeaker not found: " + name); + return 0; + } + }) + ) + ); + } + + private ArgumentBuilder<CommandSourceStack, ?> buildListCommand() { + return Commands.literal("list") + .requires(cs -> cs.getSender().hasPermission("svcintercom.speaker.list")) + .then(Commands.argument("world", StringArgumentType.word()) + .suggests((ctx, builder) -> { + Helpers.getAllWorldsSuggestion(builder); + return builder.buildFuture(); + }) + .executes(ctx -> { + CommandSender sender = ctx.getSource().getSender(); + String worldName = StringArgumentType.getString(ctx, "world"); + + World world = Bukkit.getWorld(worldName); + if (world == null) { + sender.sendMessage("§cWorld not found: " + worldName); + return 0; + } + + List<Speaker> speakers = SpeakerManager.inst().getSpeakers(world.getUID()); + if (speakers.isEmpty()) { + sender.sendMessage("§cNo speakers in world: " + worldName); + return 0; + } + + sender.sendMessage("§aSpeakers in §f" + worldName + "§a:"); + for (Speaker speaker : speakers) { + sender.sendMessage(" §f- " + speaker.toString()); + } + return 1; + }) + ) + .executes(ctx -> { + CommandSender sender = ctx.getSource().getSender(); + if (!(sender instanceof Player player)) { + sender.sendMessage("§cYou must specify a world or be a player"); + return 0; + } + + World world = player.getWorld(); + List<Speaker> speakers = SpeakerManager.inst().getSpeakers(world.getUID()); + if (speakers.isEmpty()) { + sender.sendMessage("§cNo speakers in your current world"); + return 0; + } + + sender.sendMessage("§aSpeakers in §f" + world.getName() + "§a:"); + for (Speaker speaker : speakers) { + sender.sendMessage(" §f- " + speaker.toString()); + } + return 1; + }); + } +} diff --git a/src/main/java/eu/projnull/spelis/svci/voice/Speaker.java b/src/main/java/eu/projnull/spelis/svci/voice/Speaker.java new file mode 100644 index 0000000..0791831 --- /dev/null +++ b/src/main/java/eu/projnull/spelis/svci/voice/Speaker.java @@ -0,0 +1,74 @@ +package eu.projnull.spelis.svci.voice; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.UUID; + +public class Speaker { + private final UUID worldId; + private final double x; + private final double y; + private final double z; + private final double range; + private final String name; + + public Speaker(UUID worldId, double x, double y, double z, double range, String name) { + this.worldId = worldId; + this.x = x; + this.y = y; + this.z = z; + this.range = range; + this.name = name; + } + + public UUID getWorldId() { + return worldId; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public double getRange() { + return range; + } + + public String getName() { + return name; + } + + public Location getLocation() { + World world = Bukkit.getWorld(worldId); + if (world == null) return null; + return new Location(world, x, y, z); + } + + public boolean isInRange(Location playerLocation) { + if (playerLocation == null || playerLocation.getWorld() == null) return false; + if (!playerLocation.getWorld().getUID().equals(worldId)) return false; + + double dx = playerLocation.getX() - x; + double dy = playerLocation.getY() - y; + double dz = playerLocation.getZ() - z; + double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + return distance <= range; + } + + @Override + public String toString() { + World world = Bukkit.getWorld(worldId); + String worldName = world != null ? world.getName() : worldId.toString(); + return String.format("%s @ %s (%.1f, %.1f, %.1f) range: %.1f", name, worldName, x, y, z, range); + } +} diff --git a/src/main/java/eu/projnull/spelis/svci/voice/SpeakerManager.java b/src/main/java/eu/projnull/spelis/svci/voice/SpeakerManager.java new file mode 100644 index 0000000..113639f --- /dev/null +++ b/src/main/java/eu/projnull/spelis/svci/voice/SpeakerManager.java @@ -0,0 +1,164 @@ +package eu.projnull.spelis.svci.voice; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import eu.projnull.spelis.svci.Intercom; +import org.bukkit.Location; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class SpeakerManager { + private static final SpeakerManager INSTANCE = new SpeakerManager(); + private final Map<UUID, List<Speaker>> speakers = new ConcurrentHashMap<>(); + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private File dataFile; + + private SpeakerManager() { + } + + public static SpeakerManager inst() { + return INSTANCE; + } + + public void initialize(File dataFolder) { + this.dataFile = new File(dataFolder, "speakers.json"); + load(); + } + + public void addSpeaker(Speaker speaker) { + speakers.computeIfAbsent(speaker.getWorldId(), k -> new ArrayList<>()).add(speaker); + save(); + } + + public boolean removeSpeaker(UUID worldId, String name) { + List<Speaker> worldSpeakers = speakers.get(worldId); + if (worldSpeakers == null) return false; + + boolean removed = worldSpeakers.removeIf(s -> s.getName().equalsIgnoreCase(name)); + if (removed) { + if (worldSpeakers.isEmpty()) { + speakers.remove(worldId); + } + save(); + } + return removed; + } + + public List<Speaker> getSpeakers(UUID worldId) { + return speakers.getOrDefault(worldId, Collections.emptyList()); + } + + public List<Speaker> getSpeakersInRange(Location location) { + if (location == null || location.getWorld() == null) return Collections.emptyList(); + + List<Speaker> worldSpeakers = speakers.get(location.getWorld().getUID()); + if (worldSpeakers == null) return Collections.emptyList(); + + return worldSpeakers.stream() + .filter(speaker -> speaker.isInRange(location)) + .collect(Collectors.toList()); + } + + public Speaker getSpeaker(UUID worldId, String name) { + List<Speaker> worldSpeakers = speakers.get(worldId); + if (worldSpeakers == null) return null; + + return worldSpeakers.stream() + .filter(s -> s.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + public boolean speakerExists(UUID worldId, String name) { + return getSpeaker(worldId, name) != null; + } + + private void save() { + try { + dataFile.getParentFile().mkdirs(); + + List<SpeakerData> data = new ArrayList<>(); + for (Map.Entry<UUID, List<Speaker>> entry : speakers.entrySet()) { + for (Speaker speaker : entry.getValue()) { + data.add(new SpeakerData( + speaker.getWorldId().toString(), + speaker.getX(), + speaker.getY(), + speaker.getZ(), + speaker.getRange(), + speaker.getName() + )); + } + } + + try (FileWriter writer = new FileWriter(dataFile)) { + gson.toJson(data, writer); + } + + Intercom.LOGGER.info("Saved {} speakers to disk", data.size()); + } catch (IOException e) { + Intercom.LOGGER.error("Failed to save speakers", e); + } + } + + private void load() { + if (!dataFile.exists()) { + Intercom.LOGGER.info("No speakers file found, starting fresh"); + return; + } + + try (FileReader reader = new FileReader(dataFile)) { + Type listType = new TypeToken<List<SpeakerData>>() {}.getType(); + List<SpeakerData> data = gson.fromJson(reader, listType); + + if (data == null) { + Intercom.LOGGER.warn("Speakers file is empty or corrupted"); + return; + } + + speakers.clear(); + for (SpeakerData speakerData : data) { + UUID worldId = UUID.fromString(speakerData.worldId); + Speaker speaker = new Speaker( + worldId, + speakerData.x, + speakerData.y, + speakerData.z, + speakerData.range, + speakerData.name + ); + speakers.computeIfAbsent(worldId, k -> new ArrayList<>()).add(speaker); + } + + Intercom.LOGGER.info("Loaded {} speakers from disk", data.size()); + } catch (IOException e) { + Intercom.LOGGER.error("Failed to load speakers", e); + } + } + + private static class SpeakerData { + String worldId; + double x; + double y; + double z; + double range; + String name; + + SpeakerData(String worldId, double x, double y, double z, double range, String name) { + this.worldId = worldId; + this.x = x; + this.y = y; + this.z = z; + this.range = range; + this.name = name; + } + } +} diff --git a/src/main/java/eu/projnull/spelis/svci/voice/VoicePlugin.java b/src/main/java/eu/projnull/spelis/svci/voice/VoicePlugin.java index 55b19ca..ff6575e 100644 --- a/src/main/java/eu/projnull/spelis/svci/voice/VoicePlugin.java +++ b/src/main/java/eu/projnull/spelis/svci/voice/VoicePlugin.java @@ -59,16 +59,67 @@ public class VoicePlugin implements VoicechatPlugin { VoicechatServerApi api = event.getVoicechat(); + // Get all speakers in this world + java.util.List<Speaker> speakers = SpeakerManager.inst().getSpeakers(worldId); + + if (speakers.isEmpty()) { + // Fallback to old behavior if no speakers are defined + for (org.bukkit.entity.Player online : player.getWorld().getPlayers()) { + if (online.getUniqueId().equals(player.getUniqueId())) { + continue; + } + + VoicechatConnection connection = api.getConnectionOf(online.getUniqueId()); + + if (connection == null) continue; + + api.sendStaticSoundPacketTo(connection, event.getPacket().toStaticSoundPacket()); + } + return; + } + + // Use positional audio from speakers for (org.bukkit.entity.Player online : player.getWorld().getPlayers()) { if (online.getUniqueId().equals(player.getUniqueId())) { continue; } VoicechatConnection connection = api.getConnectionOf(online.getUniqueId()); - if (connection == null) continue; - api.sendStaticSoundPacketTo(connection, event.getPacket().toStaticSoundPacket()); + // Find nearest speaker to the player + Speaker nearestSpeaker = null; + double nearestDistance = Double.MAX_VALUE; + + for (Speaker speaker : speakers) { + if (speaker.isInRange(online.getLocation())) { + org.bukkit.Location speakerLoc = speaker.getLocation(); + if (speakerLoc == null) continue; + + double distance = online.getLocation().distance(speakerLoc); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestSpeaker = speaker; + } + } + } + + // Only send audio if player is in range of at least one speaker + if (nearestSpeaker != null) { + org.bukkit.Location speakerLoc = nearestSpeaker.getLocation(); + + // Create a locational sound packet from the speaker position + de.maxhenkel.voicechat.api.Position position = api.createPosition( + speakerLoc.getX(), + speakerLoc.getY(), + speakerLoc.getZ() + ); + + api.sendLocationalSoundPacketTo( + connection, + event.getPacket().toLocationalSoundPacket(position) + ); + } } } } |
