diff --git a/README.md b/README.md index 8faf2ed..7cf512c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lobby Systems v1.2.2 — Fabric 26.2 +# Lobby Systems v1.3.0 — Fabric 26.2 -Server-side lobby routing untuk Minecraft 26.2/Fabric. Versi 1.2.2 menambahkan portal antar-dimensi berbasis WorldEdit selection, last-position per player/per dimension, minimum playtime sebelum kembali ke lobby, dan konfigurasi terpusat di `config/lobby/`. +Server-side lobby routing untuk Minecraft 26.2/Fabric. Versi 1.3.0 menambahkan viewer gamerule per-dimension dengan pagination dan pengelolaan gamemode player berdasarkan dimension, sambil mempertahankan portal antar-dimensi, last-position, minimum playtime, dan konfigurasi terpusat di `config/lobby/`. -### Reload feedback v1.2.2 +### Reload feedback `/ls reload` menulis banner `RELOAD START` dan `RELOAD COMPLETE` hanya ke console server. Ringkasan reload satu baris dikirim langsung hanya ke player admin/operator yang menjalankan command; ringkasan tersebut tidak dibroadcast ke player lain dan tidak dikirim melalui command feedback ke console. @@ -38,7 +38,7 @@ chmod +x compile.sh release.sh ```text ============================================ Lobby Systems - Fabric 26.2 Build - Version: 1.2.2 + Version: 1.3.0 Namespace: id.dartokloning.lobbysystems Mod ID: dartokloning ============================================ @@ -47,7 +47,7 @@ chmod +x compile.sh release.sh Output utama: ```text -build/libs/lobby-systems-1.2.2.jar +build/libs/lobby-systems-1.3.0.jar ``` `*-sources.jar` hanya source archive dan tidak perlu dipasang ke server. @@ -78,21 +78,22 @@ State penting Lobby Systems memakai separator agar mudah terlihat di server cons ```text ============================================ Lobby Systems - READY - Version: 1.2.2 + Version: 1.3.0 Minecraft: 26.2 Config directory: config/lobby WorldEdit: available ============================================ ``` -## Struktur konfigurasi v1.2.2 +## Struktur konfigurasi v1.3.0 ```text config/ └── lobby/ ├── config.json ├── player-positions.json - └── portals.json + ├── portals.json + └── dimension-gamemodes.json ``` ### `config/lobby/config.json` @@ -151,9 +152,26 @@ Portal dibuat lewat command dan tersimpan otomatis. Contoh: Portal adalah region/cuboid, bukan harus Nether Portal asli. Anda bebas membuat bentuk doorway, arch, lantai, gateway, dan sebagainya di dalam region tersebut. + +### `config/lobby/dimension-gamemodes.json` + +Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems v1.3.0 menambahkan mapping dimension → gamemode dan menerapkannya ketika player masuk ke dimension tersebut. File ini dibuat otomatis dan dapat dikelola lewat command admin. + +Contoh: + +```json +{ + "minecraft:lobby": "adventure", + "minecraft:worldcreative": "creative", + "minecraft:overworld": "survival" +} +``` + +Gamemode yang didukung: `survival`, `creative`, `adventure`, dan `spectator`. + ## Migrasi dari v1.1.x -Pada startup pertama v1.2.2: +Pada startup pertama v1.3.0: ```text config/lobby-systems.json @@ -177,7 +195,7 @@ akan dimigrasikan ke: config/lobby/player-positions.json ``` -Nama file lama hanya digunakan oleh `LegacyConfigMigrator` untuk proses migrasi. Runtime v1.2.2 membaca dan menulis file baru di `config/lobby/`. +Nama file lama hanya digunakan oleh `LegacyConfigMigrator` untuk proses migrasi. Runtime v1.3.0 membaca dan menulis file baru di `config/lobby/`. Jika file tujuan baru sudah ada, migrator tidak menimpanya. @@ -198,6 +216,14 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya. /ls reload /ls tp +/ls gamerule +/ls gamerule --page + +/ls gamemode +/ls gamemode list +/ls gamemode set +/ls gamemode remove + /ls portal create /ls portal delete /ls portal list @@ -207,12 +233,39 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya. `/ls tp` tetap command maintenance/admin. Player biasa tidak memerlukannya karena flow normal menggunakan portal. + +### Gamerule viewer per dimension + +`/ls gamerule ` menampilkan gamerule aktual dari dimension yang sedang ter-load. Viewer ini bersifat read-only dan cocok dipakai bersama WorldGameRules. Hasil ditampilkan 10 rule per halaman. + +Contoh: + +```text +/ls gamerule minecraft:lobby +/ls gamerule minecraft:lobby --page 2 +``` + +Header chat menampilkan `halaman/total halaman`, nama dimension, dan jumlah gamerule. Jika masih ada halaman berikutnya, command untuk membuka halaman selanjutnya ikut ditampilkan. + +### Gamemode per dimension + +Gunakan mapping gamemode agar player otomatis mendapat gamemode yang sesuai saat memasuki dimension. Contoh: + +```text +/ls gamemode set minecraft:lobby adventure +/ls gamemode set minecraft:worldcreative creative +/ls gamemode set minecraft:overworld survival +``` + +`/ls gamemode ` melihat mapping satu dimension, `/ls gamemode list` melihat semua mapping, dan `/ls gamemode remove ` menghapus mapping. Mapping disimpan ke `config/lobby/dimension-gamemodes.json` dan ikut direload oleh `/ls reload`. + ### `/ls reload` Reload live: - `config/lobby/config.json` - `config/lobby/portals.json` +- `config/lobby/dimension-gamemodes.json` - lobby exclusion pada player-position store Tidak perlu restart untuk perubahan JSON tersebut. diff --git a/gradle.properties b/gradle.properties index d1192b1..a0e2ec1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,6 @@ minecraft_version=26.2 loader_version=0.19.3 loom_version=1.17-SNAPSHOT fabric_api_version=0.156.0+26.2 -mod_version=1.2.2 +mod_version=1.3.0 maven_group=id.dartokloning archives_base_name=lobby-systems diff --git a/src/main/java/id/dartokloning/lobbysystems/ConfigPaths.java b/src/main/java/id/dartokloning/lobbysystems/ConfigPaths.java index 7302d3b..e3f5e0f 100644 --- a/src/main/java/id/dartokloning/lobbysystems/ConfigPaths.java +++ b/src/main/java/id/dartokloning/lobbysystems/ConfigPaths.java @@ -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() {} } diff --git a/src/main/java/id/dartokloning/lobbysystems/DimensionGameModeManager.java b/src/main/java/id/dartokloning/lobbysystems/DimensionGameModeManager.java new file mode 100644 index 0000000..066a6d4 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/DimensionGameModeManager.java @@ -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>() {}.getType(); + private static final Path PATH = ConfigPaths.DIMENSION_GAMEMODES; + + private final Map configured = new LinkedHashMap<>(); + private final Map lastDimension = new ConcurrentHashMap<>(); + + public boolean load() { + if (!Files.exists(PATH)) { + save(); + return true; + } + + try (Reader reader = Files.newBufferedReader(PATH)) { + Map loaded = GSON.fromJson(reader, MAP_TYPE); + configured.clear(); + if (loaded != null) { + for (Map.Entry 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 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(); + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java index 21fdfe6..31b3641 100644 --- a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java +++ b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java @@ -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 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 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" + + "\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 all = DIMENSION_GAMEMODES.all(); + if (all.isEmpty()) { + sendCommandMessage(source, Component.literal( + "§eBelum ada gamemode dimension yang dikonfigurasi.\n§7Gunakan §f/ls gamemode set ")); + 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_-]+")) { diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6f7facf..42dacd0 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -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",