package eu.mhsl.craftattack.loginServer; import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import net.minestom.server.dialog.*; import net.minestom.server.entity.Player; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.util.ArrayList; import java.util.List; import java.util.Locale; public final class Dialog { private static final String namespace = "profile"; private static final Key nextKey = Key.key(namespace, "next"); private static final Key backKey = Key.key(namespace, "back"); private static final Key finishKey = Key.key(namespace, "finish"); private static final Key cancelKey = Key.key(namespace, "cancel"); private static final Key disconnectKey = Key.key(namespace, "disconnect"); private static final String privacyUrl = "https://mhsl.eu/id.html"; private static final String rulesUrl = "https://mhsl.eu/craftattack/rules"; private static final int rulesWidth = 300; private static final int buttonWidth = 150; private static final int cancelWidth = 2 * buttonWidth + 4; private static final String rulesPreamble = "Alle Teilnehmer müssen die Regeln lesen. Sie sorgen für einen reibungslosen, geordneten Ablauf von CraftAttack 9 und sind wörtlich zu nehmen. Sie gehen bewusst nicht tief ins Detail und umreißen manche Themen nur grob. Auf Wunsch der Spieler entscheiden die Administratoren und orientieren sich dabei an den Regeln. Für Vollständigkeit, Richtigkeit und Aktualität aller Angaben wird keine Gewähr übernommen. Ob und wie Regeln durchgesetzt werden, entscheiden die Administratoren, nachdem sie sich mit dem Geschädigten abgesprochen und eine zur Situation passende Maßnahme gewählt haben."; private static final List rules = List.of( "1. Das Wichtigste ist ein respektvoller und toleranter Umgang miteinander. Spielspaß, Offenheit und Interaktion aller stehen im Vordergrund. Drohungen, Belästigungen und jedes andere respektlose Verhalten gegenüber Mitspielern sind streng verboten und werden hart bestraft.", "2. Sexistische, diskriminierende, rassistische, pornographische oder illegale Inhalte sind überall untersagt, ob in Minecraft-Namen, Skins, Chat-Nachrichten oder Links. Den Chat mit Nachrichten jeder Art zuzuspammen ist ebenfalls verboten. Der bei der Anmeldung angegebene MC-Name soll bis zum Ende des Projekts nicht geändert werden. Zweitaccounts dürfen weder genutzt noch angemeldet werden.", "3. Clientmodifikationen, die deutliche Vorteile gegenüber anderen Spielern bringen, sind nicht erlaubt. Wer nicht mit Vanilla-Minecraft spielt, muss seinen Client und seine Mods selbst darauf prüfen, ob sie entscheidende Vorteile verschaffen, und solche Mods entfernen. Einfache Minimaps (Draufsicht) und Cosmetics sind selbstverständlich erlaubt. Die Administratoren wollen, dass niemand übertreibt oder sich unfaire Vorteile verschafft, und setzen dabei auf die Selbstreflexion jedes Einzelnen. Im Zweifel sollst du dich an einen Administrator wenden, der dir genauere Auskunft geben kann.", "4. Maschinen, Farmen und andere Bauten, die Lag erzeugen oder den Spielfluss stören könnten, dürfen weder gebaut noch betrieben werden. Im Zweifel ist eine Anfrage bei den Administratoren erwünscht. Farmen und Redstone-Schaltungen sollen nur aktiviert werden, wenn es nötig ist. Übergroße Villager-Baukomplexe dürfen nur in Absprache mit den Administratoren errichtet und betrieben werden. Farmen zu bauen gehört selbstverständlich zum Projekt und ist kein Problem, solange diese Bedingungen eingehalten werden.", "5. Jeder Spieler darf überall Items verkaufen. Es ist aber sinnvoll und erwünscht, alle Shops gemeinsam im Shoppingdistrict anzusiedeln, um die Interaktion zu fördern. Ein Shop muss im ausgewiesenen Bereich stehen und über das dortige Wegenetz erreichbar sein. Der Shoppingdistrict ist ausschließlich für Shops gedacht. Mehrere Shops für dasselbe Item sind möglich und erwünscht. Diebstahl im Shoppingdistrict wird genauso bestraft wie Diebstahl im Allgemeinen. Private Bauten müssen einen angemessenen Abstand zum Shoppingdistrict einhalten.", "6. Bestimmte Gebiete abzustecken ist grundsätzlich erlaubt, unangemessen große Grundstücke sind aber untersagt. Die maximale Größe wird im Einzelfall entschieden. Die Grenzen bereits abgesteckter Grundstücke dürfen nicht verändert werden. Bei Inaktivität, anderen Beschwerden oder Verstößen ist ein Administrator zu kontaktieren.", "7. Ziel ist ein Umgang, bei dem für niemanden etwas Negatives entsteht. Andere zu bestehlen oder ohne Erlaubnis ihre Bauwerke zu verändern ist nicht gestattet. Es gibt dabei eine gewisse Toleranz und einen Spielraum, der aber nicht überschritten werden darf. Das gilt sowohl für Diebstahl als auch für Griefing, also das Zerstören fremder Bauten ohne Erlaubnis. Außerdem ist es verboten, andere ohne nachvollziehbaren Grund zu töten. Jede Situation muss einzeln bewertet werden, deshalb gibt es hier bewusst keine pauschale Regel. Klar ist aber, dass zwischen einem Töten aus Spaß mit geringen Folgen und wiederholtem oder dauerhaftem Töten mit schlimmeren Folgen unterschieden wird.", "8. Die Administratoren bewerten einzelne Situationen, verhängen Strafen und treffen Entscheidungen. Ein internes Strikesystem sorgt dafür, dass alle Spieler gleich behandelt werden. Die Administratoren sprechen sich außerdem untereinander ab, damit alle Sichtweisen einfließen. Alle Entscheidungen müssen im Sinne des Projekts getroffen werden. Entscheidungen und Anweisungen der Administratoren ist stets Folge zu leisten, wenn sie als Administrator handeln. Im normalen Spielbetrieb sind sie ganz normale Mitspieler ohne spielentscheidende Sonderrechte. Es ist nicht ihre Aufgabe, überall nach dem Rechten zu sehen. Sie sind Ansprechpartner und übernehmen die Administratorenrolle erst, wenn ihnen ein Geschädigter ein Problem vorlegt, und handeln dann entsprechend. Grundsätzlich greifen die Administratoren nur ein, wenn die Spieler es fordern. Solange beide Parteien zufrieden sind, passiert nichts. Wenn sich zum Beispiel zwei Spieler bewusst ein PvP-Duell liefern, hat das logischerweise keine Konsequenzen.", "9. Jeder Teilnehmer kann sich an den Support bzw. das Administratoren-Team wenden. Administratoren sind die Spieler mit Admin-Tag auf dem Server. Zwei von ihnen sind außerdem Administratoren der WhatsApp-Gruppe. Man erreicht sie direkt im Server-Chat oder auf dem TeamSpeak „mhsl.eu“. Zusätzlich kann man sie per WhatsApp anschreiben, etwa wenn gerade kein Administrator auf dem Server ist oder bei anderen Rückfragen. Bei Unzufriedenheit, Regelverstößen, Anregungen oder Fragen steht das Administratoren-Team allen Spielern jederzeit zur Verfügung.", "10. Konflikte sollen grundsätzlich zuerst zwischen den Spielern geklärt werden, bevor ein Administrator kontaktiert wird. Jeder Regelverstoß hat unterschiedliche Folgen, von Ermahnungen über Tagesbanns bis zum permanenten Bann. Diese möglichen Konsequenzen müssen alle Teilnehmer akzeptieren." ); private Dialog() {} public static boolean isDialogKey(Key key) { return key.namespace().equals(namespace); } public static boolean isCancelKey(Key key) { return key.equals(cancelKey); } public static boolean isDisconnectKey(Key key) { return key.equals(disconnectKey); } public static void open(Player player, DialogState state) { if (state.isDone()) showDone(player, state.doneMessage()); else if (state.submitting) showSubmitting(player); else showPage(player, state, state.pendingError); } public static void showError(Player player, DialogState state, String message) { state.pendingError = error(message); showPage(player, state, state.pendingError); } private static void showSubmitting(Player player) { player.showDialog(new net.minestom.server.dialog.Dialog.Notice( new DialogMetadata( Component.text("Anmeldung wird gesendet …"), null, false, false, DialogAfterAction.NONE, List.of(plain(Component.text("Einen Moment bitte, deine Anmeldung wird übermittelt."), rulesWidth)), List.of() ), new DialogActionButton(Component.text("Bitte warten …"), null, cancelWidth, null) )); } private static void showDone(Player player, String message) { player.showDialog(new net.minestom.server.dialog.Dialog.Notice( new DialogMetadata( Component.text("Anmeldung abgeschlossen"), null, false, false, DialogAfterAction.NONE, List.of(plain(Component.text(message), rulesWidth)), List.of() ), button("Verbindung trennen", disconnectKey, cancelWidth) )); } public static boolean handleClick(Player player, DialogState state, Key key, CompoundBinaryTag payload) { state.pendingError = null; if (key.equals(backKey)) { state.previousPage(); showPage(player, state, null); return false; } saveCurrentPage(state, payload); Component error = validateCurrentPage(player, state); if (error != null) { showPage(player, state, error); return false; } if (key.equals(nextKey)) { state.nextPage(); showPage(player, state, null); return false; } return key.equals(finishKey); } private static void showPage(Player player, DialogState state, Component error) { List body = new ArrayList<>(); List inputs = new ArrayList<>(); String title; switch (state.currentPage()) { case data -> { title = "Profildaten"; body.add(plain("Bitte fülle deine Profildaten aus.")); inputs.add(text("name", "Vorname (*)", state.firstName, 50)); inputs.add(text("secondName", "Nachname (*)", state.secondName, 50)); inputs.add(text("birthday", "Geburtstag (TT.MM.JJJJ) (*)", state.birthday, 10)); inputs.add(text("phoneNumber", "Telefonnummer (optional)", state.phoneNumber, 30)); } case consent -> { title = "Serverregeln"; body.add(plain(Component.text("Serverregeln", NamedTextColor.GOLD, TextDecoration.BOLD), rulesWidth)); body.add(plain(Component.text(rulesPreamble, NamedTextColor.GRAY), rulesWidth)); for (String rule : rules) { body.add(plain(Component.text(rule), rulesWidth)); } body.add(plain(Component.text() .append(link("Datenschutzerklärung", privacyUrl)) .append(Component.text(" | ")) .append(link("Vollständige Regeln", rulesUrl)) .build(), rulesWidth)); } case privacy -> { title = "Einverständnis (1/3)"; body.add(plain(Component.text( "Ich bin mit der Speicherung meiner in der Anmeldung angegebenen, persönlichen Daten einverstanden.", NamedTextColor.GRAY), rulesWidth)); inputs.add(checkbox("privacy", "Datenspeicherung zustimmen (*)", state.privacyAccepted)); } case logs -> { title = "Einverständnis (2/3)"; body.add(plain(Component.text( "Ich bin mit der Speicherung in Form von Logs aller meiner, beim Spielen anfallenden, " + "persönlichen Daten durch den Server einverstanden. " + "Die Log-Speicherung betrifft jede Interaktion im Spiel und zugehörige Daten, " + "z.B. Chatnachrichten, welche vom Minecraft-Client an den Server übermittelt werden.", NamedTextColor.GRAY), rulesWidth)); inputs.add(checkbox("logs", "Log-Speicherung zustimmen (*)", state.logsAccepted)); } case rules -> { title = "Einverständnis (3/3)"; body.add(plain(Component.text( "Ich bin mit den Regeln einverstanden und achte sie.", NamedTextColor.GRAY), rulesWidth)); inputs.add(checkbox("rules", "Regeln zustimmen (*)", state.rulesAccepted)); } default -> throw new IllegalStateException(); } if (error != null) body.add(plain(error)); String fullTitle = "Schritt " + state.pageNumber() + "/" + state.pageCount() + " – " + title; List buttons = new ArrayList<>(); if (!state.isFirstPage()) buttons.add(button("← Zurück", backKey)); if (state.isLastPage()) buttons.add(button("Anmeldung abschließen", finishKey)); else buttons.add(button("Weiter →", nextKey, state.isFirstPage() ? cancelWidth : buttonWidth)); buttons.add(button("Anmeldung abbrechen", cancelKey, cancelWidth)); player.showDialog(new net.minestom.server.dialog.Dialog.MultiAction( new DialogMetadata( Component.text(fullTitle), null, false, false, state.isLastPage() ? DialogAfterAction.CLOSE : DialogAfterAction.NONE, body, inputs ), buttons, null, state.isFirstPage() ? 1 : 2 )); } private static void saveCurrentPage(DialogState state, CompoundBinaryTag payload) { switch (state.currentPage()) { case data -> { state.firstName = payload.getString("name", "").trim(); state.secondName = payload.getString("secondName", "").trim(); state.birthday = payload.getString("birthday", "").trim(); state.phoneNumber = payload.getString("phoneNumber", "").trim(); } case consent -> {} case privacy -> state.privacyAccepted = payload.getBoolean("privacy", false); case logs -> state.logsAccepted = payload.getBoolean("logs", false); case rules -> state.rulesAccepted = payload.getBoolean("rules", false); } } private static Component validateCurrentPage(Player player, DialogState state) { switch (state.currentPage()) { case data -> { if (state.firstName.isBlank() || state.secondName.isBlank() || state.birthday.isBlank()) return error("Bitte fülle alle Pflichtfelder aus."); if (!isValidBirthday(state.birthday)) return error("Bitte gib ein richtiges Geburtsdatum an."); } case consent -> {} case privacy -> { if (!state.privacyAccepted) return error("Du musst zustimmen, um fortzufahren."); } case logs -> { if (!state.logsAccepted) return error("Du musst zustimmen, um fortzufahren."); } case rules -> { if (!state.rulesAccepted) return error("Du musst zustimmen, um fortzufahren."); } } return null; } public static boolean isValidBirthday(String birthday) { DateTimeFormatter formatter = DateTimeFormatter .ofPattern("dd.MM.uuuu", Locale.GERMANY) .withResolverStyle(ResolverStyle.STRICT); try { LocalDate date = LocalDate.parse(birthday, formatter); return !date.isAfter(LocalDate.now()); } catch (DateTimeParseException e) { return false; } } private static DialogActionButton button(String label, Key key) { return button(label, key, buttonWidth); } private static DialogActionButton button(String label, Key key, int width) { return new DialogActionButton( Component.text(label), null, width, new DialogAction.DynamicCustom(key, CompoundBinaryTag.builder().build()) ); } private static Component link(String text, String url) { return Component.text(text, NamedTextColor.AQUA, TextDecoration.UNDERLINED) .clickEvent(ClickEvent.openUrl(url)); } private static DialogInput text(String key, String label, String initial, int maxLength) { return new DialogInput.Text(key, 300, Component.text(label), true, initial, maxLength, null); } private static DialogInput checkbox(String key, String label, boolean initial) { return new DialogInput.Boolean(key, Component.text(label), initial, "true", "false"); } private static DialogBody plain(String text) { return plain(Component.text(text)); } private static DialogBody plain(Component component) { return plain(component, 300); } private static DialogBody plain(Component component, int width) { return new DialogBody.PlainMessage(component, width); } private static Component error(String text) { return Component.text(text, NamedTextColor.RED); } }