Fix player join

This commit is contained in:
Darto KLoning
2026-09-07 07:06:49 +07:00
parent 244a8865f0
commit dda9d76ec0
7 changed files with 152 additions and 62 deletions
+13 -9
View File
@@ -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. **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 ```text
============================================ ============================================
Lobby Systems - Fabric 26.2 Build Lobby Systems - Fabric 26.2 Build
Version: 1.5.3 Version: 1.5.5
Namespace: id.dartokloning.lobbysystems Namespace: id.dartokloning.lobbysystems
Mod ID: dartokloning Mod ID: dartokloning
============================================ ============================================
@@ -52,7 +52,7 @@ chmod +x compile.sh release.sh
Output utama: Output utama:
```text ```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. `*-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 ```text
============================================ ============================================
Lobby Systems - READY Lobby Systems - READY
Version: 1.5.3 Version: 1.5.5
Minecraft: 26.2 Minecraft: 26.2
Config directory: config/lobby Config directory: config/lobby
WorldEdit: available WorldEdit: available
============================================ ============================================
``` ```
## Struktur konfigurasi v1.5.3 ## Struktur konfigurasi v1.5.5
```text ```text
config/ config/
@@ -401,9 +401,13 @@ Setelah portal aktif, `portalCooldownTicks` mencegah portal terpicu berulang saa
## Flow server ## Flow server
```text ```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 Player berjalan ke portal
↓ ↓
+1 -1
View File
@@ -5,6 +5,6 @@ minecraft_version=26.2
loader_version=0.19.3 loader_version=0.19.3
loom_version=1.17-SNAPSHOT loom_version=1.17-SNAPSHOT
fabric_api_version=0.156.0+26.2 fabric_api_version=0.156.0+26.2
mod_version=1.5.3 mod_version=1.5.5
maven_group=id.dartokloning maven_group=id.dartokloning
archives_base_name=lobby-systems archives_base_name=lobby-systems
@@ -23,7 +23,9 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.gamerules.GameRule; import net.minecraft.world.level.gamerules.GameRule;
import id.dartokloning.lobbysystems.mixin.ServerPlayerGameModeAccessor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -131,23 +133,24 @@ public final class LobbySystemsMod implements ModInitializer {
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> { ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
ServerPlayer player = listener.getPlayer(); ServerPlayer player = listener.getPlayer();
server.execute(() -> {
POSITIONS.captureCurrent(player); // Initial dimension/position sudah dipindahkan ke Lobby oleh PlayerListMixin
TELEPORTS.scheduleJoinToLobby(player); // sebelum paket login/world awal dikirim. Di titik JOIN ini kita hanya
if (CONFIG.get().manageDimensionGamemode) { // sinkronkan state runtime dan memberi enforcement singkat sebagai proteksi
// Terapkan segera pada state player yang sudah dimuat, lalu pertahankan // terhadap mod lain yang mengubah gamemode tepat setelah login.
// stabilization window agar state login yang diterapkan terlambat oleh if (CONFIG.get().manageDimensionGamemode) {
// vanilla/mod lain tidak mengalahkan konfigurasi Lobby Systems. DIMENSION_GAMEMODES.applyForPlayer(player, true);
DIMENSION_GAMEMODES.applyForPlayer(player, true); DIMENSION_GAMEMODES.beginJoinEnforcement(player.getUUID());
DIMENSION_GAMEMODES.beginJoinEnforcement(player.getUUID()); }
} SESSIONS.onJoin(player.getUUID(), lobbyKey());
LobbyLog.state(
"PLAYER JOIN", LobbyLog.state(
"Player: " + player.getName().getString(), "PLAYER JOIN",
"UUID: " + player.getUUID(), "Player: " + player.getName().getString(),
"Lobby teleport: scheduled" "UUID: " + player.getUUID(),
); "Initial dimension: " + player.level().dimension().identifier(),
}); "Lobby placement: pre-login / immediate"
);
}); });
ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> { ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> {
@@ -166,11 +169,7 @@ public final class LobbySystemsMod implements ModInitializer {
}); });
ServerTickEvents.END_SERVER_TICK.register(server -> { ServerTickEvents.END_SERVER_TICK.register(server -> {
TELEPORTS.tick(server, (player, minecraftServer) -> { TELEPORTS.tick();
if (teleportToLobby(player, minecraftServer, false)) {
SESSIONS.onJoin(player.getUUID(), lobbyKey());
}
});
if (CONFIG.get().manageDimensionGamemode) { if (CONFIG.get().manageDimensionGamemode) {
DIMENSION_GAMEMODES.tick(server); DIMENSION_GAMEMODES.tick(server);
@@ -940,6 +939,68 @@ public final class LobbySystemsMod implements ModInitializer {
return 1; 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<Level> 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<Level> 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) { static ServerLevel findLevelForManager(MinecraftServer server, String dimensionId) {
return findLevel(server, dimensionId); return findLevel(server, dimensionId);
} }
@@ -2,7 +2,6 @@ package id.dartokloning.lobbysystems;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative; import net.minecraft.world.entity.Relative;
@@ -17,39 +16,22 @@ import java.util.UUID;
/** /**
* Menangani teleport lintas dimensi melalui TeleportTransition vanilla. * Menangani teleport lintas dimensi melalui TeleportTransition vanilla.
* Juga memberi lock singkat agar request teleport tidak bertumpuk ketika * Login player tidak memakai coordinator ini: initial placement ke Lobby dilakukan
* client masih menyelesaikan pergantian dimension/chunk. * sebelum PlayerList mengirim world awal ke client.
*/ */
public final class TeleportCoordinator { public final class TeleportCoordinator {
private static final int TELEPORT_LOCK_TICKS = 40; // 2 detik 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<UUID, Long> lockedUntilTick = new HashMap<>(); private final Map<UUID, Long> lockedUntilTick = new HashMap<>();
private final Map<UUID, PendingLobbyTeleport> pendingJoinTeleports = new HashMap<>();
private long currentTick; private long currentTick;
public void scheduleJoinToLobby(ServerPlayer player) {
pendingJoinTeleports.put(player.getUUID(), new PendingLobbyTeleport(currentTick + JOIN_DELAY_TICKS));
}
public void onDisconnect(UUID uuid) { public void onDisconnect(UUID uuid) {
lockedUntilTick.remove(uuid); lockedUntilTick.remove(uuid);
pendingJoinTeleports.remove(uuid);
} }
public void tick(MinecraftServer server, LobbyTeleportHandler lobbyHandler) { public void tick() {
currentTick++; currentTick++;
lockedUntilTick.entrySet().removeIf(entry -> entry.getValue() <= 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) { public boolean isLocked(ServerPlayer player) {
@@ -66,7 +48,6 @@ public final class TeleportCoordinator {
ResourceKey<Level> from = player.level().dimension(); ResourceKey<Level> from = player.level().dimension();
ResourceKey<Level> to = target.dimension(); ResourceKey<Level> to = target.dimension();
// Paksa destination chunk tersedia sebelum transition dikirim ke client.
int chunkX = ((int) Math.floor(x)) >> 4; int chunkX = ((int) Math.floor(x)) >> 4;
int chunkZ = ((int) Math.floor(z)) >> 4; int chunkZ = ((int) Math.floor(z)) >> 4;
target.getChunk(chunkX, chunkZ); target.getChunk(chunkX, chunkZ);
@@ -108,11 +89,4 @@ public final class TeleportCoordinator {
return true; return true;
} }
@FunctionalInterface
public interface LobbyTeleportHandler {
void teleport(ServerPlayer player, MinecraftServer server);
}
private record PendingLobbyTeleport(long executeAtTick) {}
} }
@@ -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);
}
}
@@ -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);
}
+3 -1
View File
@@ -3,7 +3,9 @@
"package": "id.dartokloning.lobbysystems.mixin", "package": "id.dartokloning.lobbysystems.mixin",
"compatibilityLevel": "JAVA_25", "compatibilityLevel": "JAVA_25",
"mixins": [ "mixins": [
"ServerLevelGameRulesMixin" "ServerLevelGameRulesMixin",
"PlayerListMixin",
"ServerPlayerGameModeAccessor"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1