Add dimension gamemode and read gamerule list

This commit is contained in:
Darto KLoning
2026-09-06 15:55:55 +07:00
parent db84a0e4dd
commit 5358bf9a8e
6 changed files with 429 additions and 12 deletions
@@ -9,6 +9,7 @@ public final class ConfigPaths {
public static final Path CONFIG = DIRECTORY.resolve("config.json");
public static final Path PLAYER_POSITIONS = DIRECTORY.resolve("player-positions.json");
public static final Path PORTALS = DIRECTORY.resolve("portals.json");
public static final Path DIMENSION_GAMEMODES = DIRECTORY.resolve("dimension-gamemodes.json");
private ConfigPaths() {}
}
@@ -0,0 +1,164 @@
package id.dartokloning.lobbysystems;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Mengelola gamemode yang diterapkan ketika player memasuki dimension tertentu.
* Minecraft menyimpan gamemode pada player, bukan pada dimension, jadi Lobby Systems
* menerapkan mapping dimension -> gamemode setiap kali perpindahan dimension terdeteksi.
*/
public final class DimensionGameModeManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Type MAP_TYPE = new TypeToken<LinkedHashMap<String, String>>() {}.getType();
private static final Path PATH = ConfigPaths.DIMENSION_GAMEMODES;
private final Map<String, String> configured = new LinkedHashMap<>();
private final Map<UUID, String> lastDimension = new ConcurrentHashMap<>();
public boolean load() {
if (!Files.exists(PATH)) {
save();
return true;
}
try (Reader reader = Files.newBufferedReader(PATH)) {
Map<String, String> loaded = GSON.fromJson(reader, MAP_TYPE);
configured.clear();
if (loaded != null) {
for (Map.Entry<String, String> entry : loaded.entrySet()) {
String dimension = normalizeDimension(entry.getKey());
String mode = normalizeMode(entry.getValue());
if (dimension.isBlank()) {
LobbySystemsMod.LOGGER.warn("Skipping blank dimension in {}", PATH);
continue;
}
if (parseMode(mode) == null) {
LobbySystemsMod.LOGGER.warn("Skipping invalid gamemode '{}' for dimension '{}' in {}", entry.getValue(), dimension, PATH);
continue;
}
configured.put(dimension, mode);
}
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to load {}. Keeping previous dimension gamemode config.", PATH, e);
return false;
}
}
public boolean save() {
try {
Files.createDirectories(PATH.getParent());
try (Writer writer = Files.newBufferedWriter(PATH)) {
GSON.toJson(configured, MAP_TYPE, writer);
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to save {}", PATH, e);
return false;
}
}
public void tick(MinecraftServer server) {
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
String dimension = player.level().dimension().identifier().toString();
String previous = lastDimension.put(player.getUUID(), dimension);
if (!dimension.equals(previous)) {
applyForPlayer(player, dimension, true);
}
}
}
public void onDisconnect(UUID uuid) {
lastDimension.remove(uuid);
}
public String get(String dimension) {
return configured.get(normalizeDimension(dimension));
}
public Map<String, String> all() {
return Map.copyOf(configured);
}
public boolean set(String dimension, String mode) {
String normalizedDimension = normalizeDimension(dimension);
String normalizedMode = normalizeMode(mode);
if (normalizedDimension.isBlank() || parseMode(normalizedMode) == null) return false;
configured.put(normalizedDimension, normalizedMode);
return save();
}
public boolean remove(String dimension) {
String key = normalizeDimension(dimension);
if (configured.remove(key) == null) return false;
save();
return true;
}
public int applyToDimension(MinecraftServer server, String dimension) {
int changed = 0;
String normalized = normalizeDimension(dimension);
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
if (player.level().dimension().identifier().toString().equals(normalized)) {
if (applyForPlayer(player, normalized, false)) changed++;
lastDimension.put(player.getUUID(), normalized);
}
}
return changed;
}
private boolean applyForPlayer(ServerPlayer player, String dimension, boolean log) {
String configuredMode = configured.get(normalizeDimension(dimension));
if (configuredMode == null) return false;
GameType gameType = parseMode(configuredMode);
if (gameType == null) return false;
boolean changed = player.setGameMode(gameType);
if (log && changed) {
LobbyLog.state(
"DIMENSION GAMEMODE APPLIED",
"Player: " + player.getName().getString(),
"Dimension: " + dimension,
"Gamemode: " + configuredMode
);
}
return changed;
}
public static GameType parseMode(String raw) {
if (raw == null) return null;
return switch (raw.trim().toLowerCase()) {
case "survival" -> GameType.SURVIVAL;
case "creative" -> GameType.CREATIVE;
case "adventure" -> GameType.ADVENTURE;
case "spectator" -> GameType.SPECTATOR;
default -> null;
};
}
private static String normalizeMode(String raw) {
return raw == null ? "" : raw.trim().toLowerCase();
}
private static String normalizeDimension(String raw) {
return raw == null ? "" : raw.trim().toLowerCase();
}
}
@@ -1,5 +1,6 @@
package id.dartokloning.lobbysystems;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
@@ -9,6 +10,7 @@ import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
@@ -17,9 +19,15 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.gamerules.GameRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public final class LobbySystemsMod implements ModInitializer {
public static final String MOD_ID = "dartokloning";
public static final Logger LOGGER = LoggerFactory.getLogger("Lobby Systems");
@@ -29,6 +37,7 @@ public final class LobbySystemsMod implements ModInitializer {
private static final PositionManager POSITIONS = new PositionManager();
private static final PortalManager PORTALS = new PortalManager();
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
private static int saveTicker;
@Override
@@ -59,6 +68,13 @@ public final class LobbySystemsMod implements ModInitializer {
"Portals: " + PORTALS.size()
);
boolean gameModesLoaded = DIMENSION_GAMEMODES.load();
LobbyLog.state(
gameModesLoaded ? "DIMENSION GAMEMODES LOADED" : "DIMENSION GAMEMODES LOAD FAILED",
"File: " + ConfigPaths.DIMENSION_GAMEMODES,
"Configured dimensions: " + DIMENSION_GAMEMODES.all().size()
);
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
ServerPlayer player = listener.getPlayer();
server.execute(() -> {
@@ -79,6 +95,7 @@ public final class LobbySystemsMod implements ModInitializer {
SESSIONS.onDisconnect(player.getUUID());
TELEPORTS.onDisconnect(player.getUUID());
PORTALS.onDisconnect(player.getUUID());
DIMENSION_GAMEMODES.onDisconnect(player.getUUID());
LobbyLog.state(
"PLAYER DISCONNECT",
"Player: " + player.getName().getString(),
@@ -94,6 +111,8 @@ public final class LobbySystemsMod implements ModInitializer {
}
});
DIMENSION_GAMEMODES.tick(server);
ResourceKey<Level> lobby = lobbyKey();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
POSITIONS.update(player);
@@ -128,6 +147,50 @@ public final class LobbySystemsMod implements ModInitializer {
StringArgumentType.getString(ctx, "dimension"),
true,
true))))
.then(Commands.literal("gamerule")
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> listGameRules(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "dimension"),
1))
.then(Commands.literal("--page")
.then(Commands.argument("page", IntegerArgumentType.integer(1))
.executes(ctx -> listGameRules(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "dimension"),
IntegerArgumentType.getInteger(ctx, "page")))))))
.then(Commands.literal("gamemode")
.then(Commands.literal("list")
.executes(ctx -> listDimensionGameModes(ctx.getSource())))
.then(Commands.literal("set")
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(
new String[]{"survival", "creative", "adventure", "spectator"}, builder))
.executes(ctx -> setDimensionGameMode(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "dimension"),
StringArgumentType.getString(ctx, "mode"))))))
.then(Commands.literal("remove")
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> removeDimensionGameMode(
ctx.getSource(),
StringArgumentType.getString(ctx, "dimension")))))
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> showDimensionGameMode(
ctx.getSource(),
StringArgumentType.getString(ctx, "dimension")))))
.then(Commands.literal("portal")
.then(Commands.literal("create")
.then(Commands.argument("name", StringArgumentType.word())
@@ -226,6 +289,16 @@ public final class LobbySystemsMod implements ModInitializer {
return 0;
}
boolean gameModesOk = DIMENSION_GAMEMODES.load();
if (!gameModesOk) {
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension-gamemodes.json bermasalah. Cek console."));
LobbyLog.state("RELOAD FAILED", "Reason: dimension-gamemodes.json gagal dibaca");
return 0;
}
for (String dimension : DIMENSION_GAMEMODES.all().keySet()) {
DIMENSION_GAMEMODES.applyToDimension(server, dimension);
}
if (source.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(Component.literal(
"§aLobby Systems direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
@@ -237,11 +310,137 @@ public final class LobbySystemsMod implements ModInitializer {
"Lobby: " + CONFIG.get().lobbyDimension,
"Minimum play: " + CONFIG.get().minimumPlaySeconds + "s",
"Portals: " + PORTALS.size(),
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size(),
"Player positions lobby exclusion: active"
);
return 1;
}
private static void sendCommandMessage(CommandSourceStack source, Component message) {
if (source.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(message);
} else {
source.sendSuccess(() -> message, false);
}
}
private static final int GAMERULES_PER_PAGE = 10;
private static int listGameRules(CommandSourceStack source, MinecraftServer server, String dimensionId, int requestedPage) {
ServerLevel level = findLevel(server, dimensionId);
if (level == null) {
source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
return 0;
}
List<GameRuleEntry> entries = new ArrayList<>();
for (GameRule<?> rule : BuiltInRegistries.GAME_RULE) {
Identifier id = BuiltInRegistries.GAME_RULE.getKey(rule);
if (id == null) continue;
entries.add(new GameRuleEntry(id.toString(), gameRuleValue(level, rule)));
}
entries.sort(Comparator.comparing(GameRuleEntry::name));
int totalPages = Math.max(1, (entries.size() + GAMERULES_PER_PAGE - 1) / GAMERULES_PER_PAGE);
int page = Math.min(Math.max(1, requestedPage), totalPages);
int start = (page - 1) * GAMERULES_PER_PAGE;
int end = Math.min(start + GAMERULES_PER_PAGE, entries.size());
sendCommandMessage(source, Component.literal(
"§6§lLobby Systems Gamerules §r§7[§f" + page + "§7/§f" + totalPages + "§7]"
+ "\n§7Dimension: §f" + level.dimension().identifier()
+ " §8| §7Rules: §f" + entries.size()));
for (int i = start; i < end; i++) {
GameRuleEntry entry = entries.get(i);
String valueColor = "true".equals(entry.value()) ? "§a" : "false".equals(entry.value()) ? "§c" : "§e";
sendCommandMessage(source, Component.literal("§8• §f" + entry.name() + " §7= " + valueColor + entry.value()));
}
if (page < totalPages) {
int next = page + 1;
sendCommandMessage(source, Component.literal(
"§7Halaman selanjutnya: §f/ls gamerule " + level.dimension().identifier() + " --page " + next));
}
return entries.size();
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static String gameRuleValue(ServerLevel level, GameRule<?> rule) {
Object value = level.getGameRules().get((GameRule) rule);
return String.valueOf(value);
}
private static int showDimensionGameMode(CommandSourceStack source, String dimensionId) {
String mode = DIMENSION_GAMEMODES.get(dimensionId);
if (mode == null) {
sendCommandMessage(source, Component.literal(
"§6Lobby Systems Gamemode\n§7Dimension: §f" + dimensionId
+ "\n§7Configured: §e<none>"
+ "\n§8Player akan mempertahankan gamemode sebelumnya sampai mapping dibuat."));
return 1;
}
sendCommandMessage(source, Component.literal(
"§6Lobby Systems Gamemode\n§7Dimension: §f" + dimensionId + "\n§7Gamemode: §a" + mode));
return 1;
}
private static int listDimensionGameModes(CommandSourceStack source) {
Map<String, String> all = DIMENSION_GAMEMODES.all();
if (all.isEmpty()) {
sendCommandMessage(source, Component.literal(
"§eBelum ada gamemode dimension yang dikonfigurasi.\n§7Gunakan §f/ls gamemode set <dimension> <mode>"));
return 1;
}
sendCommandMessage(source, Component.literal("§6§lLobby Systems Dimension Gamemodes §r§7(" + all.size() + ")"));
all.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry ->
sendCommandMessage(source, Component.literal("§8• §f" + entry.getKey() + " §7= §a" + entry.getValue())));
return all.size();
}
private static int setDimensionGameMode(CommandSourceStack source, MinecraftServer server, String dimensionId, String rawMode) {
ServerLevel level = findLevel(server, dimensionId);
if (level == null) {
source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
return 0;
}
if (DimensionGameModeManager.parseMode(rawMode) == null) {
source.sendFailure(Component.literal("§cGamemode tidak valid. Pilih survival, creative, adventure, atau spectator."));
return 0;
}
String normalizedDimension = level.dimension().identifier().toString();
String normalizedMode = rawMode.trim().toLowerCase();
if (!DIMENSION_GAMEMODES.set(normalizedDimension, normalizedMode)) {
source.sendFailure(Component.literal("§cGagal menyimpan dimension-gamemodes.json. Cek console."));
return 0;
}
int applied = DIMENSION_GAMEMODES.applyToDimension(server, normalizedDimension);
sendCommandMessage(source, Component.literal(
"§aGamemode dimension disimpan. §7" + normalizedDimension + " §8→ §f" + normalizedMode
+ " §7| Player diterapkan sekarang: §f" + applied));
LobbyLog.state(
"DIMENSION GAMEMODE UPDATED",
"Dimension: " + normalizedDimension,
"Gamemode: " + normalizedMode,
"Players applied now: " + applied
);
return 1;
}
private static int removeDimensionGameMode(CommandSourceStack source, String dimensionId) {
if (!DIMENSION_GAMEMODES.remove(dimensionId)) {
source.sendFailure(Component.literal("§cTidak ada mapping gamemode untuk dimension '" + dimensionId + "'."));
return 0;
}
sendCommandMessage(source, Component.literal(
"§aMapping gamemode dimension dihapus: §f" + dimensionId
+ "\n§7Player tidak diubah sampai berpindah dimension atau mapping baru dibuat."));
LobbyLog.state("DIMENSION GAMEMODE REMOVED", "Dimension: " + dimensionId);
return 1;
}
private record GameRuleEntry(String name, String value) {}
private static int createPortal(ServerPlayer player, MinecraftServer server, String rawName, String rawTarget) {
String name = PortalManager.normalizeName(rawName);
if (!name.matches("[a-z0-9_-]+")) {
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "dartokloning",
"version": "${version}",
"name": "Lobby Systems",
"description": "Server-side Fabric lobby routing, persistent world positions, minimum playtime, and WorldEdit-selected dimension portals.",
"description": "Server-side Fabric lobby routing, persistent world positions, minimum playtime, portals, gamerule inspection, and per-dimension player gamemodes.",
"authors": ["DartoKloning"],
"license": "MIT",
"environment": "server",