This commit is contained in:
RedSavant
2026-03-23 19:46:06 +01:00
parent 88547c6d5e
commit 41633b1bd5
9 changed files with 347 additions and 2 deletions

View File

@@ -1,12 +1,46 @@
package fr.redsavant;
import fr.redsavant.commands.MuteCommand;
import fr.redsavant.proximity.*;
import fr.redsavant.ws.WebSocketClient;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class LagoonPlugin extends BootstrapPlugin {
private WebSocketClient wsClient;
private ProximityTask proximityTask;
@Override
protected void onStart() {
saveDefaultConfig();
String roomCode = getConfig().getString("room-code", "CHANGE_ME");
URI wsUri = URI.create("wss://lagoon.under-scape.com/ws/rooms/" + roomCode + "/plugins/proximity");
PlayerCodeManager codeManager = new PlayerCodeManager();
Set<UUID> mutedPlayers = new HashSet<>();
wsClient = new WebSocketClient(wsUri, msg -> {
// future reponse de drewen
});
wsClient.connect();
getServer().getPluginManager().registerEvents(new ProximityListener(codeManager), this);
getCommand("proximity").setExecutor(new MuteCommand(mutedPlayers));
proximityTask = new ProximityTask(wsClient, codeManager, mutedPlayers);
proximityTask.runTaskTimerAsynchronously(this, 20L, 60L);
getLogger().info("LagoonPlugin enabled — room: " + roomCode);
}
@Override
protected void onStop() {
if (proximityTask != null) proximityTask.cancel();
if (wsClient != null) wsClient.close();
}
}
}

View File

@@ -0,0 +1,43 @@
package fr.redsavant.commands;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Set;
import java.util.UUID;
public class MuteCommand implements CommandExecutor {
private final Set<UUID> mutedPlayers;
public MuteCommand(Set<UUID> mutedPlayers) {
this.mutedPlayers = mutedPlayers;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length < 1) {
sender.sendMessage("§cUsage : /proximity mute <player>");
return true;
}
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
sender.sendMessage("§cUnknow player");
return true;
}
UUID uuid = target.getUniqueId();
if (mutedPlayers.contains(uuid)) {
mutedPlayers.remove(uuid);
sender.sendMessage("§a" + target.getName() + " has been unmuted on Lagoon");
} else {
mutedPlayers.add(uuid);
sender.sendMessage("§e" + target.getName() + " has been muted on Lagoon");
}
return true;
}
}

View File

@@ -0,0 +1,37 @@
package fr.redsavant.proximity;
import org.bukkit.entity.Player;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class PlayerCodeManager {
private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ambiguous chars removed
private static final int CODE_LENGTH = 6;
private static final SecureRandom RANDOM = new SecureRandom();
private final Map<UUID, String> codes = new HashMap<>();
public String getOrCreate(Player player) {
return codes.computeIfAbsent(player.getUniqueId(), uuid -> generateCode());
}
public String get(UUID uuid) {
return codes.get(uuid);
}
public void remove(UUID uuid) {
codes.remove(uuid);
}
private String generateCode() {
StringBuilder sb = new StringBuilder(CODE_LENGTH);
for (int i = 0; i < CODE_LENGTH; i++) {
sb.append(CHARS.charAt(RANDOM.nextInt(CHARS.length())));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,38 @@
package fr.redsavant.proximity;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class ProximityListener implements Listener {
private final PlayerCodeManager codeManager;
public ProximityListener(PlayerCodeManager codeManager) {
this.codeManager = codeManager;
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
String code = codeManager.getOrCreate(event.getPlayer());
sendCodeMessage(event.getPlayer(), code);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
codeManager.remove(event.getPlayer().getUniqueId());
}
private void sendCodeMessage(org.bukkit.entity.Player player, String code) {
TextComponent msg = new TextComponent("§a[Lagoon] §fTon code de liaison : §b§l" + code);
msg.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, code));
msg.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new ComponentBuilder("§7Clique pour copier").create()));
player.spigot().sendMessage(msg);
}
}

View File

@@ -0,0 +1,56 @@
package fr.redsavant.proximity;
import fr.redsavant.ws.ProximityPayload;
import fr.redsavant.ws.WebSocketClient;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class ProximityTask extends BukkitRunnable {
private final WebSocketClient wsClient;
private final PlayerCodeManager codeManager;
private final Set<UUID> mutedPlayers;
public ProximityTask(WebSocketClient wsClient, PlayerCodeManager codeManager, Set<UUID> mutedPlayers) {
this.wsClient = wsClient;
this.codeManager = codeManager;
this.mutedPlayers = mutedPlayers;
}
@Override
public void run() {
if (!wsClient.isConnected()) return;
List<ProximityPayload.PlayerEntry> entries = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
String code = codeManager.get(player.getUniqueId());
if (code == null) continue;
int layer = switch (player.getWorld().getEnvironment()) {
case NETHER -> 1;
case THE_END -> 2;
default -> 0;
};
// LE WORLD ZEBI (spigot c'est nul X< )
boolean muted = mutedPlayers.contains(player.getUniqueId());
entries.add(new ProximityPayload.PlayerEntry(
player.getLocation().getX(),
player.getLocation().getY(),
player.getLocation().getZ(),
layer,
code,
muted
));
}
wsClient.send(ProximityPayload.buildPositionsJson(entries));
}
}

View File

@@ -0,0 +1 @@
room-code: "CHANGE_ME"

View File

@@ -6,4 +6,9 @@ authors:
- RedSavant
- UnderScape (lagoon)
api-version: '1.21'
main: fr.redsavant.LagoonPlugin
main: fr.redsavant.LagoonPlugin
commands:
proximity:
description: Base command
usage: /proximity mute <player>
permission: lagoon.admin

View File

@@ -0,0 +1,37 @@
package fr.redsavant.ws;
import java.util.List;
public class ProximityPayload {
public record PlayerEntry(double x, double y, double z, int layer, String code, boolean muted) {}
public static String buildPositionsJson(List<PlayerEntry> entries) {
StringBuilder sb = new StringBuilder();
sb.append("{\"type\":\"positions\",\"data\":[");
for (int i = 0; i < entries.size(); i++) {
PlayerEntry e = entries.get(i);
sb.append("{")
.append("\"x\":").append(e.x()).append(",")
.append("\"y\":").append(e.y()).append(",")
.append("\"z\":").append(e.z()).append(",")
.append("\"layer\":").append(e.layer()).append(",")
.append("\"code\":\"").append(e.code()).append("\"")
.append(",\"muted\":").append(e.muted())
.append("}");
if (i < entries.size() - 1) sb.append(",");
}
sb.append("]}");
return sb.toString();
}
public static int dimensionToLayer(String dimensionKey) {
return switch (dimensionKey) {
case "minecraft:the_nether" -> 1;
case "minecraft:the_end" -> 2;
default -> 0; // overworld
};
}
}

View File

@@ -0,0 +1,94 @@
package fr.redsavant.ws;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Logger;
public class WebSocketClient implements WebSocket.Listener {
private static final Logger LOGGER = Logger.getLogger(WebSocketClient.class.getName());
private final URI uri;
private final Consumer<String> onMessage;
private WebSocket socket;
private boolean connected = false;
private final ScheduledExecutorService reconnectScheduler = Executors.newSingleThreadScheduledExecutor();
public WebSocketClient(URI uri, Consumer<String> onMessage) {
this.uri = uri;
this.onMessage = onMessage;
}
public void connect() {
HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(uri, this)
.thenAccept(ws -> {
this.socket = ws;
this.connected = true;
LOGGER.info("WebSocket connected : " + uri);
})
.exceptionally(ex -> {
LOGGER.warning("Failed to connect to WebSocket, retry in 5s : " + ex.getMessage());
scheduleReconnect();
return null;
});
}
public void send(String message) {
if (socket != null && connected) {
socket.sendText(message, true);
}
}
public void close() {
reconnectScheduler.shutdownNow();
if (socket != null) {
socket.sendClose(WebSocket.NORMAL_CLOSURE, "shutdown");
}
}
public boolean isConnected() {
return connected;
}
private void scheduleReconnect() {
reconnectScheduler.schedule(this::connect, 5, TimeUnit.SECONDS);
}
@Override
public void onOpen(WebSocket webSocket) {
connected = true;
LOGGER.info("WebSocket opened");
webSocket.request(1);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
onMessage.accept(data.toString());
webSocket.request(1);
return null;
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
connected = false;
LOGGER.warning("WebSocket closed (" + statusCode + "), re-connect in 5s...");
scheduleReconnect();
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
connected = false;
LOGGER.warning("WebSocket error : " + error.getMessage());
scheduleReconnect();
}
}