Upload via GUI (61 Dateien)

This commit is contained in:
2026-11-14 23:40:19 +00:00
parent e7fb9bf98a
commit d7e1745714
2 changed files with 1624 additions and 1399 deletions
@@ -0,0 +1,147 @@
package net.viper.status.modules.chat;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* Nachrichtenformat für Chat-Spiele (NexusLobby) zwischen Sub-Server und Proxy.
*
* Hintergrund: Der Chat läuft komplett über diesen Proxy ({@link ChatModule}). Die Sub-Server bekommen
* keine Chat-Ereignisse und können Antworten auf ein Chat-Spiel deshalb nicht selbst erkennen. Der
* Sub-Server meldet dem Proxy Start und Ende eines Spiels samt gültiger Antworten. Der Proxy erkennt die
* Antwort im Chat und meldet sie dem Sub-Server zurück.
*
* Richtung Proxy: Kanal "BungeeCord", Unterkanal {@link #SUBCHANNEL}
* START: Unterkanal, "START", Spiel-ID, Sekunden, Groß/Klein beachten, Antwort verstecken, Anzahl, Antworten...
* STOP: Unterkanal, "STOP", Spiel-ID
* Richtung Sub-Server: Kanal {@link #ANSWER_CHANNEL}
* ANSWER: "ANSWER", Spiel-ID, Spieler-UUID, Nachricht
*
* Das Format muss mit ChatGameProtocol in NexusLobby übereinstimmen.
*/
public final class ChatGameBridge {
public static final String SUBCHANNEL = "NexusChatGame";
public static final String ANSWER_CHANNEL = "statusapi:chatgame";
/** So viele Sekunden länger als angegeben bleibt ein Spiel im Proxy gültig (Sicherheitspuffer). */
private static final long GRACE_SECONDS = 5;
private static final int MAX_ANSWERS = 20;
private ChatGameBridge() {}
/** Ein laufendes Chat-Spiel auf einem Sub-Server. */
public static final class Game {
public final String id;
public final List<String> answers;
public final boolean caseSensitive;
public final boolean hideAnswer;
public final long expiresAt;
public Game(String id, List<String> answers, boolean caseSensitive, boolean hideAnswer, long expiresAt) {
this.id = id;
this.answers = Collections.unmodifiableList(new ArrayList<>(answers));
this.caseSensitive = caseSensitive;
this.hideAnswer = hideAnswer;
this.expiresAt = expiresAt;
}
public boolean expired(long now) {
return now > expiresAt;
}
/** Prüft, ob die Chat-Nachricht eine gültige Antwort ist. */
public boolean matches(String message) {
String text = clean(message);
if (text.isEmpty()) return false;
for (String answer : answers) {
if (caseSensitive ? answer.equals(text) : answer.equalsIgnoreCase(text)) return true;
}
return false;
}
}
/** Eine Nachricht des Sub-Servers an den Proxy. */
public static final class Message {
public final String action; // START oder STOP
public final String gameId;
public final Game game; // nur bei START, sonst null
Message(String action, String gameId, Game game) {
this.action = action;
this.gameId = gameId;
this.game = game;
}
}
/**
* Liest eine Plugin-Message des Sub-Servers.
*
* @param data Daten einer Nachricht auf dem Kanal "BungeeCord"
* @param now aktuelle Zeit in Millisekunden
* @return die Nachricht oder null, wenn sie nicht zum Chat-Spiel gehört oder fehlerhaft ist
*/
public static Message read(byte[] data, long now) {
if (data == null) return null;
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) {
if (!SUBCHANNEL.equals(in.readUTF())) return null;
String action = in.readUTF();
String id = in.readUTF();
if ("STOP".equals(action)) return new Message("STOP", id, null);
if (!"START".equals(action)) return null;
int seconds = Math.max(1, Math.min(600, in.readInt()));
boolean caseSensitive = in.readBoolean();
boolean hideAnswer = in.readBoolean();
int count = in.readInt();
if (count < 1 || count > MAX_ANSWERS) return null;
List<String> answers = new ArrayList<>();
for (int i = 0; i < count; i++) answers.add(clean(in.readUTF()));
long expiresAt = now + (seconds + GRACE_SECONDS) * 1000L;
return new Message("START", id, new Game(id, answers, caseSensitive, hideAnswer, expiresAt));
} catch (IOException | RuntimeException e) {
return null;
}
}
/** Baut die Antwort-Nachricht an den Sub-Server. */
public static byte[] answer(String gameId, UUID player, String message) {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bytes);
out.writeUTF("ANSWER");
out.writeUTF(gameId);
out.writeUTF(player.toString());
out.writeUTF(message);
return bytes.toByteArray();
} catch (IOException e) {
throw new IllegalStateException(e); // kann bei einem Byte-Array nicht passieren
}
}
/** Entfernt Farbcodes (Paragraphenzeichen), unsichtbare Zeichen und überflüssige Leerzeichen. */
public static String clean(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == 0xA7) { // Paragraphenzeichen: das Zeichen danach gehört zum Farbcode
i++;
continue;
}
if ((c >= 0x200B && c <= 0x200F) || c == 0x2060 || c == 0xFEFF) continue; // unsichtbar
sb.append(c == 0xA0 ? ' ' : c);
}
return sb.toString().trim();
}
}
@@ -5,8 +5,10 @@ import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.*; import net.md_5.bungee.api.chat.*;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.ChatEvent; import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent; import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Listener;
@@ -84,6 +86,9 @@ public class ChatModule implements Module, Listener {
// UUIDs die gerade auf Plugin-Chat-Eingabe warten // UUIDs die gerade auf Plugin-Chat-Eingabe warten
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Laufende Chat-Spiele (NexusLobby): Servername → Spiel mit den gültigen Antworten
private final Map<String, ChatGameBridge.Game> chatGames = new ConcurrentHashMap<>();
// Geyser-Pr\u00e4fix f\u00fcr Bedrock-Spieler (Standard: ".") // Geyser-Pr\u00e4fix f\u00fcr Bedrock-Spieler (Standard: ".")
private static final String GEYSER_PREFIX = "."; private static final String GEYSER_PREFIX = ".";
@@ -142,6 +147,7 @@ public class ChatModule implements Module, Listener {
// Listener & Befehle registrieren // Listener & Befehle registrieren
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
ProxyServer.getInstance().registerChannel(ChatGameBridge.ANSWER_CHANNEL);
registerCommands(); registerCommands();
logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kan\u00e4le geladen."); logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kan\u00e4le geladen.");
@@ -160,6 +166,8 @@ public class ChatModule implements Module, Listener {
helpopCooldowns.clear(); helpopCooldowns.clear();
reportCooldowns.clear(); reportCooldowns.clear();
lastChatMessages.clear(); lastChatMessages.clear();
chatGames.clear();
try { ProxyServer.getInstance().unregisterChannel(ChatGameBridge.ANSWER_CHANNEL); } catch (Exception ignored) {}
} }
// ========================================================= // =========================================================
@@ -191,10 +199,80 @@ public class ChatModule implements Module, Listener {
return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch return; // Event NICHT cancellen → Nachricht geht mit Originalsignatur durch
} }
// Antwort auf ein Chat-Spiel (NexusLobby auf dem Sub-Server)?
if (handleChatGameAnswer(player, e.getMessage())) {
e.setCancelled(true);
return;
}
e.setCancelled(true); e.setCancelled(true);
processChat(player, e.getMessage()); processChat(player, e.getMessage());
} }
// =========================================================
// CHAT-SPIELE (NexusLobby)
//
// Der Chat läuft komplett über diesen Proxy, die Sub-Server bekommen keine Chat-Ereignisse.
// Damit ein Chat-Spiel auf einem Sub-Server die Antwort erkennt, meldet der Sub-Server Start und
// Ende eines Spiels samt gültiger Antworten (BungeeCord-Kanal, Unterkanal "NexusChatGame").
// Passt eine Chat-Nachricht zur Antwort, melden wir sie über den Kanal "statusapi:chatgame"
// an den Sub-Server und zeigen sie (je nach Einstellung des Spiels) nicht im Chat an.
// Format und Prüfung: siehe ChatGameBridge.
// =========================================================
/** Nachrichten der Sub-Server: Start und Ende eines Chat-Spiels. */
@EventHandler
public void onChatGameMessage(PluginMessageEvent e) {
if (!"BungeeCord".equals(e.getTag()) && !"bungeecord:main".equals(e.getTag())) return;
// Nur Nachrichten von Servern, niemals von Spielern
if (!(e.getSender() instanceof Server)) return;
ChatGameBridge.Message msg = ChatGameBridge.read(e.getData(), System.currentTimeMillis());
if (msg == null) return;
e.setCancelled(true);
String serverName = ((Server) e.getSender()).getInfo().getName();
if ("START".equals(msg.action)) {
chatGames.put(serverName, msg.game);
} else if ("STOP".equals(msg.action)) {
ChatGameBridge.Game running = chatGames.get(serverName);
if (running != null && running.id.equals(msg.gameId)) chatGames.remove(serverName, running);
}
}
/**
* Prüft, ob die Nachricht die Antwort auf das laufende Chat-Spiel des Servers ist, auf dem der
* Spieler steht. Dann wird sie dem Sub-Server gemeldet.
*
* @return true, wenn die Nachricht nicht im Chat angezeigt werden soll
*/
private boolean handleChatGameAnswer(ProxiedPlayer player, String message) {
if (chatGames.isEmpty() || message == null) return false;
Server server = player.getServer();
if (server == null) return false;
String serverName = server.getInfo().getName();
ChatGameBridge.Game game = chatGames.get(serverName);
if (game == null) return false;
if (game.expired(System.currentTimeMillis())) {
chatGames.remove(serverName, game);
return false;
}
// Gemutete Spieler dürfen nicht antworten (Admins mit Bypass werden nie gemutet)
boolean isAdmin = player.hasPermission(config.getAdminBypassPermission());
if (!isAdmin && muteManager.isMuted(player.getUniqueId())) return false;
if (!game.matches(message)) return false;
// Nur der erste richtige Treffer zählt: Danach ist das Spiel hier beendet
if (!chatGames.remove(serverName, game)) return false;
server.sendData(ChatGameBridge.ANSWER_CHANNEL, ChatGameBridge.answer(game.id, player.getUniqueId(), message));
return game.hideAnswer;
}
/** /**
* Zentrale Chat-Verarbeitungslogik. * Zentrale Chat-Verarbeitungslogik.
*/ */