From c4f4bd971a7e5841f1c445ee3d780a55539e3a23 Mon Sep 17 00:00:00 2001 From: Darto KLoning Date: Sun, 6 Sep 2026 21:00:59 +0700 Subject: [PATCH] Bugfix compatibility --- README.md | 24 ++--- gradle.properties | 2 +- .../lobbysystems/LobbyConfig.java | 3 + .../lobbysystems/LobbySystemsMod.java | 92 +++++++++++++------ .../WorldEditSelectionBridge.java | 36 ++++++-- 5 files changed, 112 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 0066a16..61234c7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lobby Systems v1.4.0 — Fabric 26.2 +# Lobby Systems v1.4.1 — Fabric 26.2 -Server-side lobby routing untuk Minecraft 26.2/Fabric. v1.4.0 menambahkan **native persistent per-dimension gamerules** milik Lobby Systems, tetap mempertahankan portal antar-dimensi, last-position, minimum playtime, gamerule viewer ber-pagination, serta per-dimension player gamemode. +Server-side lobby routing untuk Minecraft 26.2/Fabric. v1.4.1 mempertahankan native persistent per-dimension gamerules dan menambahkan compatibility control untuk dimension gamemode serta perbaikan WorldEdit 7.4.x portal selection. -**v1.4.0 feature release:** gamerule tidak lagi bergantung pada WorldGameRules. Lobby Systems membuat instance `GameRules` terpisah untuk setiap `ServerLevel`, menyimpan override ke `config/lobby/dimension-gamerules.json`, lalu menerapkannya kembali saat world load, server start, dan `/ls reload`. WorldGameRules harus dihapus karena kedua mod mengubah mekanisme gamerule per-world. +**v1.4.1 compatibility/bugfix release:** `manageDimensionGamemode` (default `true`) menentukan apakah Lobby Systems menjadi source of truth gamemode per dimension. Jika Dimensional Inventories terdeteksi saat opsi ini aktif, console menampilkan compatibility warning agar konfigurasi gamemode kedua mod disamakan. Integrasi WorldEdit juga diperbarui untuk API Fabric WorldEdit 7.4.x (`FabricAdapter.get().fromNativePlayer(...)`). ### Reload feedback @@ -40,7 +40,7 @@ chmod +x compile.sh release.sh ```text ============================================ Lobby Systems - Fabric 26.2 Build - Version: 1.4.0 + Version: 1.4.1 Namespace: id.dartokloning.lobbysystems Mod ID: dartokloning ============================================ @@ -49,7 +49,7 @@ chmod +x compile.sh release.sh Output utama: ```text -build/libs/lobby-systems-1.4.0.jar +build/libs/lobby-systems-1.4.1.jar ``` `*-sources.jar` hanya source archive dan tidak perlu dipasang ke server. @@ -80,14 +80,14 @@ State penting Lobby Systems memakai separator agar mudah terlihat di server cons ```text ============================================ Lobby Systems - READY - Version: 1.4.0 + Version: 1.4.1 Minecraft: 26.2 Config directory: config/lobby WorldEdit: available ============================================ ``` -## Struktur konfigurasi v1.4.0 +## Struktur konfigurasi v1.4.1 ```text config/ @@ -112,6 +112,7 @@ Contoh: "lobbyYaw": 0.0, "lobbyPitch": 0.0, "minimumPlaySeconds": 300, + "manageDimensionGamemode": true, "portalCheckIntervalTicks": 5, "portalCooldownTicks": 60, "inLobbyMessage": "§eKamu sudah berada di lobby.", @@ -158,7 +159,7 @@ Portal adalah region/cuboid, bukan harus Nether Portal asli. Anda bebas membuat ### `config/lobby/dimension-gamemodes.json` -Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems v1.4.0 menambahkan mapping dimension → gamemode dan menerapkannya ketika player masuk ke dimension tersebut. File ini dibuat otomatis dan dapat dikelola lewat command admin. +Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems menyimpan mapping dimension → gamemode di file ini. Jika `manageDimensionGamemode=true` (default), mapping diterapkan ketika player memasuki dimension. Jika `false`, Lobby Systems tidak mengubah gamemode player dan command gamemode menjadi viewer-only. Contoh: @@ -172,6 +173,8 @@ Contoh: Gamemode yang didukung: `survival`, `creative`, `adventure`, dan `spectator`. +Jika mod **Dimensional Inventories** (`dimensional-inventories`) terpasang dan `manageDimensionGamemode=true`, Lobby Systems akan menampilkan warning di console karena kedua mod dapat mengubah gamemode player saat perpindahan dimension/pool. Agar transisi konsisten, samakan gamemode dimension/pool pada kedua mod. Jika Anda ingin Dimensional Inventories menjadi pengelola utama gamemode, set `manageDimensionGamemode=false`. + ## Migrasi dari v1.1.x Pada startup pertama v1.4.0: @@ -224,7 +227,6 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya. /ls gamerule set /ls gamerule reset -/ls gamemode /ls gamemode list /ls gamemode set /ls gamemode remove @@ -280,7 +282,7 @@ Gunakan mapping gamemode agar player otomatis mendapat gamemode yang sesuai saat /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 gamemode list` menampilkan semua mapping. Saat `manageDimensionGamemode=true`, subcommand `set` dan `remove` tersedia dan autocomplete dimension hanya muncul setelah kedua subcommand tersebut. Saat `manageDimensionGamemode=false`, command tree hanya menampilkan `/ls gamemode list`. Mapping tetap disimpan di `config/lobby/dimension-gamemodes.json` dan ikut direload oleh `/ls reload`. ### `/ls reload` @@ -322,7 +324,7 @@ Contoh lain: Nama portal hanya menerima huruf kecil `a-z`, angka `0-9`, `_`, dan `-`. -WorldEdit hanya dibutuhkan saat membaca selection untuk **membuat** portal. Setelah `portals.json` tersimpan, portal runtime dikelola Lobby Systems sendiri. Jika WorldEdit dilepas kemudian, portal yang sudah ada tetap dapat bekerja. +WorldEdit hanya dibutuhkan saat membaca selection untuk **membuat** portal. v1.4.1 memakai bridge WorldEdit Fabric 7.4.x melalui `FabricAdapter.get().fromNativePlayer(...)` dengan fallback API lama. Setelah `portals.json` tersimpan, portal runtime dikelola Lobby Systems sendiri. Jika WorldEdit dilepas kemudian, portal yang sudah ada tetap dapat bekerja. ### Menghapus dan mengecek portal diff --git a/gradle.properties b/gradle.properties index 53cbb36..bd2d1cb 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.4.0 +mod_version=1.4.1 maven_group=id.dartokloning archives_base_name=lobby-systems diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java b/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java index cb3b976..0824f27 100644 --- a/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java +++ b/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java @@ -9,6 +9,9 @@ public final class LobbyConfig { public float lobbyPitch = 0.0f; public int minimumPlaySeconds = 300; + /** Jika true, Lobby Systems menjadi source of truth gamemode per dimension. */ + public boolean manageDimensionGamemode = true; + /** Seberapa sering region portal diperiksa. 5 tick = 4 kali/detik. */ public int portalCheckIntervalTicks = 5; /** Cooldown setelah portal terpicu. 60 tick = 3 detik. */ diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java index 733c993..50cea48 100644 --- a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java +++ b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java @@ -3,6 +3,7 @@ package id.dartokloning.lobbysystems; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import net.fabricmc.api.ModInitializer; +import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; @@ -79,6 +80,8 @@ public final class LobbySystemsMod implements ModInitializer { "Configured dimensions: " + DIMENSION_GAMEMODES.all().size() ); + logDimensionInventoryCompatibility(); + boolean gameRulesLoaded = DIMENSION_GAMERULES.load(); LobbyLog.state( gameRulesLoaded ? "DIMENSION GAMERULES LOADED" : "DIMENSION GAMERULES LOAD FAILED", @@ -145,7 +148,9 @@ public final class LobbySystemsMod implements ModInitializer { } }); - DIMENSION_GAMEMODES.tick(server); + if (CONFIG.get().manageDimensionGamemode) { + DIMENSION_GAMEMODES.tick(server); + } ResourceKey lobby = lobbyKey(); for (ServerPlayer player : server.getPlayerList().getPlayers()) { @@ -229,6 +234,7 @@ public final class LobbySystemsMod implements ModInitializer { .then(Commands.literal("list") .executes(ctx -> listDimensionGameModes(ctx.getSource()))) .then(Commands.literal("set") + .requires(source -> CONFIG.get().manageDimensionGamemode) .then(Commands.argument("dimension", IdentifierArgument.id()) .suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource( ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder)) @@ -241,18 +247,13 @@ public final class LobbySystemsMod implements ModInitializer { IdentifierArgument.getId(ctx, "dimension").toString(), StringArgumentType.getString(ctx, "mode")))))) .then(Commands.literal("remove") + .requires(source -> CONFIG.get().manageDimensionGamemode) .then(Commands.argument("dimension", IdentifierArgument.id()) .suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource( ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder)) .executes(ctx -> removeDimensionGameMode( ctx.getSource(), - IdentifierArgument.getId(ctx, "dimension").toString())))) - .then(Commands.argument("dimension", IdentifierArgument.id()) - .suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource( - ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder)) - .executes(ctx -> showDimensionGameMode( - ctx.getSource(), - IdentifierArgument.getId(ctx, "dimension").toString())))) + IdentifierArgument.getId(ctx, "dimension").toString()))))) .then(Commands.literal("portal") .then(Commands.literal("create") .then(Commands.argument("name", StringArgumentType.word()) @@ -357,9 +358,13 @@ public final class LobbySystemsMod implements ModInitializer { 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 (CONFIG.get().manageDimensionGamemode) { + for (String dimension : DIMENSION_GAMEMODES.all().keySet()) { + DIMENSION_GAMEMODES.applyToDimension(server, dimension); + } } + logDimensionInventoryCompatibility(); + refreshCommandTrees(server); boolean gameRulesOk = DIMENSION_GAMERULES.load(); if (!gameRulesOk) { @@ -380,7 +385,8 @@ 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(), + "Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size() + + " (management " + (CONFIG.get().manageDimensionGamemode ? "enabled" : "disabled") + ")", "Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied", "Player positions lobby exclusion: active" ); @@ -570,34 +576,29 @@ public final class LobbySystemsMod implements ModInitializer { return rule.serialize(level.getGameRules().get(rule)); } - 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()) { + String hint = CONFIG.get().manageDimensionGamemode + ? "\n§7Gunakan §f/ls gamemode set " + : "\n§7Management gamemode sedang §cdisabled§7; mode viewer-only aktif."; sendCommandMessage(source, Component.literal( - "§eBelum ada gamemode dimension yang dikonfigurasi.\n§7Gunakan §f/ls gamemode set ")); + "§eBelum ada gamemode dimension yang dikonfigurasi." + hint)); return 1; } - sendCommandMessage(source, Component.literal("§6§lLobby Systems Dimension Gamemodes §r§7(" + all.size() + ")")); + sendCommandMessage(source, Component.literal( + "§6§lLobby Systems Dimension Gamemodes §r§7(" + all.size() + ")" + + (CONFIG.get().manageDimensionGamemode ? " §8[managed]" : " §8[viewer-only]"))); 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) { + if (!CONFIG.get().manageDimensionGamemode) { + source.sendFailure(Component.literal("§cDimension gamemode management sedang disabled di config.json.")); + return 0; + } ServerLevel level = findLevel(server, dimensionId); if (level == null) { source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load.")); @@ -627,6 +628,10 @@ public final class LobbySystemsMod implements ModInitializer { } private static int removeDimensionGameMode(CommandSourceStack source, String dimensionId) { + if (!CONFIG.get().manageDimensionGamemode) { + source.sendFailure(Component.literal("§cDimension gamemode management sedang disabled di config.json.")); + return 0; + } if (!DIMENSION_GAMEMODES.remove(dimensionId)) { source.sendFailure(Component.literal("§cTidak ada mapping gamemode untuk dimension '" + dimensionId + "'.")); return 0; @@ -638,6 +643,39 @@ public final class LobbySystemsMod implements ModInitializer { return 1; } + private static void logDimensionInventoryCompatibility() { + if (!FabricLoader.getInstance().isModLoaded("dimensional-inventories")) { + return; + } + + if (CONFIG.get().manageDimensionGamemode) { + LobbyLog.state( + "COMPATIBILITY WARNING", + "Dimensional Inventories: detected", + "manageDimensionGamemode: true", + "Both mods can apply player gamemode on dimension/pool changes.", + "Keep matching gamemodes in Lobby Systems and Dimensional Inventories to avoid conflicting transitions." + ); + } else { + LobbyLog.state( + "DIMENSION GAMEMODE COMPATIBILITY", + "Dimensional Inventories: detected", + "manageDimensionGamemode: false", + "Lobby Systems gamemode commands are viewer-only; Dimensional Inventories may manage player gamemode." + ); + } + } + + private static void refreshCommandTrees(MinecraftServer server) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + try { + server.getCommands().sendCommands(player); + } catch (RuntimeException e) { + LOGGER.warn("Failed to refresh command tree for {} after config reload.", player.getGameProfile().name(), e); + } + } + } + private record GameRuleEntry(String name, String value) {} private static int createPortal(ServerPlayer player, MinecraftServer server, String rawName, String rawTarget) { diff --git a/src/main/java/id/dartokloning/lobbysystems/WorldEditSelectionBridge.java b/src/main/java/id/dartokloning/lobbysystems/WorldEditSelectionBridge.java index 60dec65..e6eefac 100644 --- a/src/main/java/id/dartokloning/lobbysystems/WorldEditSelectionBridge.java +++ b/src/main/java/id/dartokloning/lobbysystems/WorldEditSelectionBridge.java @@ -9,6 +9,9 @@ import java.lang.reflect.Modifier; /** * Soft integration dengan WorldEdit. Reflection sengaja dipakai agar Lobby Systems * tetap dapat dijalankan tanpa WorldEdit; portal yang sudah tersimpan tetap bekerja. + * + * Mendukung WorldEdit Fabric 7.4.x (FabricAdapter.get().fromNativePlayer(...)) + * sekaligus fallback API lama agar bridge tidak rapuh saat server memakai build berbeda. */ public final class WorldEditSelectionBridge { private WorldEditSelectionBridge() {} @@ -23,13 +26,10 @@ public final class WorldEditSelectionBridge { } Class adapterClass = Class.forName("com.sk89q.worldedit.fabric.FabricAdapter"); - Object actor; - try { - actor = invokeCompatibleStatic(adapterClass, "adaptPlayer", player); - } catch (NoSuchMethodException ignored) { - actor = invokeCompatibleStatic(adapterClass, "adapt", player); + Object actor = adaptPlayer(adapterClass, player); + if (actor == null) { + throw new IllegalStateException("FabricAdapter tidak dapat mengadaptasi player."); } - if (actor == null) throw new IllegalStateException("FabricAdapter tidak dapat mengadaptasi player."); Class worldEditClass = Class.forName("com.sk89q.worldedit.WorldEdit"); Object worldEdit = worldEditClass.getMethod("getInstance").invoke(null); @@ -53,6 +53,30 @@ public final class WorldEditSelectionBridge { ); } + private static Object adaptPlayer(Class adapterClass, ServerPlayer player) throws Exception { + // WorldEdit 7.4.x Fabric API: FabricAdapter.get().fromNativePlayer(ServerPlayer) + try { + Object adapter = adapterClass.getMethod("get").invoke(null); + try { + return invokeCompatible(adapter, "fromNativePlayer", player); + } catch (NoSuchMethodException ignored) { + // Some CoreMcAdapter builds may expose a differently named instance bridge. + try { + return invokeCompatible(adapter, "adaptPlayer", player); + } catch (NoSuchMethodException ignoredAgain) { + return invokeCompatible(adapter, "adapt", player); + } + } + } catch (NoSuchMethodException ignored) { + // Backward compatibility for older WorldEdit Fabric adapters. + try { + return invokeCompatibleStatic(adapterClass, "adaptPlayer", player); + } catch (NoSuchMethodException ignoredAgain) { + return invokeCompatibleStatic(adapterClass, "adapt", player); + } + } + } + private static int intValue(Object object, String method) throws Exception { return ((Number) object.getClass().getMethod(method).invoke(object)).intValue(); }