Compare commits

...
8 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
9 changed files with 406 additions and 152 deletions

No files matched your search

@@ -1,86 +1,104 @@
package eu.mhsl.craftattack.loginServer; package eu.mhsl.craftattack.loginServer;
import com.google.gson.JsonArray; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Optional; import java.util.Base64;
import java.util.UUID; import java.util.UUID;
public final class Api { public final class Api {
private static final HttpClient client = HttpClient.newHttpClient(); private static final HttpClient client = HttpClient.newHttpClient();
private static final boolean offline = false;
private Api() {} private Api() {}
public static void setProfile(UUID uuid, String username, String name, String secondName, String birthday, String phoneNumber) {
JsonObject body = new JsonObject();
body.addProperty("uuid", uuid.toString());
body.addProperty("username", username);
body.addProperty("name", name);
body.addProperty("secondName", secondName);
body.addProperty("birthday", birthday);
body.addProperty("phoneNumber", phoneNumber);
send("POST", "players", "", body);
}
public static boolean isRegistered(UUID uuid) { public static boolean isRegistered(UUID uuid) {
return find("players", "uuid", uuid.toString()).isPresent(); 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 boolean isSecretValid(String username, String secret) { public static final class SignupError extends RuntimeException {
return find("secrets", "player", username, "secret", secret).isPresent(); public final String code;
public SignupError(String code, String message) {
super(message);
this.code = code;
}
} }
private static Optional<JsonObject> find(String collection, String... fieldValuePairs) { public static void signup(
StringBuilder filter = new StringBuilder(); String firstname,
for (int i = 0; i < fieldValuePairs.length; i += 2) { String lastname,
if (i > 0) filter.append(" && "); String birthdayIso,
String escaped = fieldValuePairs[i + 1].replace("\\", "\\\\").replace("'", "\\'"); String phone,
filter.append(fieldValuePairs[i]).append("='").append(escaped).append("'"); String username,
UUID uuid,
String edition
) {
if (offline) {
System.out.println("[Api offline] signup: " + firstname + " " + lastname + ", " + birthdayIso
+ ", " + phone + ", " + username + ", " + uuid + ", " + edition);
return;
} }
String query = "?perPage=1&filter=" + URLEncoder.encode(filter.toString(), StandardCharsets.UTF_8); JsonObject body = new JsonObject();
JsonObject response = send("GET", collection, query, null); body.addProperty("firstname", firstname);
if (!response.has("items")) return Optional.empty(); body.addProperty("lastname", lastname);
JsonArray items = response.getAsJsonArray("items"); body.addProperty("birthday", birthdayIso);
return items.isEmpty() ? Optional.empty() : Optional.of(items.get(0).getAsJsonObject()); 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 JsonObject send(String method, String collection, String path, JsonObject body) { private static HttpResponse<String> call(String method, String path, String body, boolean withAuth) {
JsonObject login = new JsonObject();
login.addProperty("identity", config("email"));
login.addProperty("password", config("password"));
String token = call("POST", "/api/collections/_superusers/auth-with-password", login, null)
.get("token").getAsString();
return call(method, "/api/collections/" + collection + "/records" + path, body, token);
}
private static JsonObject call(String method, String path, JsonObject body, String token) {
try { try {
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(config("url") + path)) HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(config("url") + path))
.timeout(Duration.ofSeconds(5)) .timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.method(method, body == null .method(method, body == null
? HttpRequest.BodyPublishers.noBody() ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body.toString())); : HttpRequest.BodyPublishers.ofString(body));
if (token != null) builder.header("Authorization", token);
HttpResponse<String> response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (withAuth) {
if (response.statusCode() >= 300) { String secret = config("secret");
throw new IllegalStateException("API-Fehler " + response.statusCode() + ": " + response.body()); if (!secret.isBlank()) {
String credentials = Base64.getEncoder()
.encodeToString(("api:" + secret).getBytes(StandardCharsets.UTF_8));
builder.header("Authorization", "Basic " + credentials);
}
} }
if (response.body().isBlank()) return new JsonObject();
return JsonParser.parseString(response.body()).getAsJsonObject(); return client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
throw new IllegalStateException("API nicht erreichbar", e); throw new IllegalStateException("API nicht erreichbar", e);
} }
@@ -89,12 +107,4 @@ public final class Api {
private static String config(String key) { private static String config(String key) {
return Main.globalConfig.node("api", key).getString(""); return Main.globalConfig.node("api", key).getString("");
} }
public static boolean removeSecret(String username, String secret) {
Optional<JsonObject> entry = find("secrets", "player", username, "secret", secret);
if (entry.isEmpty()) return false;
send("DELETE", "secrets", "/" + entry.get().get("id").getAsString(), null);
return true;
}
} }
@@ -22,11 +22,15 @@ public final class Dialog {
private static final Key nextKey = Key.key(namespace, "next"); private static final Key nextKey = Key.key(namespace, "next");
private static final Key backKey = Key.key(namespace, "back"); private static final Key backKey = Key.key(namespace, "back");
private static final Key finishKey = Key.key(namespace, "finish"); 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 privacyUrl = "https://mhsl.eu/id.html";
private static final String rulesUrl = "https://mhsl.eu/craftattack/rules"; private static final String rulesUrl = "https://mhsl.eu/craftattack/rules";
private static final int rulesWidth = 300; 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 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.";
@@ -49,19 +53,66 @@ public final class Dialog {
return key.namespace().equals(namespace); 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) { public static void open(Player player, DialogState state) {
showPage(player, state, null); 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) { public static boolean handleClick(Player player, DialogState state, Key key, CompoundBinaryTag payload) {
state.pendingError = null;
saveCurrentPage(state, payload);
if (key.equals(backKey)) { if (key.equals(backKey)) {
state.previousPage(); state.previousPage();
showPage(player, state, null); showPage(player, state, null);
return false; return false;
} }
saveCurrentPage(state, payload);
Component error = validateCurrentPage(player, state); Component error = validateCurrentPage(player, state);
if (error != null) { if (error != null) {
showPage(player, state, error); showPage(player, state, error);
@@ -83,50 +134,52 @@ public final class Dialog {
String title; String title;
switch (state.currentPage()) { switch (state.currentPage()) {
case secret -> {
title = "Secret";
body.add(plain("Bitte gib das Secret ein, das du erhalten hast."));
inputs.add(text("secret", "Secret", state.secret, 50));
}
case data -> { case data -> {
title = "Profildaten"; title = "Profildaten";
body.add(plain("Bitte fülle deine Profildaten aus.")); body.add(plain("Bitte fülle deine Profildaten aus."));
inputs.add(text("name", "Vorname", state.firstName, 50)); inputs.add(text("name", "Vorname (*)", state.firstName, 50));
inputs.add(text("secondName", "Nachname", state.secondName, 50)); inputs.add(text("secondName", "Nachname (*)", state.secondName, 50));
inputs.add(text("birthday", "Geburtstag (TT.MM.JJJJ)", state.birthday, 10)); inputs.add(text("birthday", "Geburtstag (TT.MM.JJJJ) (*)", state.birthday, 10));
inputs.add(text("phoneNumber", "Telefonnummer (optional)", state.phoneNumber, 30)); inputs.add(text("phoneNumber", "Telefonnummer (optional)", state.phoneNumber, 30));
} }
case consent -> { case consent -> {
title = "Einverständnis"; title = "Serverregeln";
body.add(plain(Component.text("Serverregeln", NamedTextColor.GOLD, TextDecoration.BOLD), rulesWidth)); body.add(plain(Component.text("Serverregeln", NamedTextColor.GOLD, TextDecoration.BOLD), rulesWidth));
body.add(plain(Component.text(rulesPreamble, NamedTextColor.GRAY), rulesWidth)); body.add(plain(Component.text(rulesPreamble, NamedTextColor.GRAY), rulesWidth));
for (String rule : rules) { for (String rule : rules) {
body.add(plain(Component.text(rule), rulesWidth)); body.add(plain(Component.text(rule), rulesWidth));
} }
boolean bedrock = PlayerHandler.isBedrockUuid(player.getUuid());
body.add(plain(Component.text() body.add(plain(Component.text()
.append(link("Datenschutzerklärung", privacyUrl)) .append(bedrock ? Component.text(privacyUrl) : link("Datenschutzerklärung", privacyUrl))
.append(Component.text(" | ")) .append(Component.text(" | "))
.append(link("Vollständige Regeln", rulesUrl)) .append(bedrock ? Component.text(rulesUrl) : link("Vollständige Regeln", rulesUrl))
.build(), rulesWidth)); .build(), rulesWidth));
}
case privacy -> {
title = "Einverständnis (1/3)";
body.add(plain(Component.text( body.add(plain(Component.text(
"Mit den drei Haken erklärst du: " + "Ich bin mit der Speicherung meiner in der Anmeldung angegebenen, persönlichen Daten einverstanden.",
"1) Ich bin mit der Speicherung meiner in der Anmeldung angegebenen, persönlichen Daten einverstanden. " + NamedTextColor.GRAY), rulesWidth));
"2) Ich bin mit der Speicherung in Form von Logs aller meiner, beim Spielen anfallenden, " + 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. " + "persönlichen Daten durch den Server einverstanden. " +
"Die Log-Speicherung betrifft jede Interaktion im Spiel und zugehörige Daten, " + "Die Log-Speicherung betrifft jede Interaktion im Spiel und zugehörige Daten, " +
"z.B. Chatnachrichten, welche vom Minecraft-Client an den Server übermittelt werden. " + "z.B. Chatnachrichten, welche vom Minecraft-Client an den Server übermittelt werden.",
"3) Ich bin mit den Regeln einverstanden und achte sie.",
NamedTextColor.GRAY), rulesWidth)); NamedTextColor.GRAY), rulesWidth));
inputs.add(checkbox("logs", "Log-Speicherung zustimmen (*)", state.logsAccepted));
inputs.add(checkbox("privacy", }
"Ich stimme der Speicherung meiner\nAnmeldedaten zu. *", case rules -> {
state.privacyAccepted)); title = "Einverständnis (3/3)";
inputs.add(checkbox("logs", body.add(plain(Component.text(
"Ich stimme der Speicherung meiner\nSpiel-Logs zu. *", "Ich bin mit den Regeln einverstanden und achte sie.",
state.logsAccepted)); NamedTextColor.GRAY), rulesWidth));
inputs.add(checkbox("rules", inputs.add(checkbox("rules", "Regeln zustimmen (*)", state.rulesAccepted));
"Ich akzeptiere die Regeln\nund halte sie ein. *",
state.rulesAccepted));
} }
default -> throw new IllegalStateException(); default -> throw new IllegalStateException();
} }
@@ -135,10 +188,13 @@ public final class Dialog {
String fullTitle = "Schritt " + state.pageNumber() + "/" + state.pageCount() + " – " + title; String fullTitle = "Schritt " + state.pageNumber() + "/" + state.pageCount() + " – " + title;
boolean bedrock = PlayerHandler.isBedrockUuid(player.getUuid());
List<DialogActionButton> buttons = new ArrayList<>(); List<DialogActionButton> buttons = new ArrayList<>();
if (!state.isFirstPage()) buttons.add(button("← Zurück", backKey));
if (state.isLastPage()) buttons.add(button("Anmeldung abschließen", finishKey)); if (state.isLastPage()) buttons.add(button("Anmeldung abschließen", finishKey));
else buttons.add(button("Weiter →", nextKey)); 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( player.showDialog(new net.minestom.server.dialog.Dialog.MultiAction(
new DialogMetadata( new DialogMetadata(
@@ -146,53 +202,48 @@ public final class Dialog {
null, null,
false, false,
false, false,
state.isLastPage() ? DialogAfterAction.CLOSE : DialogAfterAction.NONE, bedrock || state.isLastPage() ? DialogAfterAction.CLOSE : DialogAfterAction.NONE,
body, body,
inputs inputs
), ),
buttons, buttons,
null, null,
2 state.isFirstPage() ? 1 : 2
)); ));
} }
private static void saveCurrentPage(DialogState state, CompoundBinaryTag payload) { private static void saveCurrentPage(DialogState state, CompoundBinaryTag payload) {
switch (state.currentPage()) { switch (state.currentPage()) {
case secret -> state.secret = payload.getString("secret", "").trim();
case data -> { case data -> {
state.firstName = payload.getString("name", "").trim(); state.firstName = payload.getString("name", "").trim();
state.secondName = payload.getString("secondName", "").trim(); state.secondName = payload.getString("secondName", "").trim();
state.birthday = payload.getString("birthday", "").trim(); state.birthday = payload.getString("birthday", "").trim();
state.phoneNumber = payload.getString("phoneNumber", "").trim(); state.phoneNumber = payload.getString("phoneNumber", "").trim();
} }
case consent -> { case consent -> {}
state.privacyAccepted = payload.getBoolean("privacy", false); case privacy -> state.privacyAccepted = payload.getBoolean("privacy", false);
state.logsAccepted = payload.getBoolean("logs", false); case logs -> state.logsAccepted = payload.getBoolean("logs", false);
state.rulesAccepted = payload.getBoolean("rules", false); case rules -> state.rulesAccepted = payload.getBoolean("rules", false);
}
} }
} }
private static Component validateCurrentPage(Player player, DialogState state) { private static Component validateCurrentPage(Player player, DialogState state) {
switch (state.currentPage()) { switch (state.currentPage()) {
case secret -> {
try {
if (!Api.isSecretValid(player.getUsername(), state.secret))
return error("Das Secret ist ungültig.");
} catch (IllegalStateException e) {
e.printStackTrace();
return error("Die Überprüfung ist gerade nicht möglich. Bitte versuche es später erneut.");
}
}
case data -> { case data -> {
if (state.firstName.isBlank() || state.secondName.isBlank() || state.birthday.isBlank()) if (state.firstName.isBlank() || state.secondName.isBlank() || state.birthday.isBlank())
return error("Bitte fülle alle Pflichtfelder aus."); return error("Bitte fülle alle Pflichtfelder aus.");
if (!isValidBirthday(state.birthday)) if (!isValidBirthday(state.birthday))
return error("Bitte gib ein richtiges Geburtsdatum an."); return error("Bitte gib ein richtiges Geburtsdatum an.");
} }
case consent -> { case consent -> {}
if (!(state.privacyAccepted && state.logsAccepted && state.rulesAccepted)) case privacy -> {
return error("Du musst allen drei Punkten zustimmen, um fortzufahren."); 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; return null;
@@ -211,14 +262,23 @@ public final class Dialog {
} }
private static DialogActionButton button(String label, Key key) { 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( return new DialogActionButton(
Component.text(label), Component.text(label),
null, null,
150, width,
new DialogAction.DynamicCustom(key, CompoundBinaryTag.builder().build()) 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) { 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); return new DialogInput.Text(key, 300, Component.text(label), true, initial, maxLength, null);
} }
@@ -242,9 +302,4 @@ public final class Dialog {
private static Component error(String text) { private static Component error(String text) {
return Component.text(text, NamedTextColor.RED); return Component.text(text, NamedTextColor.RED);
} }
private static Component link(String text, String url) {
return Component.text(text, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
.clickEvent(ClickEvent.openUrl(url));
}
} }
@@ -1,14 +1,15 @@
package eu.mhsl.craftattack.loginServer; package eu.mhsl.craftattack.loginServer;
import net.kyori.adventure.text.Component;
import java.util.List; import java.util.List;
public final class DialogState { public final class DialogState {
public enum Page { secret, data, consent } public enum Page { data, consent, privacy, logs, rules }
private final List<Page> pages; private static final List<Page> pages = List.of(Page.data, Page.consent, Page.privacy, Page.logs, Page.rules);
private int pageIndex = 0; private int pageIndex = 0;
String secret = "";
String firstName = ""; String firstName = "";
String secondName = ""; String secondName = "";
String birthday = ""; String birthday = "";
@@ -17,18 +18,28 @@ public final class DialogState {
boolean logsAccepted = false; boolean logsAccepted = false;
boolean rulesAccepted = false; boolean rulesAccepted = false;
private DialogState(List<Page> pages) { private String doneMessage = null;
this.pages = pages; volatile boolean submitting = false;
} Component pendingError = null;
private DialogState() {}
public static DialogState create() { public static DialogState create() {
boolean isAfterStart = Main.globalConfig return new DialogState();
.node("server", "is-after-start") }
.getBoolean(false);
return new DialogState(isAfterStart public static DialogState done(String message) {
? List.of(Page.secret, Page.data, Page.consent) DialogState state = new DialogState();
: List.of(Page.data, Page.consent)); state.doneMessage = message;
return state;
}
public boolean isDone() {
return doneMessage != null;
}
public String doneMessage() {
return doneMessage;
} }
public Page currentPage() { public Page currentPage() {
@@ -58,4 +69,8 @@ public final class DialogState {
public void previousPage() { public void previousPage() {
pageIndex = Math.max(0, pageIndex - 1); pageIndex = Math.max(0, pageIndex - 1);
} }
public void goToPage(Page page) {
pageIndex = pages.indexOf(page);
}
} }
@@ -6,37 +6,86 @@ import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.event.player.PlayerCustomClickEvent; import net.minestom.server.event.player.PlayerCustomClickEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.InstanceManager;
import net.minestom.server.timer.Task;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public final class PlayerHandler { public final class PlayerHandler {
private static final Map<UUID, DialogState> states = new ConcurrentHashMap<>(); 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() {} private PlayerHandler() {}
public static void join(AsyncPlayerConfigurationEvent event, InstanceManager instanceManager) { public static void join(AsyncPlayerConfigurationEvent event, InstanceManager instanceManager) {
Player player = event.getPlayer(); Player player = event.getPlayer();
event.setSpawningInstance(instanceManager.createInstanceContainer()); Instance instance = instanceManager.createInstanceContainer();
instance.defaultClock().time(18000);
instance.defaultClock().pause();
event.setSpawningInstance(instance);
player.setRespawnPoint(new Pos(0, 100, 0)); 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())) { if (Api.isRegistered(player.getUuid())) {
player.kick("Du bist bereits auf Craftattack registriert, wir freuen uns auf dich!"); state = DialogState.done("Du bist bereits auf Craftattack registriert, wir freuen uns auf dich!");
return; states.put(player.getUuid(), state);
} else {
state = states.compute(player.getUuid(), (id, existing) ->
existing == null || existing.isDone() ? DialogState.create() : existing);
} }
DialogState state = states.computeIfAbsent(player.getUuid(), id -> DialogState.create());
MinecraftServer.getSchedulerManager() MinecraftServer.getSchedulerManager()
.buildTask(() -> Dialog.open(player, state)) .buildTask(() -> showDialog(player, state))
.delay(Duration.ofMillis(500)) .delay(Duration.ofMillis(500))
.schedule(); .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) { public static void handleDialogClick(PlayerCustomClickEvent event) {
if (!Dialog.isDialogKey(event.getKey())) return; if (!Dialog.isDialogKey(event.getKey())) return;
@@ -44,36 +93,113 @@ public final class PlayerHandler {
DialogState state = states.get(player.getUuid()); DialogState state = states.get(player.getUuid());
if (state == null) return; 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; if (!(event.getPayload() instanceof CompoundBinaryTag payload)) return;
boolean finished = Dialog.handleClick(player, state, event.getKey(), payload); boolean finished = Dialog.handleClick(player, state, event.getKey(), payload);
if (finished) complete(player, state); if (finished) complete(player, state);
} }
private static void complete(Player player, DialogState state) { private static void cancel(Player player) {
states.remove(player.getUuid()); states.remove(player.getUuid());
lastLook.remove(player.getUuid());
player.closeDialog();
player.kick("Anmeldung abgebrochen. Du kannst dich jederzeit erneut anmelden.");
}
if (Api.isRegistered(player.getUuid())) { private static void disconnect(Player player) {
player.kick("Du bist bereits auf Craftattack registriert, wir freuen uns auf dich!"); 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; return;
} }
player.sendMessage("Danke für deine Anmeldung."); state.submitting = true;
player.sendMessage("Du bist für das Projekt Craftattack 9 registriert!"); showDialog(player, state);
Api.setProfile(player.getUuid(), player.getUsername(), state.firstName, state.secondName, state.birthday, state.phoneNumber); Thread.startVirtualThread(() -> {
Api.removeSecret(player.getUsername(), state.secret); 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;
}
MinecraftServer.getSchedulerManager() finish(player, "Danke für deine Anmeldung! Du bist für das Projekt Craftattack 9 registriert, wir freuen uns auf dich!");
.buildTask(() -> player.kick("Anmeldung erfolgreich, wir freuen uns auf dich!")) });
.delay(Duration.ofMillis(1000)) }
.schedule();
System.out.println("Vorname: " + state.firstName); private static void handleSignupError(Player player, DialogState state, Api.SignupError e) {
System.out.println("Nachname: " + state.secondName); switch (e.code) {
System.out.println("Geburtstag: " + state.birthday); case "CONFLICT" -> finish(player, "Du bist bereits auf Craftattack registriert, wir freuen uns auf dich!");
System.out.println("Telefonnummer: " + state.phoneNumber); case "BAD_REQUEST" -> {
System.out.println("Spielername: " + player.getUsername()); state.goToPage(DialogState.Page.data);
System.out.println("Spieler UUID: " + player.getUuid()); 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()
);
});
}
}
@@ -12,8 +12,10 @@ public interface Listener {
static void registerAll(EventNode<Event> node, InstanceManager instanceManager) { static void registerAll(EventNode<Event> node, InstanceManager instanceManager) {
List<Listener> listeners = List.of( List<Listener> listeners = List.of(
new PlayerJoinListener(instanceManager), new PlayerJoinListener(instanceManager),
new PlayerDisconnectListener(),
new PlayerMoveListener(), new PlayerMoveListener(),
new DialogClickListener() new DialogClickListener(),
new ChatFormatHandler()
); );
listeners.forEach(listener -> listener.register(node)); 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()));
}
}
@@ -1,5 +1,6 @@
package eu.mhsl.craftattack.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.Event;
import net.minestom.server.event.EventNode; import net.minestom.server.event.EventNode;
import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.event.player.PlayerMoveEvent;
@@ -7,6 +8,9 @@ import net.minestom.server.event.player.PlayerMoveEvent;
public class PlayerMoveListener implements Listener { public class PlayerMoveListener implements Listener {
@Override @Override
public void register(EventNode<Event> node) { public void register(EventNode<Event> node) {
node.addListener(PlayerMoveEvent.class, event -> event.setCancelled(true)); node.addListener(PlayerMoveEvent.class, event -> {
event.setCancelled(true);
PlayerHandler.onMove(event.getPlayer(), event.getNewPosition().yaw(), event.getNewPosition().pitch());
});
} }
} }
+1 -3
View File
@@ -3,7 +3,6 @@ server:
view-distance: 12 view-distance: 12
compression-threshold: 0 compression-threshold: 0
open-to-lan: false open-to-lan: false
is-after-start: false
bungeecord: bungeecord:
enabled: false enabled: false
@@ -15,5 +14,4 @@ velocity:
api: api:
url: "" url: ""
email: "" secret: ""
password: ""