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
@@ -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<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) {
return findLevel(server, dimensionId);
}
@@ -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<UUID, Long> lockedUntilTick = new HashMap<>();
private final Map<UUID, PendingLobbyTeleport> 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<Level> from = player.level().dimension();
ResourceKey<Level> 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) {}
}
@@ -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);
}