This commit is contained in:
Darto KLoning
2026-10-03 17:37:46 +07:00
parent 0cd06d418f
commit 73ca595a96
9 changed files with 577 additions and 491 deletions
@@ -11,5 +11,16 @@ public final class AuthConfig {
public int pbkdf2Iterations = 210000;
public String mojangProfileUrl = "https://api.minecraftservices.com/minecraft/profile/lookup/name/{username}";
public String legacyMojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}";
public boolean requireLoginEveryJoin = true;
/**
* false: premium players use Mojang authentication as their login, while local/cracked
* players must use their Lobby Systems password on every connection.
* true: every player, including premium players, must also register/use a Lobby Systems
* password after Mojang authentication.
*/
public boolean requireLoginEveryJoin = false;
/** Rules are accepted once per version. Increment to require acceptance again. */
public boolean rulesEnabled = true;
public int rulesVersion = 1;
}
@@ -4,8 +4,6 @@ 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.ClientboundSetSubtitleTextPacket;
import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
@@ -22,6 +20,7 @@ import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Locale;
@@ -33,6 +32,10 @@ public final class AuthManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final SecureRandom RANDOM = new SecureRandom();
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(4)).build();
private static final String DIALOG_RULES = "dartokloning:auth_rules";
private static final String DIALOG_REGISTER = "dartokloning:auth_register";
private static final String DIALOG_LOGIN = "dartokloning:auth_login";
private final Map<String, Account> accounts = new HashMap<>();
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
private final Map<String, PremiumCache> premiumCache = new ConcurrentHashMap<>();
@@ -46,13 +49,21 @@ public final class AuthManager {
AuthConfig c = GSON.fromJson(r, AuthConfig.class);
if (c != null) config = c;
}
} else saveConfig();
}
saveConfig(); // writes newly-added defaults/fields during upgrades
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());
if (loaded != null) { accounts.clear(); accounts.putAll(loaded); }
if (loaded != null) {
accounts.clear();
accounts.putAll(loaded);
}
}
} else saveAccounts();
} else {
saveAccounts();
}
migrateAccounts();
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to load Lobby Systems Auth", e);
@@ -62,7 +73,56 @@ public final class AuthManager {
public AuthConfig config() { return config; }
/** Called from the login mixin. */
/** Re-applies the freshly loaded auth configuration to players already online. */
public void applyReload(MinecraftServer server) {
premiumCache.clear();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
if (!config.enabled) {
Session existing = sessions.get(player.getUUID());
if (existing != null) {
existing.authenticated = true;
existing.stage = Stage.AUTHENTICATED;
clearDialog(player);
}
continue;
}
Session s = sessions.get(player.getUUID());
if (s == null) continue;
Account account = account(player.getName().getString());
// A rules-version bump is intentionally live: online players must accept
// the new version before continuing.
if (needsRules(account)) {
s.authenticated = false;
s.stage = Stage.RULES;
s.joinedAt = System.currentTimeMillis();
s.attempts = 0;
showDialog(player, DIALOG_RULES);
continue;
}
// false means premium accounts are satisfied by Mojang authentication.
if (s.premium && !config.requireLoginEveryJoin) {
authenticate(player);
continue;
}
// true makes the Lobby Systems password an additional layer for premium
// accounts, and local accounts always require it on each connection.
if (s.premium && config.requireLoginEveryJoin && s.authenticated) {
s.authenticated = false;
s.joinedAt = System.currentTimeMillis();
s.attempts = 0;
}
if (!s.authenticated) {
s.stage = isRegistered(player.getName().getString()) ? Stage.LOGIN : Stage.REGISTER;
showCurrentDialog(player, s);
}
}
}
/** Called from the login mixin before the player enters play state. */
public LoginRoute resolveLoginRoute(String username) {
if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM;
PremiumResult result = premiumStatus(username);
@@ -74,7 +134,7 @@ public final class AuthManager {
}
public PremiumResult premiumStatus(String username) {
String key = username.toLowerCase(Locale.ROOT);
String key = key(username);
PremiumCache cached = premiumCache.get(key);
long now = System.currentTimeMillis();
if (cached != null && now - cached.checkedAt < 86_400_000L) return cached.result;
@@ -111,20 +171,41 @@ public final class AuthManager {
if (!config.enabled) return;
String name = player.getName().getString();
boolean local = LobbySystemsMod.consumeLocalLogin(name);
if (!local) {
sessions.put(player.getUUID(), new Session(true, true, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
boolean premium = !local;
Session s = new Session(false, premium, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
sessions.put(player.getUUID(), s);
Account account = account(name);
if (premium) {
ensurePremiumMetadata(player, account);
account = account(name);
}
if (needsRules(account)) {
s.stage = Stage.RULES;
showDialog(player, DIALOG_RULES);
return;
}
Session s = new Session(false, false, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
sessions.put(player.getUUID(), s);
sendPrompt(player, true);
continueAfterRules(player);
}
public void onDisconnect(UUID uuid) { sessions.remove(uuid); }
public boolean isAuthenticated(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return !config.enabled || (s != null && s.authenticated); }
public boolean isPremium(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return s != null && s.premium; }
public boolean isRegistered(String username) { synchronized (accounts) { return accounts.containsKey(username.toLowerCase(Locale.ROOT)); } }
public boolean isAuthenticated(ServerPlayer player) {
Session s = sessions.get(player.getUUID());
return !config.enabled || (s != null && s.authenticated);
}
public boolean isPremium(ServerPlayer player) {
Session s = sessions.get(player.getUUID());
return s != null && s.premium;
}
public boolean isRegistered(String username) {
Account a = account(username);
return a != null && a.hasPassword();
}
public void tick(MinecraftServer server) {
if (!config.enabled) return;
@@ -136,84 +217,198 @@ public final class AuthManager {
player.connection.disconnect(Component.literal("Authentication timed out. Please reconnect and try again."));
continue;
}
// Server-authoritative freeze. This also prevents walking into Lobby portals before authentication.
// Keep the player at the safe Lobby authentication position. World safety is
// intentionally handled by the configured Lobby dimension itself.
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;
sendPrompt(player, false);
showCurrentDialog(player, s);
}
}
}
public int register(ServerPlayer player, String password, String confirm) {
if (!config.enabled) return fail(player, "§cAuthentication is disabled.");
/** Called by the rules dialog's command-template action. */
public int acceptRules(ServerPlayer player, boolean accepted) {
Session s = sessions.get(player.getUUID());
if (s != null && s.premium) return fail(player, "§ePremium accounts do not need local registration.");
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
String name = player.getName().getString();
PremiumResult premium = premiumStatus(name);
if (premium != PremiumResult.NOT_PREMIUM) return fail(player, premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.");
if (!password.equals(confirm)) return fail(player, "§cPasswords do not match.");
if (password.length() < config.minimumPasswordLength) return fail(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.");
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);
return 0;
}
synchronized (accounts) {
String key = name.toLowerCase(Locale.ROOT);
if (accounts.containsKey(key)) return fail(player, "§cThis local account is already registered. Use /login <password>.");
accounts.put(key, hash(password));
String name = player.getName().getString();
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.rulesAcceptedAt = Instant.now().toString();
saveAccounts();
}
authenticate(player, false);
continueAfterRules(player);
return 1;
}
public int register(ServerPlayer player, String password, String confirm) {
if (!config.enabled) return failAndReopen(player, "§cAuthentication is disabled.", DIALOG_REGISTER);
Session s = sessions.get(player.getUUID());
if (s == null || s.authenticated) return failAndReopen(player, "§eYou are already authenticated.", DIALOG_REGISTER);
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
if (s.premium && !config.requireLoginEveryJoin) return fail(player, "§ePremium accounts use Mojang authentication and do not need a local password.");
String name = player.getName().getString();
if (!s.premium) {
PremiumResult premium = premiumStatus(name);
if (premium != PremiumResult.NOT_PREMIUM) {
return failAndReopen(player,
premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.",
DIALOG_REGISTER);
}
}
if (!password.equals(confirm)) return failAndReopen(player, "§cPasswords do not match.", DIALOG_REGISTER);
if (password.length() < config.minimumPasswordLength) return failAndReopen(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.", DIALOG_REGISTER);
synchronized (accounts) {
String k = key(name);
Account existing = accounts.get(k);
if (existing != null && existing.hasPassword()) return failAndReopen(player, "§cThis account is already registered. Please sign in.", DIALOG_LOGIN);
Account hashed = hash(password);
if (existing != null) {
hashed.accountType = existing.accountType;
hashed.officialUuid = existing.officialUuid;
hashed.rulesVersionAccepted = existing.rulesVersionAccepted;
hashed.rulesAcceptedAt = existing.rulesAcceptedAt;
} else {
hashed.accountType = s.premium ? "PREMIUM" : "LOCAL";
hashed.officialUuid = s.premium ? player.getUUID().toString() : null;
}
accounts.put(k, hashed);
saveAccounts();
}
authenticate(player);
player.sendSystemMessage(Component.literal("§aRegistration successful. You are now logged in."));
return 1;
}
public int login(ServerPlayer player, String password) {
Session s = sessions.get(player.getUUID());
if (s != null && s.premium) return fail(player, "§ePremium account is already authenticated.");
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
Account account;
synchronized (accounts) { account = accounts.get(player.getName().getString().toLowerCase(Locale.ROOT)); }
if (account == null) return fail(player, "§cAccount not registered. Use /register <password> <password>.");
if (s == null || s.authenticated) return fail(player, "§eYou are already authenticated.");
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
Account account = account(player.getName().getString());
if (account == null || !account.hasPassword()) return failAndReopen(player, "§cAccount not registered. Please create a password first.", DIALOG_REGISTER);
if (!verify(password, account)) {
if (s != null && ++s.attempts >= config.maxLoginAttempts) {
if (++s.attempts >= config.maxLoginAttempts) {
player.connection.disconnect(Component.literal("Too many failed login attempts."));
return 0;
}
return fail(player, "§cIncorrect password.");
int remaining = Math.max(0, config.maxLoginAttempts - s.attempts);
return failAndReopen(player, "§cIncorrect password. §7" + remaining + " attempt(s) remaining.", DIALOG_LOGIN);
}
authenticate(player, false);
authenticate(player);
player.sendSystemMessage(Component.literal("§aLogin successful. Welcome back!"));
return 1;
}
private void authenticate(ServerPlayer player, boolean premium) {
Session old = sessions.get(player.getUUID());
sessions.put(player.getUUID(), new Session(true, premium, 0, old == null ? System.currentTimeMillis() : old.joinedAt,
player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal("§aAUTHENTICATED")));
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§fWelcome, " + player.getName().getString() + "!")));
private void continueAfterRules(ServerPlayer player) {
Session s = sessions.get(player.getUUID());
if (s == null) return;
if (s.premium && !config.requireLoginEveryJoin) {
authenticate(player);
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
return;
}
if (isRegistered(player.getName().getString())) {
s.stage = Stage.LOGIN;
showDialog(player, DIALOG_LOGIN);
} else {
s.stage = Stage.REGISTER;
showDialog(player, DIALOG_REGISTER);
}
}
private void sendPrompt(ServerPlayer player, boolean chat) {
boolean registered = isRegistered(player.getName().getString());
String title = registered ? "§eLOGIN REQUIRED" : "§bREGISTER REQUIRED";
String command = registered ? "/login <password>" : "/register <password> <password>";
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal(title)));
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§f" + command)));
if (chat) player.sendSystemMessage(Component.literal("§6[Lobby Systems Auth] §f" + (registered ? "Please login: §e" : "Please register: §b") + command));
private void authenticate(ServerPlayer player) {
Session s = sessions.get(player.getUUID());
if (s == null) return;
s.authenticated = true;
s.stage = Stage.AUTHENTICATED;
s.attempts = 0;
clearDialog(player);
}
private static int fail(ServerPlayer p, String msg) { p.sendSystemMessage(Component.literal(msg)); return 0; }
private boolean needsRules(Account account) {
return config.rulesEnabled && (account == null || account.rulesVersionAccepted < config.rulesVersion);
}
private void ensurePremiumMetadata(ServerPlayer player, Account existing) {
synchronized (accounts) {
String k = key(player.getName().getString());
Account a = existing;
if (a == null) {
a = Account.metadataOnly(true);
accounts.put(k, a);
}
a.accountType = "PREMIUM";
a.officialUuid = player.getUUID().toString();
saveAccounts();
}
}
private void showCurrentDialog(ServerPlayer player, Session s) {
switch (s.stage) {
case RULES -> showDialog(player, DIALOG_RULES);
case REGISTER -> showDialog(player, DIALOG_REGISTER);
case LOGIN -> showDialog(player, DIALOG_LOGIN);
default -> { }
}
}
private void showDialog(ServerPlayer player, String id) {
try {
String command = "dialog show " + player.getGameProfile().name() + " " + id;
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to show auth dialog {} to {}", id, player.getName().getString(), e);
player.sendSystemMessage(Component.literal("§cAuthentication UI could not be opened. Please reconnect or contact staff."));
}
}
private void clearDialog(ServerPlayer player) {
try {
String command = "dialog clear " + player.getGameProfile().name();
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
} catch (Exception e) {
LobbySystemsMod.LOGGER.debug("Failed to clear auth dialog for {}: {}", player.getName().getString(), e.toString());
}
}
private int failAndReopen(ServerPlayer player, String message, String dialog) {
player.sendSystemMessage(Component.literal(message));
showDialog(player, dialog);
return 0;
}
private static int fail(ServerPlayer p, String msg) {
p.sendSystemMessage(Component.literal(msg));
return 0;
}
private Account hash(String password) {
try {
byte[] salt = new byte[16]; RANDOM.nextBytes(salt);
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, config.pbkdf2Iterations, 256);
byte[] digest = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
spec.clearPassword();
return new Account(Base64.getEncoder().encodeToString(salt), Base64.getEncoder().encodeToString(digest), config.pbkdf2Iterations);
} catch (Exception e) { throw new IllegalStateException("Unable to hash password", e); }
} catch (Exception e) {
throw new IllegalStateException("Unable to hash password", e);
}
}
private boolean verify(String password, Account a) {
@@ -223,15 +418,98 @@ public final class AuthManager {
byte[] actual = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
spec.clearPassword();
return MessageDigest.isEqual(actual, Base64.getDecoder().decode(a.hash));
} catch (Exception e) { return false; }
} catch (Exception e) {
return false;
}
}
private void saveConfig() throws Exception { try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); } }
private void saveAccounts() { try { Files.createDirectories(ConfigPaths.AUTH_DIRECTORY); try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); } } catch (Exception e) { LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e); } }
private Account account(String username) {
synchronized (accounts) {
return accounts.get(key(username));
}
}
private void migrateAccounts() {
boolean changed = false;
synchronized (accounts) {
for (Account a : accounts.values()) {
if (a.accountType == null || a.accountType.isBlank()) {
a.accountType = "LOCAL";
changed = true;
}
}
if (changed) saveAccounts();
}
}
private static String key(String username) { return username.toLowerCase(Locale.ROOT); }
private void saveConfig() throws Exception {
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); }
}
private void saveAccounts() {
try {
Files.createDirectories(ConfigPaths.AUTH_DIRECTORY);
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); }
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e);
}
}
public enum PremiumResult { PREMIUM, NOT_PREMIUM, UNKNOWN }
public enum LoginRoute { PREMIUM, LOCAL, UNKNOWN }
private enum Stage { RULES, REGISTER, LOGIN, AUTHENTICATED }
private record PremiumCache(PremiumResult result, long checkedAt) {}
private static final class Account { String salt; String hash; int iterations; Account(String salt,String hash,int iterations){this.salt=salt;this.hash=hash;this.iterations=iterations;} }
private static final class Session { boolean authenticated; boolean premium; int attempts; long joinedAt; long lastReminder; double x,y,z; float yaw,pitch; Session(boolean a,boolean p,int at,long j,double x,double y,double z,float yaw,float pitch){authenticated=a;premium=p;attempts=at;joinedAt=j;lastReminder=0;this.x=x;this.y=y;this.z=z;this.yaw=yaw;this.pitch=pitch;} }
private static final class Account {
String salt;
String hash;
int iterations;
String accountType;
String officialUuid;
int rulesVersionAccepted;
String rulesAcceptedAt;
Account(String salt, String hash, int iterations) {
this.salt = salt;
this.hash = hash;
this.iterations = iterations;
}
static Account metadataOnly(boolean premium) {
Account a = new Account(null, null, 0);
a.accountType = premium ? "PREMIUM" : "LOCAL";
return a;
}
boolean hasPassword() {
return salt != null && !salt.isBlank() && hash != null && !hash.isBlank() && iterations > 0;
}
}
private static final class Session {
boolean authenticated;
final boolean premium;
int attempts;
long joinedAt;
long lastReminder;
final double x, y, z;
final float yaw, pitch;
Stage stage = Stage.RULES;
Session(boolean authenticated, boolean premium, int attempts, long joinedAt,
double x, double y, double z, float yaw, float pitch) {
this.authenticated = authenticated;
this.premium = premium;
this.attempts = attempts;
this.joinedAt = joinedAt;
this.lastReminder = 0;
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
}
}
}
@@ -247,19 +247,29 @@ public final class LobbySystemsMod implements ModInitializer {
ctx.getSource().getPlayerOrException(), ctx.getSource().getServer())));
dispatcher.register(Commands.literal("register")
.then(Commands.argument("password", StringArgumentType.word())
.then(Commands.argument("confirm", StringArgumentType.word())
.then(Commands.argument("password", StringArgumentType.string())
.then(Commands.argument("confirm", StringArgumentType.string())
.executes(ctx -> AUTH.register(
ctx.getSource().getPlayerOrException(),
StringArgumentType.getString(ctx, "password"),
StringArgumentType.getString(ctx, "confirm"))))));
dispatcher.register(Commands.literal("login")
.then(Commands.argument("password", StringArgumentType.word())
.then(Commands.argument("password", StringArgumentType.string())
.executes(ctx -> AUTH.login(
ctx.getSource().getPlayerOrException(),
StringArgumentType.getString(ctx, "password")))));
// Internal command used by the vanilla dialog command-template action.
// It intentionally has no operator requirement, but AuthManager only accepts it
// while the calling player is in the RULES authentication stage.
dispatcher.register(Commands.literal("lobbyauth")
.then(Commands.literal("rules")
.then(Commands.argument("accepted", com.mojang.brigadier.arguments.BoolArgumentType.bool())
.executes(ctx -> AUTH.acceptRules(
ctx.getSource().getPlayerOrException(),
com.mojang.brigadier.arguments.BoolArgumentType.getBool(ctx, "accepted"))))));
dispatcher.register(Commands.literal("ls")
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
.then(Commands.literal("reload")
@@ -581,6 +591,13 @@ public final class LobbySystemsMod implements ModInitializer {
return 0;
}
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");
return 0;
}
AUTH.applyReload(server);
POSITIONS.setExcludedDimension(key);
boolean portalOk = PORTALS.load();
if (!portalOk) {
@@ -0,0 +1,19 @@
{
"type": "minecraft:notice",
"title": {"text": "Welcome Back!", "color": "yellow", "bold": true},
"can_close_with_escape": false,
"pause": false,
"after_action": "close",
"body": [
{"type": "minecraft:plain_message", "contents": {"text": "Please enter your password to continue.", "color": "white"}},
{"type": "minecraft:plain_message", "contents": {"text": "If you forgot your password, contact staff.", "color": "gray"}}
],
"inputs": [
{"type": "minecraft:text", "key": "password", "label": {"text": "Password"}, "width": 300, "max_length": 128, "initial": ""}
],
"action": {
"label": {"text": "Sign In", "color": "green"},
"width": 300,
"action": {"type": "dynamic/run_command", "template": "login $(password)"}
}
}
@@ -0,0 +1,20 @@
{
"type": "minecraft:notice",
"title": {"text": "Create Account", "color": "aqua", "bold": true},
"can_close_with_escape": false,
"pause": false,
"after_action": "close",
"body": [
{"type": "minecraft:plain_message", "contents": {"text": "Welcome! Create a secure password below.", "color": "white"}},
{"type": "minecraft:plain_message", "contents": {"text": "Your password protects your progress.", "color": "gray"}}
],
"inputs": [
{"type": "minecraft:text", "key": "password", "label": {"text": "Password"}, "width": 300, "max_length": 128, "initial": ""},
{"type": "minecraft:text", "key": "confirm", "label": {"text": "Confirm Password"}, "width": 300, "max_length": 128, "initial": ""}
],
"action": {
"label": {"text": "Register", "color": "green"},
"width": 300,
"action": {"type": "dynamic/run_command", "template": "register $(password) $(confirm)"}
}
}
@@ -0,0 +1,19 @@
{
"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)"}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "dartokloning",
"version": "${version}",
"name": "Lobby Systems",
"description": "Server-side Fabric lobby routing, hybrid premium/cracked authentication, persistent world positions, portals, per-dimension gamemodes/gamerules, and starter kits.",
"description": "Server-side Fabric lobby routing with hybrid premium/cracked authentication, vanilla dialog rules/register/login UI, persistent world positions, portals, per-dimension gamemodes/gamerules, and starter kits.",
"authors": [
"DartoKloning"
],