Upload via GUI (61 Dateien)
This commit is contained in:
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user