rules
This commit is contained in:
@@ -6,7 +6,6 @@ public final class AuthConfig {
|
||||
public boolean protectPremiumUsernames = true;
|
||||
public int authenticationTimeoutSeconds = 120;
|
||||
public int maxLoginAttempts = 3;
|
||||
public int reminderIntervalSeconds = 8;
|
||||
public int minimumPasswordLength = 6;
|
||||
public int pbkdf2Iterations = 210000;
|
||||
public String mojangProfileUrl = "https://api.minecraftservices.com/minecraft/profile/lookup/name/{username}";
|
||||
@@ -20,7 +19,4 @@ public final class AuthConfig {
|
||||
*/
|
||||
public boolean requireLoginEveryJoin = false;
|
||||
|
||||
/** Rules are accepted once per version. Increment to require acceptance again. */
|
||||
public boolean rulesEnabled = true;
|
||||
public int rulesVersion = 1;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
@@ -23,6 +24,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -40,6 +42,7 @@ public final class AuthManager {
|
||||
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
|
||||
private final Map<String, PremiumCache> premiumCache = new ConcurrentHashMap<>();
|
||||
private AuthConfig config = new AuthConfig();
|
||||
private RulesConfig rules = new RulesConfig();
|
||||
|
||||
public boolean load() {
|
||||
try {
|
||||
@@ -52,6 +55,14 @@ public final class AuthManager {
|
||||
}
|
||||
saveConfig(); // writes newly-added defaults/fields during upgrades
|
||||
|
||||
if (Files.exists(ConfigPaths.AUTH_RULES)) {
|
||||
try (Reader r = Files.newBufferedReader(ConfigPaths.AUTH_RULES)) {
|
||||
RulesConfig loadedRules = GSON.fromJson(r, RulesConfig.class);
|
||||
if (loadedRules != null) rules = loadedRules;
|
||||
}
|
||||
}
|
||||
saveRules(); // creates rules.json on first run and writes newly-added defaults
|
||||
|
||||
if (Files.exists(ConfigPaths.AUTH_ACCOUNTS)) {
|
||||
try (Reader r = Files.newBufferedReader(ConfigPaths.AUTH_ACCOUNTS)) {
|
||||
Map<String, Account> loaded = GSON.fromJson(r, new TypeToken<Map<String, Account>>(){}.getType());
|
||||
@@ -98,7 +109,7 @@ public final class AuthManager {
|
||||
s.stage = Stage.RULES;
|
||||
s.joinedAt = System.currentTimeMillis();
|
||||
s.attempts = 0;
|
||||
showDialog(player, DIALOG_RULES);
|
||||
showRulesDialog(player);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -183,11 +194,10 @@ public final class AuthManager {
|
||||
|
||||
if (needsRules(account)) {
|
||||
s.stage = Stage.RULES;
|
||||
showDialog(player, DIALOG_RULES);
|
||||
return;
|
||||
} else {
|
||||
prepareAfterRules(player);
|
||||
}
|
||||
|
||||
continueAfterRules(player);
|
||||
scheduleInitialDialog(s);
|
||||
}
|
||||
|
||||
public void onDisconnect(UUID uuid) { sessions.remove(uuid); }
|
||||
@@ -210,10 +220,13 @@ public final class AuthManager {
|
||||
public void tick(MinecraftServer server) {
|
||||
if (!config.enabled) return;
|
||||
long now = System.currentTimeMillis();
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
// Use a snapshot: disconnect() can synchronously remove a player from PlayerList.
|
||||
// Iterating the live ArrayList while doing that caused the v1.8.1 server crash.
|
||||
for (ServerPlayer player : List.copyOf(server.getPlayerList().getPlayers())) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null || s.authenticated) continue;
|
||||
if (now - s.joinedAt >= config.authenticationTimeoutSeconds * 1000L) {
|
||||
long remainingMillis = config.authenticationTimeoutSeconds * 1000L - (now - s.joinedAt);
|
||||
if (remainingMillis <= 0L) {
|
||||
player.connection.disconnect(Component.literal("Authentication timed out. Please reconnect and try again."));
|
||||
continue;
|
||||
}
|
||||
@@ -222,10 +235,24 @@ public final class AuthManager {
|
||||
if (player.distanceToSqr(s.x, s.y, s.z) > 0.01) {
|
||||
player.connection.teleport(s.x, s.y, s.z, s.yaw, s.pitch);
|
||||
}
|
||||
if (now - s.lastReminder >= config.reminderIntervalSeconds * 1000L) {
|
||||
s.lastReminder = now;
|
||||
|
||||
// JOIN can fire just before the player becomes targetable by /dialog. Defer the
|
||||
// first dialog slightly instead of spamming a failing command immediately.
|
||||
if (s.dialogPending && now >= s.dialogReadyAt) {
|
||||
s.dialogPending = false;
|
||||
showCurrentDialog(player, s);
|
||||
}
|
||||
|
||||
// A live countdown is shown in the action bar. Do not reopen the dialog every
|
||||
// few seconds: reopening destroys text currently typed into password fields.
|
||||
long remainingSeconds = Math.max(1L, (remainingMillis + 999L) / 1000L);
|
||||
if (remainingSeconds != s.lastCountdownSecond) {
|
||||
s.lastCountdownSecond = remainingSeconds;
|
||||
player.connection.send(new ClientboundSetActionBarTextPacket(
|
||||
Component.literal("Authentication time remaining: " + remainingSeconds + "s")
|
||||
.withStyle(net.minecraft.ChatFormatting.YELLOW)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +262,7 @@ public final class AuthManager {
|
||||
if (s == null || s.authenticated || s.stage != Stage.RULES) return 0;
|
||||
if (!accepted) {
|
||||
player.sendSystemMessage(Component.literal("§cYou must accept the server rules to continue."));
|
||||
showDialog(player, DIALOG_RULES);
|
||||
showRulesDialog(player);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -244,7 +271,7 @@ public final class AuthManager {
|
||||
Account a = accounts.computeIfAbsent(key(name), ignored -> Account.metadataOnly(s.premium));
|
||||
a.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
||||
if (s.premium) a.officialUuid = player.getUUID().toString();
|
||||
a.rulesVersionAccepted = config.rulesVersion;
|
||||
a.rulesVersionAccepted = rules.version;
|
||||
a.rulesAcceptedAt = Instant.now().toString();
|
||||
saveAccounts();
|
||||
}
|
||||
@@ -316,20 +343,28 @@ public final class AuthManager {
|
||||
private void continueAfterRules(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) return;
|
||||
if (prepareAfterRules(player)) showCurrentDialog(player, s);
|
||||
}
|
||||
|
||||
/** Selects the post-rules stage. Returns true when a dialog is required. */
|
||||
private boolean prepareAfterRules(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) return false;
|
||||
|
||||
if (s.premium && !config.requireLoginEveryJoin) {
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isRegistered(player.getName().getString())) {
|
||||
s.stage = Stage.LOGIN;
|
||||
showDialog(player, DIALOG_LOGIN);
|
||||
} else {
|
||||
s.stage = Stage.REGISTER;
|
||||
showDialog(player, DIALOG_REGISTER);
|
||||
}
|
||||
s.stage = isRegistered(player.getName().getString()) ? Stage.LOGIN : Stage.REGISTER;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void scheduleInitialDialog(Session s) {
|
||||
if (s == null || s.authenticated) return;
|
||||
s.dialogPending = true;
|
||||
s.dialogReadyAt = System.currentTimeMillis() + 1000L;
|
||||
}
|
||||
|
||||
private void authenticate(ServerPlayer player) {
|
||||
@@ -342,7 +377,7 @@ public final class AuthManager {
|
||||
}
|
||||
|
||||
private boolean needsRules(Account account) {
|
||||
return config.rulesEnabled && (account == null || account.rulesVersionAccepted < config.rulesVersion);
|
||||
return rules.enabled && (account == null || account.rulesVersionAccepted < rules.version);
|
||||
}
|
||||
|
||||
private void ensurePremiumMetadata(ServerPlayer player, Account existing) {
|
||||
@@ -361,13 +396,78 @@ public final class AuthManager {
|
||||
|
||||
private void showCurrentDialog(ServerPlayer player, Session s) {
|
||||
switch (s.stage) {
|
||||
case RULES -> showDialog(player, DIALOG_RULES);
|
||||
case RULES -> showRulesDialog(player);
|
||||
case REGISTER -> showDialog(player, DIALOG_REGISTER);
|
||||
case LOGIN -> showDialog(player, DIALOG_LOGIN);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private void showRulesDialog(ServerPlayer player) {
|
||||
try {
|
||||
com.google.gson.JsonObject dialog = new com.google.gson.JsonObject();
|
||||
dialog.addProperty("type", "minecraft:notice");
|
||||
dialog.add("title", textComponent(rules.title, "yellow", true));
|
||||
dialog.addProperty("can_close_with_escape", false);
|
||||
dialog.addProperty("pause", false);
|
||||
dialog.addProperty("after_action", "close");
|
||||
|
||||
com.google.gson.JsonArray body = new com.google.gson.JsonArray();
|
||||
body.add(plainMessage(rules.introduction, "yellow"));
|
||||
StringBuilder lines = new StringBuilder();
|
||||
if (rules.rules != null) {
|
||||
for (int i = 0; i < rules.rules.size(); i++) {
|
||||
if (i > 0) lines.append("\n\n");
|
||||
lines.append(i + 1).append(". ").append(rules.rules.get(i));
|
||||
}
|
||||
}
|
||||
body.add(plainMessage(lines.toString(), "white"));
|
||||
body.add(plainMessage(rules.footer + "\n\nRules version: " + rules.version, "gray"));
|
||||
dialog.add("body", body);
|
||||
|
||||
com.google.gson.JsonArray inputs = new com.google.gson.JsonArray();
|
||||
com.google.gson.JsonObject agreement = new com.google.gson.JsonObject();
|
||||
agreement.addProperty("type", "minecraft:boolean");
|
||||
agreement.addProperty("key", "accepted");
|
||||
agreement.add("label", textComponent(rules.agreementLabel, null, false));
|
||||
agreement.addProperty("initial", false);
|
||||
agreement.addProperty("on_true", "true");
|
||||
agreement.addProperty("on_false", "false");
|
||||
inputs.add(agreement);
|
||||
dialog.add("inputs", inputs);
|
||||
|
||||
com.google.gson.JsonObject button = new com.google.gson.JsonObject();
|
||||
button.add("label", textComponent(rules.acceptButton, "green", false));
|
||||
com.google.gson.JsonObject action = new com.google.gson.JsonObject();
|
||||
action.addProperty("type", "dynamic/run_command");
|
||||
action.addProperty("template", "lobbyauth rules $(accepted)");
|
||||
button.add("action", action);
|
||||
dialog.add("action", button);
|
||||
|
||||
String command = "dialog show " + player.getGameProfile().name() + " " + GSON.toJson(dialog);
|
||||
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
||||
LobbySystemsMod.LOGGER.info("Displayed rules dialog v{} to {}", rules.version, player.getName().getString());
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to show rules dialog v{} to {}", rules.version, player.getName().getString(), e);
|
||||
player.sendSystemMessage(Component.literal("§cServer rules UI could not be opened. Please reconnect or contact staff."));
|
||||
}
|
||||
}
|
||||
|
||||
private static com.google.gson.JsonObject textComponent(String text, String color, boolean bold) {
|
||||
com.google.gson.JsonObject o = new com.google.gson.JsonObject();
|
||||
o.addProperty("text", text == null ? "" : text);
|
||||
if (color != null) o.addProperty("color", color);
|
||||
if (bold) o.addProperty("bold", true);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static com.google.gson.JsonObject plainMessage(String text, String color) {
|
||||
com.google.gson.JsonObject o = new com.google.gson.JsonObject();
|
||||
o.addProperty("type", "minecraft:plain_message");
|
||||
o.add("contents", textComponent(text, color, false));
|
||||
return o;
|
||||
}
|
||||
|
||||
private void showDialog(ServerPlayer player, String id) {
|
||||
try {
|
||||
String command = "dialog show " + player.getGameProfile().name() + " " + id;
|
||||
@@ -448,6 +548,10 @@ public final class AuthManager {
|
||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); }
|
||||
}
|
||||
|
||||
private void saveRules() throws Exception {
|
||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_RULES)) { GSON.toJson(rules, w); }
|
||||
}
|
||||
|
||||
private void saveAccounts() {
|
||||
try {
|
||||
Files.createDirectories(ConfigPaths.AUTH_DIRECTORY);
|
||||
@@ -493,7 +597,10 @@ public final class AuthManager {
|
||||
final boolean premium;
|
||||
int attempts;
|
||||
long joinedAt;
|
||||
long lastReminder;
|
||||
long lastReminder; // retained for binary/source migration; no longer reopens dialogs
|
||||
boolean dialogPending;
|
||||
long dialogReadyAt;
|
||||
long lastCountdownSecond = -1L;
|
||||
final double x, y, z;
|
||||
final float yaw, pitch;
|
||||
Stage stage = Stage.RULES;
|
||||
|
||||
@@ -16,6 +16,7 @@ public final class ConfigPaths {
|
||||
public static final Path AUTH_DIRECTORY = DIRECTORY.resolve("auth");
|
||||
public static final Path AUTH_CONFIG = AUTH_DIRECTORY.resolve("config.json");
|
||||
public static final Path AUTH_ACCOUNTS = AUTH_DIRECTORY.resolve("accounts.json");
|
||||
public static final Path AUTH_RULES = AUTH_DIRECTORY.resolve("rules.json");
|
||||
|
||||
private ConfigPaths() {}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
authLoaded ? "AUTH SYSTEM READY" : "AUTH SYSTEM LOAD FAILED",
|
||||
"Config: " + ConfigPaths.AUTH_CONFIG,
|
||||
"Accounts: " + ConfigPaths.AUTH_ACCOUNTS,
|
||||
"Rules: " + ConfigPaths.AUTH_RULES,
|
||||
"Enabled: " + AUTH.config().enabled,
|
||||
"Hybrid online-mode support: enabled"
|
||||
);
|
||||
@@ -568,7 +569,10 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
LobbyLog.state(
|
||||
"RELOAD START",
|
||||
"Config: " + ConfigPaths.CONFIG,
|
||||
"Portals: " + ConfigPaths.PORTALS
|
||||
"Portals: " + ConfigPaths.PORTALS,
|
||||
"Auth config: " + ConfigPaths.AUTH_CONFIG,
|
||||
"Auth rules: " + ConfigPaths.AUTH_RULES,
|
||||
"Auth accounts: " + ConfigPaths.AUTH_ACCOUNTS
|
||||
);
|
||||
|
||||
if (!CONFIG.load()) {
|
||||
@@ -592,8 +596,8 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
}
|
||||
|
||||
if (!AUTH.load()) {
|
||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi auth config/accounts bermasalah. Cek console."));
|
||||
LobbyLog.state("RELOAD FAILED", "Reason: auth config/accounts gagal dibaca");
|
||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi auth config/rules/accounts bermasalah. Cek console."));
|
||||
LobbyLog.state("RELOAD FAILED", "Reason: auth config/rules/accounts gagal dibaca");
|
||||
return 0;
|
||||
}
|
||||
AUTH.applyReload(server);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class RulesConfig {
|
||||
public boolean enabled = true;
|
||||
public int version = 1;
|
||||
public String title = "Server Rules";
|
||||
public String introduction = "Please read and accept our rules:";
|
||||
public List<String> rules = new ArrayList<>(List.of(
|
||||
"No cheating, hacking, or exploits.",
|
||||
"Be respectful to all players.",
|
||||
"No advertising or spam.",
|
||||
"Follow staff instructions.",
|
||||
"Have fun and play fair!"
|
||||
));
|
||||
public String footer = "You must accept these rules to continue.";
|
||||
public String agreementLabel = "I have read and agree to the server rules";
|
||||
public String acceptButton = "I Accept";
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"type": "minecraft:notice",
|
||||
"title": {"text": "Server Rules", "color": "yellow", "bold": true},
|
||||
"can_close_with_escape": false,
|
||||
"pause": false,
|
||||
"after_action": "close",
|
||||
"body": [
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Please read and accept our rules:", "color": "yellow"}},
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "1. No cheating, hacking, or exploits\n\n2. Be respectful to all players\n\n3. No advertising or spam\n\n4. Follow staff instructions\n\n5. Have fun and play fair!", "color": "white"}},
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Click confirm to accept and continue.", "color": "gray"}}
|
||||
],
|
||||
"inputs": [
|
||||
{"type": "minecraft:boolean", "key": "accepted", "label": {"text": "I have read and agree to the server rules"}, "initial": false, "on_true": "true", "on_false": "false"}
|
||||
],
|
||||
"action": {
|
||||
"label": {"text": "I Accept", "color": "green"},
|
||||
"action": {"type": "dynamic/run_command", "template": "lobbyauth rules $(accepted)"}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user