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();
}
}
File diff suppressed because it is too large Load Diff