Compare commits

..
13 Commits
Author SHA1 Message Date
jannis 7c0de3d0f7 fixed bedrock bugs 2026-11-06 17:31:09 +01:00
jannis acb7b17eb0 bedrock fix 2026-11-06 16:30:30 +01:00
jannis 8d3c750ea1 fixed bedrock bugs 2026-11-05 21:52:38 +01:00
jannis bbd41432c3 fixed bugs 2026-11-04 23:14:28 +01:00
jannis edd67a4fc5 bedrock uuid check 2026-11-04 14:12:08 +01:00
jannis a4063c3300 fixed dialog closing issue and changed dialog 2026-10-30 18:53:21 +01:00
jannis ba0cc8e458 fixed setting chat-via-velocity 2026-10-30 15:28:47 +01:00
jannis 15d1c70b8c update 2026-10-29 21:16:51 +01:00
jannis 3f672d1819 changed Craftattack 8 to Craftattack 9 2026-10-28 22:36:04 +01:00
jannisandClaude Opus 4.7 22c2cb3ab0 tweak consent dialog layout and clean up small issues
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-25 15:56:07 +01:00
jannis 78a01d9654 fix dialog flow and cleanup listeners 2026-10-24 23:00:51 +02:00
jannis d70b3b826e removed package minenet 2026-10-24 22:43:09 +02:00
jannis 4e6255483b added more sided dialog, added api and added PlayerHandler 2026-10-24 22:38:52 +02:00
17 changed files with 813 additions and 225 deletions

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ClaudeCodeEditorTabs">
<option name="activeSessionId" value="fe25343b-25e1-4de3-ac2a-8ba16e4c38b9" />
<option name="openSessionIds">
<list>
<option value="fe25343b-25e1-4de3-ac2a-8ba16e4c38b9" />
</list>
</option>
<option name="sessionPaths">
<map>
<entry key="fe25343b-25e1-4de3-ac2a-8ba16e4c38b9" value="/sessions/90f5ec6e-0a41-4a6f-9288-12703fc962dd" />
</map>
</option>
<option name="sessionTitles">
<map>
<entry key="fe25343b-25e1-4de3-ac2a-8ba16e4c38b9" value="/simplfy" />
</map>
</option>
</component>
</project>
+3 -2
View File
@@ -4,7 +4,7 @@ plugins {
id("com.gradleup.shadow") version "9.2.2"
}
group = "eu.mhsl.minenet"
group = "eu.mhsl.craftattack"
version = "1.0-SNAPSHOT"
repositories {
@@ -22,12 +22,13 @@ dependencies {
implementation("org.slf4j:slf4j-simple:2.0.17")
implementation("org.yaml:snakeyaml:2.4")
implementation("org.spongepowered:configurate-yaml:4.2.0")
implementation("com.google.code.gson:gson:2.11.0")
}
tasks.shadowJar {
archiveClassifier.set("")
manifest {
attributes["Main-Class"] =
"eu.mhsl.minenet.loginServer.Main"
"eu.mhsl.craftattack.loginServer.Main"
}
}
@@ -0,0 +1,110 @@
package eu.mhsl.craftattack.loginServer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.UUID;
public final class Api {
private static final HttpClient client = HttpClient.newHttpClient();
private static final boolean offline = false;
private Api() {}
public static boolean isRegistered(UUID uuid) {
if (offline) return false;
HttpResponse<String> response = call("GET", "/api/users/" + uuid, null, true);
int status = response.statusCode();
if (status == 200) return true;
if (status == 404) return false;
throw new IllegalStateException("API-Fehler " + status + ": " + response.body());
}
public static final class SignupError extends RuntimeException {
public final String code;
public SignupError(String code, String message) {
super(message);
this.code = code;
}
}
public static void signup(
String firstname,
String lastname,
String birthdayIso,
String phone,
String username,
UUID uuid,
String edition
) {
if (offline) {
System.out.println("[Api offline] signup: " + firstname + " " + lastname + ", " + birthdayIso
+ ", " + phone + ", " + username + ", " + uuid + ", " + edition);
return;
}
JsonObject body = new JsonObject();
body.addProperty("firstname", firstname);
body.addProperty("lastname", lastname);
body.addProperty("birthday", birthdayIso);
body.addProperty("phone", phone);
body.addProperty("username", username);
body.addProperty("uuid", uuid.toString());
body.addProperty("edition", edition);
HttpResponse<String> response = call("POST", "/api/users", body.toString(), true);
int status = response.statusCode();
if (status >= 200 && status < 300) return;
String code = switch (status) {
case 400 -> "BAD_REQUEST";
case 401 -> "UNAUTHORIZED";
case 409 -> "CONFLICT";
default -> "UNKNOWN";
};
String message = response.body();
try {
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
JsonElement error = json.get("error");
if (error != null && error.isJsonPrimitive()) message = error.getAsString();
} catch (RuntimeException ignored) {}
throw new SignupError(code, "Request: " + body + "\nResponse: " + response.body());
}
private static HttpResponse<String> call(String method, String path, String body, boolean withAuth) {
try {
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(config("url") + path))
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.method(method, body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
if (withAuth) {
String secret = config("secret");
if (!secret.isBlank()) {
String credentials = Base64.getEncoder()
.encodeToString(("api:" + secret).getBytes(StandardCharsets.UTF_8));
builder.header("Authorization", "Basic " + credentials);
}
}
return client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new IllegalStateException("API nicht erreichbar", e);
}
}
private static String config(String key) {
return Main.globalConfig.node("api", key).getString("");
}
}
@@ -0,0 +1,305 @@
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<String> 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.CLOSE,
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;
saveCurrentPage(state, payload);
if (key.equals(backKey)) {
state.previousPage();
showPage(player, state, null);
return false;
}
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<DialogBody> body = new ArrayList<>();
List<DialogInput> 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));
}
boolean bedrock = PlayerHandler.isBedrockUuid(player.getUuid());
body.add(plain(Component.text()
.append(bedrock ? Component.text(privacyUrl) : link("Datenschutzerklärung", privacyUrl))
.append(Component.text(" | "))
.append(bedrock ? Component.text(rulesUrl) : 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;
boolean bedrock = PlayerHandler.isBedrockUuid(player.getUuid());
List<DialogActionButton> buttons = new ArrayList<>();
if (state.isLastPage()) buttons.add(button("Anmeldung abschließen", finishKey));
else buttons.add(button("Weiter →", nextKey, state.isFirstPage() ? cancelWidth : buttonWidth));
if (!state.isFirstPage()) buttons.add(bedrock ? 1 : 0, button("← Zurück", backKey));
if(state.isFirstPage()) buttons.add(button("Anmeldung abbrechen", cancelKey, cancelWidth));
player.showDialog(new net.minestom.server.dialog.Dialog.MultiAction(
new DialogMetadata(
Component.text(fullTitle),
null,
false,
false,
bedrock || 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);
}
}
@@ -0,0 +1,76 @@
package eu.mhsl.craftattack.loginServer;
import net.kyori.adventure.text.Component;
import java.util.List;
public final class DialogState {
public enum Page { data, consent, privacy, logs, rules }
private static final List<Page> pages = List.of(Page.data, Page.consent, Page.privacy, Page.logs, Page.rules);
private int pageIndex = 0;
String firstName = "";
String secondName = "";
String birthday = "";
String phoneNumber = "";
boolean privacyAccepted = false;
boolean logsAccepted = false;
boolean rulesAccepted = false;
private String doneMessage = null;
volatile boolean submitting = false;
Component pendingError = null;
private DialogState() {}
public static DialogState create() {
return new DialogState();
}
public static DialogState done(String message) {
DialogState state = new DialogState();
state.doneMessage = message;
return state;
}
public boolean isDone() {
return doneMessage != null;
}
public String doneMessage() {
return doneMessage;
}
public Page currentPage() {
return pages.get(pageIndex);
}
public int pageNumber() {
return pageIndex + 1;
}
public int pageCount() {
return pages.size();
}
public boolean isFirstPage() {
return pageIndex == 0;
}
public boolean isLastPage() {
return pageIndex == pages.size() - 1;
}
public void nextPage() {
pageIndex = Math.min(pages.size() - 1, pageIndex + 1);
}
public void previousPage() {
pageIndex = Math.max(0, pageIndex - 1);
}
public void goToPage(Page page) {
pageIndex = pages.indexOf(page);
}
}
@@ -1,6 +1,6 @@
package eu.mhsl.minenet.loginServer;
package eu.mhsl.craftattack.loginServer;
import eu.mhsl.minenet.loginServer.listeners.Listener;
import eu.mhsl.craftattack.loginServer.listeners.Listener;
import net.minestom.server.Auth;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extras.lan.OpenToLAN;
@@ -0,0 +1,205 @@
package eu.mhsl.craftattack.loginServer;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.event.player.PlayerCustomClickEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.timer.Task;
import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class PlayerHandler {
private static final Map<UUID, DialogState> states = new ConcurrentHashMap<>();
private static final Map<UUID, float[]> lastLook = new ConcurrentHashMap<>();
private static final Map<UUID, Task> cleanupTasks = new ConcurrentHashMap<>();
private static final Duration stateTimeout = Duration.ofMinutes(30);
private PlayerHandler() {}
public static void join(AsyncPlayerConfigurationEvent event, InstanceManager instanceManager) {
Player player = event.getPlayer();
Instance instance = instanceManager.createInstanceContainer();
instance.defaultClock().time(18000);
instance.defaultClock().pause();
event.setSpawningInstance(instance);
player.setRespawnPoint(new Pos(0, 100, 0));
Task cleanup = cleanupTasks.remove(player.getUuid());
if (cleanup != null) cleanup.cancel();
DialogState state;
if (Api.isRegistered(player.getUuid())) {
state = DialogState.done("Du bist bereits auf Craftattack registriert, wir freuen uns auf dich!");
states.put(player.getUuid(), state);
} else {
state = states.compute(player.getUuid(), (id, existing) ->
existing == null || existing.isDone() ? DialogState.create() : existing);
}
MinecraftServer.getSchedulerManager()
.buildTask(() -> showDialog(player, state))
.delay(Duration.ofMillis(500))
.schedule();
}
public static void leave(Player player) {
UUID uuid = player.getUuid();
lastLook.remove(uuid);
DialogState state = states.get(uuid);
if (state == null) return;
Task task = MinecraftServer.getSchedulerManager()
.buildTask(() -> {
states.remove(uuid, state);
cleanupTasks.remove(uuid);
})
.delay(stateTimeout)
.schedule();
Task previous = cleanupTasks.put(uuid, task);
if (previous != null) previous.cancel();
}
public static void onMove(Player player, float newYaw, float newPitch) {
if (isBedrockUuid(player.getUuid())) return;
DialogState state = states.get(player.getUuid());
if (state == null) return;
float[] last = lastLook.get(player.getUuid());
if (last != null && last[0] == newYaw && last[1] == newPitch) return;
lastLook.put(player.getUuid(), new float[]{newYaw, newPitch});
if (last == null) return;
showDialog(player, state);
}
private static void showDialog(Player player, DialogState state) {
lastLook.put(player.getUuid(), new float[]{player.getPosition().yaw(), player.getPosition().pitch()});
Dialog.open(player, state);
}
public static void handleDialogClick(PlayerCustomClickEvent event) {
if (!Dialog.isDialogKey(event.getKey())) return;
Player player = event.getPlayer();
DialogState state = states.get(player.getUuid());
if (state == null) return;
if (Dialog.isDisconnectKey(event.getKey())) {
disconnect(player);
return;
}
if (state.isDone() || state.submitting) return;
if (Dialog.isCancelKey(event.getKey())) {
cancel(player);
return;
}
if (!(event.getPayload() instanceof CompoundBinaryTag payload)) return;
boolean finished = Dialog.handleClick(player, state, event.getKey(), payload);
if (finished) complete(player, state);
}
private static void cancel(Player player) {
states.remove(player.getUuid());
lastLook.remove(player.getUuid());
player.closeDialog();
player.kick("Anmeldung abgebrochen. Du kannst dich jederzeit erneut anmelden.");
}
private static void disconnect(Player player) {
states.remove(player.getUuid());
lastLook.remove(player.getUuid());
player.closeDialog();
player.kick("Wir freuen uns auf dich!");
}
private static void complete(Player player, DialogState state) {
String birthdayIso;
try {
birthdayIso = LocalDate
.parse(state.birthday, DateTimeFormatter.ofPattern("dd.MM.uuuu", Locale.GERMANY))
.format(DateTimeFormatter.ISO_LOCAL_DATE);
} catch (RuntimeException e) {
state.goToPage(DialogState.Page.data);
Dialog.showError(player, state, "Bitte gib ein gültiges Geburtsdatum an.");
return;
}
state.submitting = true;
showDialog(player, state);
Thread.startVirtualThread(() -> {
try {
Api.signup(
state.firstName,
state.secondName,
birthdayIso,
state.phoneNumber,
signupUsername(player),
player.getUuid(),
isBedrockUuid(player.getUuid()) ? "bedrock" : "java"
);
} catch (Api.SignupError e) {
e.printStackTrace();
state.submitting = false;
handleSignupError(player, state, e);
return;
} catch (RuntimeException e) {
e.printStackTrace();
state.submitting = false;
if (player.isOnline())
Dialog.showError(player, state, "Anmeldung gerade nicht möglich. Bitte versuche es erneut.");
return;
}
finish(player, "Danke für deine Anmeldung! Du bist für das Projekt Craftattack 9 registriert, wir freuen uns auf dich!");
});
}
private static void handleSignupError(Player player, DialogState state, Api.SignupError e) {
switch (e.code) {
case "CONFLICT" -> finish(player, "Du bist bereits auf Craftattack registriert, wir freuen uns auf dich!");
case "BAD_REQUEST" -> {
state.goToPage(DialogState.Page.data);
if (player.isOnline()) Dialog.showError(player, state, "Deine Angaben sind nicht gültig. Bitte überprüfe sie.");
}
case "UNAUTHORIZED" -> {
if (player.isOnline()) Dialog.showError(player, state, "Server nicht autorisiert. Bitte Admin kontaktieren.");
}
default -> {
if (player.isOnline()) Dialog.showError(player, state, "Anmeldung fehlgeschlagen. Bitte versuche es erneut.");
}
}
}
private static void finish(Player player, String message) {
if (!player.isOnline()) {
states.remove(player.getUuid());
return;
}
DialogState done = DialogState.done(message);
states.put(player.getUuid(), done);
showDialog(player, done);
}
private static String signupUsername(Player player) {
String username = player.getUsername();
if (isBedrockUuid(player.getUuid()) && username.startsWith(".")) return username.substring(1);
return username;
}
public static boolean isBedrockUuid(UUID uuid) {
return uuid.getMostSignificantBits() == 0;
}
}
@@ -0,0 +1,31 @@
package eu.mhsl.craftattack.loginServer.listeners;
import eu.mhsl.craftattack.loginServer.Main;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerChatEvent;
public class ChatFormatHandler implements Listener {
@Override
public void register(EventNode<Event> node) {
boolean chatViaProxy = Main.globalConfig.node("velocity", "enabled").getBoolean(false)
&& Main.globalConfig.node("velocity", "chat-via-proxy").getBoolean(false);
node.addListener(PlayerChatEvent.class, event -> {
if (chatViaProxy) {
event.setCancelled(true);
return;
}
event.setFormattedMessage(
Component.text()
.append(Component.text(event.getPlayer().getUsername(), NamedTextColor.GRAY))
.append(Component.text(" » ", NamedTextColor.DARK_GRAY))
.append(Component.text(event.getRawMessage(), NamedTextColor.WHITE))
.build()
);
});
}
}
@@ -0,0 +1,13 @@
package eu.mhsl.craftattack.loginServer.listeners;
import eu.mhsl.craftattack.loginServer.PlayerHandler;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerCustomClickEvent;
public class DialogClickListener implements Listener {
@Override
public void register(EventNode<Event> node) {
node.addListener(PlayerCustomClickEvent.class, PlayerHandler::handleDialogClick);
}
}
@@ -1,4 +1,4 @@
package eu.mhsl.minenet.loginServer.listeners;
package eu.mhsl.craftattack.loginServer.listeners;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
@@ -11,9 +11,11 @@ public interface Listener {
static void registerAll(EventNode<Event> node, InstanceManager instanceManager) {
List<Listener> listeners = List.of(
new onPlayerJoin(instanceManager),
new onPlayerMove(),
new dialogOutput()
new PlayerJoinListener(instanceManager),
new PlayerDisconnectListener(),
new PlayerMoveListener(),
new DialogClickListener(),
new ChatFormatHandler()
);
listeners.forEach(listener -> listener.register(node));
@@ -0,0 +1,13 @@
package eu.mhsl.craftattack.loginServer.listeners;
import eu.mhsl.craftattack.loginServer.PlayerHandler;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerDisconnectEvent;
public class PlayerDisconnectListener implements Listener {
@Override
public void register(EventNode<Event> node) {
node.addListener(PlayerDisconnectEvent.class, event -> PlayerHandler.leave(event.getPlayer()));
}
}
@@ -0,0 +1,19 @@
package eu.mhsl.craftattack.loginServer.listeners;
import eu.mhsl.craftattack.loginServer.PlayerHandler;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.instance.InstanceManager;
public class PlayerJoinListener implements Listener {
private final InstanceManager instanceManager;
public PlayerJoinListener(InstanceManager instanceManager) {
this.instanceManager = instanceManager;
}
@Override
public void register(EventNode<Event> node) {
node.addListener(AsyncPlayerConfigurationEvent.class, event -> PlayerHandler.join(event, instanceManager));
}
}
@@ -1,14 +1,16 @@
package eu.mhsl.minenet.loginServer.listeners;
package eu.mhsl.craftattack.loginServer.listeners;
import eu.mhsl.craftattack.loginServer.PlayerHandler;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerMoveEvent;
public class onPlayerMove implements Listener {
public class PlayerMoveListener implements Listener {
@Override
public void register(EventNode<Event> node) {
node.addListener(PlayerMoveEvent.class, event -> {
event.setCancelled(true);
PlayerHandler.onMove(event.getPlayer(), event.getNewPosition().yaw(), event.getNewPosition().pitch());
});
}
}
@@ -1,83 +0,0 @@
package eu.mhsl.minenet.loginServer;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.text.Component;
import net.minestom.server.dialog.*;
import net.minestom.server.entity.Player;
import java.util.ArrayList;
import java.util.List;
public final class Dialog {
private static final Key submitKey = Key.key("profile", "submit");
public static void open(Player player) {
List<DialogInput> inputs = new ArrayList<>();
boolean isAfterStart = Main.globalConfig
.node("server", "is-after-start")
.getBoolean(false);
if (isAfterStart) {
inputs.add(new DialogInput.Text(
"secret", 300,
Component.text("Secret"),
true, "", 50, null
));
}
inputs.add(new DialogInput.Text(
"name", 300,
Component.text("Vorname"),
true, "", 50, null
));
inputs.add(new DialogInput.Text(
"secondName", 300,
Component.text("Nachname"),
true, "", 50, null
));
inputs.add(new DialogInput.Text(
"birthday", 300,
Component.text("Geburtstag (TT.MM.JJJJ)"),
true, "", 10, null
));
inputs.add(new DialogInput.Text(
"phoneNumber", 300,
Component.text("Telefonnummer (optional)"),
true, "", 30, null
));
net.minestom.server.dialog.Dialog dialog = new net.minestom.server.dialog.Dialog.Notice(
new DialogMetadata(
Component.text("Spielerprofil erstellen"),
null,
true,
false,
DialogAfterAction.CLOSE,
List.of(
new DialogBody.PlainMessage(
Component.text(
"Bitte fülle deine Profildaten aus."
),
300
)
),
inputs
),
new DialogActionButton(
Component.text("Speichern"),
null,
200,
new DialogAction.DynamicCustom(
submitKey,
CompoundBinaryTag.builder().build()
)
)
);
player.showDialog(dialog);
}
}
@@ -1,99 +0,0 @@
package eu.mhsl.minenet.loginServer.listeners;
import eu.mhsl.minenet.loginServer.Dialog;
import eu.mhsl.minenet.loginServer.Main;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerCustomClickEvent;
import net.minestom.server.timer.SchedulerManager;
import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;
import java.util.UUID;
public class dialogOutput implements Listener {
@Override
public void register(EventNode<Event> node) {
node.addListener(PlayerCustomClickEvent.class, event -> {
Player player = event.getPlayer();
SchedulerManager schedule = MinecraftServer.getSchedulerManager();
boolean isAfterStart = Main.globalConfig.node("server", "is-after-start").getBoolean(false);
if (!event.getKey().equals(
Key.key("profile", "submit"))) {
return;
}
if (!(event.getPayload() instanceof CompoundBinaryTag data)) {
return;
}
String requiredSecret = "test1234";
String secret = data.getString("secret");
String name = data.getString("name");
String secondName = data.getString("secondName");
String birthday = data.getString("birthday");
String phoneNumber = data.getString("phoneNumber");
String playerName = event.getPlayer().getUsername();
UUID playerUUID = event.getPlayer().getUuid();
if(isAfterStart && !secret.equals(requiredSecret)) {
player.sendMessage("Bitte gebe das richtige Secret an.");
schedule.buildTask(() -> {
Dialog.open(player);
}).delay(Duration.ofMillis(1500)).schedule();
return;
}
if (name.isBlank() || secondName.isBlank() || birthday.isBlank()) {
player.sendMessage("Bitte fülle alle Pflichtfelder aus.");
schedule.buildTask(() -> {
Dialog.open(player);
}).delay(Duration.ofMillis(1500)).schedule();
return;
}
if(!isValidBirthday(birthday)) {
player.sendMessage("Bitte gebe ein richtiges Geburtsdatum an.");
schedule.buildTask(() -> {
Dialog.open(player);
}).delay(Duration.ofMillis(1500)).schedule();
return;
}
player.sendMessage("Danke für deine Anmeldung.");
player.sendMessage("Du bist für das Projekt Craftattack 9 registriert!");
schedule.buildTask(() -> {
player.kick("Anmeldung erfolgreich!");
}).delay(Duration.ofMillis(1500)).schedule();
System.out.println("Vorname: " + name);
System.out.println("Nachname: " + secondName);
System.out.println("Geburtstag: " + birthday);
System.out.println("Telefonnummer: " + phoneNumber);
System.out.println("Spielername: " + playerName);
System.out.println("Spieler UUID: " + playerUUID);
});
}
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;
}
}
}
@@ -1,31 +0,0 @@
package eu.mhsl.minenet.loginServer.listeners;
import eu.mhsl.minenet.loginServer.Dialog;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.instance.InstanceManager;
import java.time.Duration;
public class onPlayerJoin implements Listener {
private final InstanceManager instanceManager;
public onPlayerJoin(InstanceManager instanceManager) {
this.instanceManager = instanceManager;
}
@Override
public void register(EventNode<Event> node) {
node.addListener(AsyncPlayerConfigurationEvent.class, event ->{
final Player player = event.getPlayer();
event.setSpawningInstance(instanceManager.createInstanceContainer());
player.setRespawnPoint(new Pos(0, 100, 0));
MinecraftServer.getSchedulerManager().buildTask(() -> {
Dialog.open(player);
}).delay(Duration.ofMillis(500)).schedule();
});
}
}
+5 -2
View File
@@ -3,7 +3,6 @@ server:
view-distance: 12
compression-threshold: 0
open-to-lan: false
is-after-start: false
bungeecord:
enabled: false
@@ -11,4 +10,8 @@ bungeecord:
velocity:
enabled: false
chat-via-proxy: false
secret: ''
secret: ''
api:
url: ""
secret: ""