diff --git a/README.md b/README.md index 6a13e1a..f5a8223 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lobby Systems v1.5.3 — Fabric 26.2 +# Lobby Systems v1.5.5 — Fabric 26.2 -Server-side lobby routing untuk Minecraft 26.2/Fabric. v1.5.3 memastikan dimension gamemode dipaksa segera setelah teleport, termasuk join/reconnect ke Lobby, dan menambahkan mode Git-only pada script compile bila JAR versi yang sama sudah pernah dibuild. +Server-side lobby routing untuk Minecraft 26.2/Fabric. v1.5.5 mengubah proses login agar Lobby menjadi initial dimension player sebelum world awal dikirim ke client, sehingga tidak ada lagi teleport join yang ditunda atau "persinggahan" di dimension terakhir. -**v1.5.3 bugfix/workflow:** gamemode Lobby kini langsung diterapkan setelah join teleport sehingga gamemode lama dari player data tidak tertinggal. `compile.bat` / `compile.sh` juga mendeteksi JAR versi yang sama dan menawarkan Compile ulang atau Git-only tanpa build ulang. +**v1.5.5 login placement fix:** player data tetap dibaca untuk menyimpan posisi gameplay terakhir, tetapi sebelum `PlayerList` mengirim initial world state, Lobby Systems mengganti initial level, posisi, yaw/pitch, dan gamemode ke konfigurasi Lobby. Mekanisme scheduled join teleport dari versi sebelumnya telah dihapus. `compile.bat` / `compile.sh` tetap mendeteksi JAR versi yang sama dan menawarkan Compile ulang atau Git-only. **v1.5.0 feature release:** Portal Visual System menambahkan permukaan swirl ungu memakai block state vanilla `minecraft:nether_portal`, dengan axis X/Z yang ditentukan otomatis dari WorldEdit selection. Frame boleh memakai block apa saja. `manageDimensionGamemode` tetap default `true`, compatibility warning Dimensional Inventories tetap aktif, dan bridge WorldEdit 7.4.x tetap dipakai. @@ -43,7 +43,7 @@ chmod +x compile.sh release.sh ```text ============================================ Lobby Systems - Fabric 26.2 Build - Version: 1.5.3 + Version: 1.5.5 Namespace: id.dartokloning.lobbysystems Mod ID: dartokloning ============================================ @@ -52,7 +52,7 @@ chmod +x compile.sh release.sh Output utama: ```text -build/libs/lobby-systems-1.5.3.jar +build/libs/lobby-systems-1.5.5.jar ``` `*-sources.jar` hanya source archive dan tidak perlu dipasang ke server. @@ -83,14 +83,14 @@ State penting Lobby Systems memakai separator agar mudah terlihat di server cons ```text ============================================ Lobby Systems - READY - Version: 1.5.3 + Version: 1.5.5 Minecraft: 26.2 Config directory: config/lobby WorldEdit: available ============================================ ``` -## Struktur konfigurasi v1.5.3 +## Struktur konfigurasi v1.5.5 ```text config/ @@ -401,9 +401,13 @@ Setelah portal aktif, `portalCooldownTicks` mencegah portal terpicu berulang saa ## Flow server ```text -JOIN +PLAYER DATA LOADED ↓ -Lobby fixed spawn +Lobby Systems pre-login placement + ↓ +Initial world sent to client = Lobby + ↓ +Lobby fixed spawn + configured gamemode ↓ Player berjalan ke portal ↓ diff --git a/gradle.properties b/gradle.properties index 3751e8f..6c8320e 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.5.3 +mod_version=1.5.5 maven_group=id.dartokloning archives_base_name=lobby-systems diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java index abf824b..fc7b128 100644 --- a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java +++ b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java @@ -23,7 +23,9 @@ 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.GameType; import net.minecraft.world.level.gamerules.GameRule; +import id.dartokloning.lobbysystems.mixin.ServerPlayerGameModeAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,23 +133,24 @@ public final class LobbySystemsMod implements ModInitializer { ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> { ServerPlayer player = listener.getPlayer(); - server.execute(() -> { - POSITIONS.captureCurrent(player); - TELEPORTS.scheduleJoinToLobby(player); - if (CONFIG.get().manageDimensionGamemode) { - // Terapkan segera pada state player yang sudah dimuat, lalu pertahankan - // stabilization window agar state login yang diterapkan terlambat oleh - // vanilla/mod lain tidak mengalahkan konfigurasi Lobby Systems. - DIMENSION_GAMEMODES.applyForPlayer(player, true); - DIMENSION_GAMEMODES.beginJoinEnforcement(player.getUUID()); - } - LobbyLog.state( - "PLAYER JOIN", - "Player: " + player.getName().getString(), - "UUID: " + player.getUUID(), - "Lobby teleport: scheduled" - ); - }); + + // Initial dimension/position sudah dipindahkan ke Lobby oleh PlayerListMixin + // sebelum paket login/world awal dikirim. Di titik JOIN ini kita hanya + // sinkronkan state runtime dan memberi enforcement singkat sebagai proteksi + // terhadap mod lain yang mengubah gamemode tepat setelah login. + if (CONFIG.get().manageDimensionGamemode) { + DIMENSION_GAMEMODES.applyForPlayer(player, true); + DIMENSION_GAMEMODES.beginJoinEnforcement(player.getUUID()); + } + SESSIONS.onJoin(player.getUUID(), lobbyKey()); + + LobbyLog.state( + "PLAYER JOIN", + "Player: " + player.getName().getString(), + "UUID: " + player.getUUID(), + "Initial dimension: " + player.level().dimension().identifier(), + "Lobby placement: pre-login / immediate" + ); }); ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> { @@ -166,11 +169,7 @@ public final class LobbySystemsMod implements ModInitializer { }); ServerTickEvents.END_SERVER_TICK.register(server -> { - TELEPORTS.tick(server, (player, minecraftServer) -> { - if (teleportToLobby(player, minecraftServer, false)) { - SESSIONS.onJoin(player.getUUID(), lobbyKey()); - } - }); + TELEPORTS.tick(); if (CONFIG.get().manageDimensionGamemode) { DIMENSION_GAMEMODES.tick(server); @@ -940,6 +939,68 @@ public final class LobbySystemsMod implements ModInitializer { return 1; } + /** + * Dipanggil dari PlayerListMixin pada HEAD placeNewPlayer, yaitu setelah player data + * sudah dimuat tetapi sebelum PlayerList mengirim paket login/world awal ke client. + * Ini membuat Lobby menjadi initial world yang benar-benar dilihat client, bukan + * teleport tertunda setelah player sempat muncul di posisi/dimension terakhir. + */ + public static void preparePlayerForInitialLogin(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + + final ResourceKey key; + try { + key = lobbyKey(); + } catch (Exception e) { + LOGGER.error("Cannot prepare login lobby: invalid lobbyDimension {}", CONFIG.get().lobbyDimension, e); + return; + } + + ServerLevel target = server.getLevel(key); + if (target == null) { + LOGGER.error("Cannot prepare login lobby: dimension {} is not loaded/found.", CONFIG.get().lobbyDimension); + return; + } + + // Simpan posisi terakhir gameplay SEBELUM level player diganti ke Lobby. + POSITIONS.captureCurrent(player); + + LobbyConfig c = CONFIG.get(); + ResourceKey previousDimension = player.level().dimension(); + + // Tidak memakai ServerPlayer#teleport di fase ini. Player belum ditempatkan ke + // world aktif; kita langsung menentukan level + posisi yang akan dipakai oleh + // PlayerList ketika membangun ClientboundLoginPacket dan initial world state. + player.setServerLevel(target); + player.gameMode.setLevel(target); + player.snapTo(c.lobbyX, c.lobbyY, c.lobbyZ, c.lobbyYaw, c.lobbyPitch); + + if (c.manageDimensionGamemode) { + String configuredMode = DIMENSION_GAMEMODES.get(c.lobbyDimension); + GameType gameType = DimensionGameModeManager.parseMode(configuredMode); + if (gameType != null && player.gameMode.getGameModeForPlayer() != gameType) { + GameType previousMode = player.gameMode.getGameModeForPlayer(); + ServerPlayerGameModeAccessor accessor = (ServerPlayerGameModeAccessor) player.gameMode; + accessor.lobbySystems$setPreviousGameModeForPlayer(previousMode); + accessor.lobbySystems$setGameModeForPlayer(gameType); + gameType.updatePlayerAbilities(player.getAbilities()); + } + } + + LobbyLog.state( + "PLAYER PRE-LOGIN PLACEMENT", + "Player: " + player.getName().getString(), + "Loaded from: " + previousDimension.identifier(), + "Initial world sent to client: " + target.dimension().identifier(), + "Position: [" + String.format("%.2f", c.lobbyX) + ", " + + String.format("%.2f", c.lobbyY) + ", " + + String.format("%.2f", c.lobbyZ) + "]", + "Gamemode: " + (c.manageDimensionGamemode + ? String.valueOf(DIMENSION_GAMEMODES.get(c.lobbyDimension)) + : "unmanaged") + ); + } + static ServerLevel findLevelForManager(MinecraftServer server, String dimensionId) { return findLevel(server, dimensionId); } diff --git a/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java b/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java index 0c5ffed..afd6f11 100644 --- a/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java +++ b/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java @@ -2,7 +2,6 @@ package id.dartokloning.lobbysystems; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Relative; @@ -17,39 +16,22 @@ import java.util.UUID; /** * Menangani teleport lintas dimensi melalui TeleportTransition vanilla. - * Juga memberi lock singkat agar request teleport tidak bertumpuk ketika - * client masih menyelesaikan pergantian dimension/chunk. + * Login player tidak memakai coordinator ini: initial placement ke Lobby dilakukan + * sebelum PlayerList mengirim world awal ke client. */ public final class TeleportCoordinator { private static final int TELEPORT_LOCK_TICKS = 40; // 2 detik - private static final int JOIN_DELAY_TICKS = 20; // 1 detik setelah JOIN private final Map lockedUntilTick = new HashMap<>(); - private final Map pendingJoinTeleports = new HashMap<>(); private long currentTick; - public void scheduleJoinToLobby(ServerPlayer player) { - pendingJoinTeleports.put(player.getUUID(), new PendingLobbyTeleport(currentTick + JOIN_DELAY_TICKS)); - } - public void onDisconnect(UUID uuid) { lockedUntilTick.remove(uuid); - pendingJoinTeleports.remove(uuid); } - public void tick(MinecraftServer server, LobbyTeleportHandler lobbyHandler) { + public void tick() { currentTick++; lockedUntilTick.entrySet().removeIf(entry -> entry.getValue() <= currentTick); - - pendingJoinTeleports.entrySet().removeIf(entry -> { - if (entry.getValue().executeAtTick > currentTick) return false; - - ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); - if (player != null) { - lobbyHandler.teleport(player, server); - } - return true; - }); } public boolean isLocked(ServerPlayer player) { @@ -66,7 +48,6 @@ public final class TeleportCoordinator { ResourceKey from = player.level().dimension(); ResourceKey to = target.dimension(); - // Paksa destination chunk tersedia sebelum transition dikirim ke client. int chunkX = ((int) Math.floor(x)) >> 4; int chunkZ = ((int) Math.floor(z)) >> 4; target.getChunk(chunkX, chunkZ); @@ -108,11 +89,4 @@ public final class TeleportCoordinator { return true; } - - @FunctionalInterface - public interface LobbyTeleportHandler { - void teleport(ServerPlayer player, MinecraftServer server); - } - - private record PendingLobbyTeleport(long executeAtTick) {} } diff --git a/src/main/java/id/dartokloning/lobbysystems/mixin/PlayerListMixin.java b/src/main/java/id/dartokloning/lobbysystems/mixin/PlayerListMixin.java new file mode 100644 index 0000000..f5f507a --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/mixin/PlayerListMixin.java @@ -0,0 +1,29 @@ +package id.dartokloning.lobbysystems.mixin; + +import id.dartokloning.lobbysystems.LobbySystemsMod; +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Menempatkan player ke Lobby sebelum PlayerList mengirim paket login/world awal. + * Dengan begitu client tidak sempat terlebih dahulu masuk ke dimension/posisi terakhir + * kemudian baru menerima teleport tertunda. + */ +@Mixin(PlayerList.class) +public abstract class PlayerListMixin { + @Inject(method = "placeNewPlayer", at = @At("HEAD")) + private void lobbySystems$prepareLobbyBeforeInitialPlacement( + Connection connection, + ServerPlayer player, + CommonListenerCookie cookie, + CallbackInfo ci + ) { + LobbySystemsMod.preparePlayerForInitialLogin(player); + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/mixin/ServerPlayerGameModeAccessor.java b/src/main/java/id/dartokloning/lobbysystems/mixin/ServerPlayerGameModeAccessor.java new file mode 100644 index 0000000..d8d2597 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/mixin/ServerPlayerGameModeAccessor.java @@ -0,0 +1,20 @@ +package id.dartokloning.lobbysystems.mixin; + +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.level.GameType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * Akses internal gamemode hanya untuk fase pre-login, sebelum connection player siap. + * Kita tidak memanggil ServerPlayer#setGameMode di titik ini karena method tersebut + * dapat mencoba mengirim update network terlalu dini. + */ +@Mixin(ServerPlayerGameMode.class) +public interface ServerPlayerGameModeAccessor { + @Accessor("gameModeForPlayer") + void lobbySystems$setGameModeForPlayer(GameType gameType); + + @Accessor("previousGameModeForPlayer") + void lobbySystems$setPreviousGameModeForPlayer(GameType gameType); +} diff --git a/src/main/resources/lobby-systems.mixins.json b/src/main/resources/lobby-systems.mixins.json index 0395f89..6cffd1e 100644 --- a/src/main/resources/lobby-systems.mixins.json +++ b/src/main/resources/lobby-systems.mixins.json @@ -3,7 +3,9 @@ "package": "id.dartokloning.lobbysystems.mixin", "compatibilityLevel": "JAVA_25", "mixins": [ - "ServerLevelGameRulesMixin" + "ServerLevelGameRulesMixin", + "PlayerListMixin", + "ServerPlayerGameModeAccessor" ], "injectors": { "defaultRequire": 1