Initial Lobby Systems v1.2.0
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class ConfigManager {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("lobby-systems.json");
|
||||
private LobbyConfig config = new LobbyConfig();
|
||||
|
||||
public LobbyConfig get() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public boolean load() {
|
||||
if (!Files.exists(PATH)) {
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
try (Reader reader = Files.newBufferedReader(PATH)) {
|
||||
LobbyConfig loaded = GSON.fromJson(reader, LobbyConfig.class);
|
||||
if (loaded == null) throw new IllegalArgumentException("Config JSON kosong atau tidak valid.");
|
||||
validate(loaded);
|
||||
config = loaded;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to load {}. Keeping previous config.", PATH, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
Files.createDirectories(PATH.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(PATH)) {
|
||||
GSON.toJson(config, writer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to save {}", PATH, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path path() {
|
||||
return PATH;
|
||||
}
|
||||
|
||||
private static void validate(LobbyConfig c) {
|
||||
if (c.lobbyDimension == null || c.lobbyDimension.isBlank()) {
|
||||
throw new IllegalArgumentException("lobbyDimension tidak boleh kosong.");
|
||||
}
|
||||
if (c.minimumPlaySeconds < 0) {
|
||||
throw new IllegalArgumentException("minimumPlaySeconds tidak boleh negatif.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
public final class LobbyConfig {
|
||||
public String lobbyDimension = "minecraft:overworld";
|
||||
public double lobbyX = 0.5;
|
||||
public double lobbyY = 100.0;
|
||||
public double lobbyZ = 0.5;
|
||||
public float lobbyYaw = 0.0f;
|
||||
public float lobbyPitch = 0.0f;
|
||||
public int minimumPlaySeconds = 300;
|
||||
|
||||
public String inLobbyMessage = "§eKamu sudah berada di lobby.";
|
||||
public String returnedMessage = "§aKamu telah kembali ke lobby.";
|
||||
public String waitMessage = "§cKamu belum bisa kembali ke lobby. Tunggu §e{time}§c lagi.";
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
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.level.Level;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public final class LobbySystemsMod implements ModInitializer {
|
||||
public static final String MOD_ID = "dartokloning";
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
private static final ConfigManager CONFIG = new ConfigManager();
|
||||
private static final SessionTracker SESSIONS = new SessionTracker();
|
||||
private static final PositionManager POSITIONS = new PositionManager();
|
||||
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
|
||||
private static int saveTicker;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
CONFIG.load();
|
||||
POSITIONS.load();
|
||||
POSITIONS.setExcludedDimension(lobbyKey());
|
||||
|
||||
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
|
||||
ServerPlayer player = listener.getPlayer();
|
||||
server.execute(() -> {
|
||||
// Simpan posisi login terakhir jika player sebelumnya logout di game dimension.
|
||||
// Kalau player login di lobby, PositionManager akan mengabaikannya secara mutlak.
|
||||
POSITIONS.captureCurrent(player);
|
||||
|
||||
// Jangan cross-dimension teleport pada callback JOIN itu sendiri.
|
||||
// Tunggu 20 tick supaya client sudah stabil di play state.
|
||||
TELEPORTS.scheduleJoinToLobby(player);
|
||||
LOGGER.info("Scheduled lobby login teleport for {} ({})", player.getName().getString(), player.getUUID());
|
||||
});
|
||||
});
|
||||
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> {
|
||||
ServerPlayer player = listener.getPlayer();
|
||||
POSITIONS.onDisconnect(player);
|
||||
SESSIONS.onDisconnect(player.getUUID());
|
||||
TELEPORTS.onDisconnect(player.getUUID());
|
||||
});
|
||||
|
||||
ServerTickEvents.END_SERVER_TICK.register(server -> {
|
||||
TELEPORTS.tick(server, (player, minecraftServer) -> {
|
||||
if (teleportToLobby(player, minecraftServer, false)) {
|
||||
SESSIONS.onJoin(player.getUUID(), lobbyKey());
|
||||
}
|
||||
});
|
||||
|
||||
ResourceKey<Level> lobby = lobbyKey();
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
POSITIONS.update(player);
|
||||
SESSIONS.update(player.getUUID(), player.level().dimension(), lobby);
|
||||
}
|
||||
|
||||
if (++saveTicker >= 1200) {
|
||||
saveTicker = 0;
|
||||
POSITIONS.saveIfDirty();
|
||||
}
|
||||
});
|
||||
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, buildContext, selection) -> {
|
||||
dispatcher.register(Commands.literal("lobby").executes(ctx -> {
|
||||
ServerPlayer player = ctx.getSource().getPlayerOrException();
|
||||
ResourceKey<Level> lobby = lobbyKey();
|
||||
|
||||
if (player.level().dimension().equals(lobby)) {
|
||||
player.sendSystemMessage(Component.literal(CONFIG.get().inLobbyMessage));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (TELEPORTS.isLocked(player)) {
|
||||
player.sendSystemMessage(Component.literal("§eTeleport sebelumnya masih diproses. Tunggu sebentar lalu coba lagi."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
long remaining = SESSIONS.remainingSeconds(player.getUUID(), CONFIG.get().minimumPlaySeconds);
|
||||
if (remaining > 0) {
|
||||
player.sendSystemMessage(Component.literal(
|
||||
CONFIG.get().waitMessage.replace("{time}", formatDuration(remaining))));
|
||||
return 0;
|
||||
}
|
||||
|
||||
POSITIONS.captureCurrent(player);
|
||||
return teleportToLobby(player, ctx.getSource().getServer(), true) ? 1 : 0;
|
||||
}));
|
||||
|
||||
dispatcher.register(Commands.literal("ls")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
||||
.then(Commands.literal("reload")
|
||||
.executes(ctx -> reloadConfig(ctx.getSource().getServer(), ctx.getSource())))
|
||||
.then(Commands.literal("tp")
|
||||
.then(Commands.argument("dimension", StringArgumentType.greedyString())
|
||||
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
|
||||
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
|
||||
.executes(ctx -> teleportToDimension(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
ctx.getSource().getServer(),
|
||||
StringArgumentType.getString(ctx, "dimension")
|
||||
))))
|
||||
);
|
||||
});
|
||||
|
||||
LOGGER.info("Lobby Systems v1.1.3 initialized for Minecraft 26.2.");
|
||||
}
|
||||
|
||||
private static int reloadConfig(MinecraftServer server, net.minecraft.commands.CommandSourceStack source) {
|
||||
if (!CONFIG.load()) {
|
||||
source.sendFailure(Component.literal("§cLobby Systems: reload gagal. Cek console untuk detail error JSON/config."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
final ResourceKey<Level> key;
|
||||
try {
|
||||
key = lobbyKey();
|
||||
if (server.getLevel(key) == null) {
|
||||
source.sendFailure(Component.literal("§eConfig berhasil dibaca, tetapi lobbyDimension '" + CONFIG.get().lobbyDimension + "' tidak sedang ter-load."));
|
||||
return 0;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
source.sendFailure(Component.literal("§cConfig berhasil dibaca, tetapi lobbyDimension tidak valid: " + CONFIG.get().lobbyDimension));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Ini bukan sekadar cleanup sekali. PositionManager menjadikan lobby sebagai
|
||||
// excluded dimension permanen dan akan menyaringnya lagi sebelum setiap save.
|
||||
POSITIONS.setExcludedDimension(key);
|
||||
|
||||
source.sendSuccess(() -> Component.literal(
|
||||
"§aLobby Systems config direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
|
||||
+ " §7| Minimum play: §f" + CONFIG.get().minimumPlaySeconds + "s"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int teleportToDimension(ServerPlayer player, MinecraftServer server, String dimensionId) {
|
||||
final Identifier id;
|
||||
try {
|
||||
id = Identifier.parse(dimensionId.trim());
|
||||
} catch (Exception e) {
|
||||
player.sendSystemMessage(Component.literal("§cDimension ID tidak valid: " + dimensionId));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
ServerLevel target = server.getLevel(key);
|
||||
if (target == null) {
|
||||
player.sendSystemMessage(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ResourceKey<Level> lobby = lobbyKey();
|
||||
if (key.equals(lobby)) {
|
||||
POSITIONS.captureCurrent(player);
|
||||
boolean ok = teleportToLobby(player, server, false);
|
||||
if (ok) {
|
||||
player.sendSystemMessage(Component.literal("§aTeleport ke lobby pada posisi spawn yang dikonfigurasi."));
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (player.level().dimension().equals(key)) {
|
||||
player.sendSystemMessage(Component.literal("§eKamu sudah berada di dimension " + dimensionId + "."));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (TELEPORTS.isLocked(player)) {
|
||||
player.sendSystemMessage(Component.literal("§eTeleport sebelumnya masih diproses. Tunggu sebentar lalu coba lagi."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
POSITIONS.captureCurrent(player);
|
||||
PlayerPosition saved = POSITIONS.get(player.getUUID(), key);
|
||||
|
||||
double x;
|
||||
double y;
|
||||
double z;
|
||||
float yaw;
|
||||
float pitch;
|
||||
boolean returning;
|
||||
|
||||
if (saved != null) {
|
||||
x = saved.x;
|
||||
y = saved.y;
|
||||
z = saved.z;
|
||||
yaw = saved.yaw;
|
||||
pitch = saved.pitch;
|
||||
returning = true;
|
||||
} else {
|
||||
BlockPos spawn = target.getRespawnData().pos();
|
||||
x = spawn.getX() + 0.5;
|
||||
y = spawn.getY();
|
||||
z = spawn.getZ() + 0.5;
|
||||
yaw = player.getYRot();
|
||||
pitch = player.getXRot();
|
||||
returning = false;
|
||||
}
|
||||
|
||||
boolean ok = TELEPORTS.teleport(player, target, x, y, z, yaw, pitch);
|
||||
if (!ok) {
|
||||
player.sendSystemMessage(Component.literal("§cTeleport ke " + dimensionId + " gagal."));
|
||||
return 0;
|
||||
}
|
||||
|
||||
player.sendSystemMessage(Component.literal(returning
|
||||
? "§aTeleport ke §f" + dimensionId + "§a pada posisi terakhirmu."
|
||||
: "§aTeleport ke §f" + dimensionId + "§a. Belum ada posisi terakhir, jadi menggunakan spawn dimension."));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static ResourceKey<Level> lobbyKey() {
|
||||
Identifier id = Identifier.parse(CONFIG.get().lobbyDimension);
|
||||
return ResourceKey.create(Registries.DIMENSION, id);
|
||||
}
|
||||
|
||||
private static boolean teleportToLobby(ServerPlayer player, MinecraftServer server, boolean sendMessage) {
|
||||
final ResourceKey<Level> key;
|
||||
try {
|
||||
key = lobbyKey();
|
||||
} catch (Exception e) {
|
||||
player.sendSystemMessage(Component.literal("§cLobby Systems: lobbyDimension tidak valid."));
|
||||
LOGGER.error("Invalid lobbyDimension: {}", CONFIG.get().lobbyDimension, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerLevel target = server.getLevel(key);
|
||||
if (target == null) {
|
||||
player.sendSystemMessage(Component.literal("§cLobby Systems: dimensi lobby '" + CONFIG.get().lobbyDimension + "' tidak ditemukan."));
|
||||
LOGGER.error("Lobby dimension {} is not loaded/found.", CONFIG.get().lobbyDimension);
|
||||
return false;
|
||||
}
|
||||
|
||||
LobbyConfig c = CONFIG.get();
|
||||
|
||||
// Jika sudah di lobby (mis. join terakhir memang lobby), tidak perlu cross-dimension
|
||||
// transition. Tetap pakai TeleportTransition untuk menempatkan player ke canonical spawn.
|
||||
boolean ok = TELEPORTS.teleport(player, target, c.lobbyX, c.lobbyY, c.lobbyZ, c.lobbyYaw, c.lobbyPitch);
|
||||
if (ok && sendMessage) {
|
||||
player.sendSystemMessage(Component.literal(c.returnedMessage));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static String formatDuration(long seconds) {
|
||||
long minutes = seconds / 60;
|
||||
long secs = seconds % 60;
|
||||
return minutes > 0 ? minutes + "m " + secs + "s" : secs + "s";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
public final class PlayerPosition {
|
||||
public double x;
|
||||
public double y;
|
||||
public double z;
|
||||
public float yaw;
|
||||
public float pitch;
|
||||
|
||||
public PlayerPosition() {
|
||||
}
|
||||
|
||||
public PlayerPosition(double x, double y, double z, float yaw, float pitch) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
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.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PositionManager {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("lobby-systems-positions.json");
|
||||
private static final Type DATA_TYPE = new TypeToken<Map<String, Map<String, PlayerPosition>>>() {}.getType();
|
||||
|
||||
private final Map<String, Map<String, PlayerPosition>> positions = new HashMap<>();
|
||||
private final Map<UUID, Snapshot> snapshots = new HashMap<>();
|
||||
private ResourceKey<Level> excludedDimension;
|
||||
private boolean dirty;
|
||||
|
||||
public void load() {
|
||||
positions.clear();
|
||||
if (!Files.exists(PATH)) return;
|
||||
|
||||
try (Reader reader = Files.newBufferedReader(PATH)) {
|
||||
Map<String, Map<String, PlayerPosition>> loaded = GSON.fromJson(reader, DATA_TYPE);
|
||||
if (loaded != null) positions.putAll(loaded);
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to load player positions from {}", PATH, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimension ini sama sekali tidak boleh disimpan sebagai last-position.
|
||||
* Dipakai untuk lobby. Pemanggilan method ini juga membersihkan data lama
|
||||
* yang mungkin masih tersisa dari versi mod sebelumnya.
|
||||
*/
|
||||
public void setExcludedDimension(ResourceKey<Level> dimension) {
|
||||
this.excludedDimension = dimension;
|
||||
purgeExcludedDimension();
|
||||
saveIfDirty();
|
||||
}
|
||||
|
||||
public void update(ServerPlayer player) {
|
||||
UUID uuid = player.getUUID();
|
||||
Snapshot current = Snapshot.from(player);
|
||||
Snapshot previous = snapshots.put(uuid, current);
|
||||
|
||||
if (previous != null && !previous.dimension.equals(current.dimension)) {
|
||||
store(uuid, previous.dimension, previous.position);
|
||||
}
|
||||
}
|
||||
|
||||
public void captureCurrent(ServerPlayer player) {
|
||||
Snapshot snapshot = Snapshot.from(player);
|
||||
snapshots.put(player.getUUID(), snapshot);
|
||||
store(player.getUUID(), snapshot.dimension, snapshot.position);
|
||||
}
|
||||
|
||||
public void onDisconnect(ServerPlayer player) {
|
||||
captureCurrent(player);
|
||||
snapshots.remove(player.getUUID());
|
||||
saveIfDirty();
|
||||
}
|
||||
|
||||
public PlayerPosition get(UUID uuid, ResourceKey<Level> dimension) {
|
||||
if (isExcluded(dimension)) return null;
|
||||
Map<String, PlayerPosition> byDimension = positions.get(uuid.toString());
|
||||
if (byDimension == null) return null;
|
||||
return byDimension.get(dimension.identifier().toString());
|
||||
}
|
||||
|
||||
public void saveIfDirty() {
|
||||
if (!dirty) return;
|
||||
save();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
// Guard terakhir: walaupun ada bug di caller, data lobby tidak boleh masuk file.
|
||||
purgeExcludedDimension();
|
||||
|
||||
try {
|
||||
Files.createDirectories(PATH.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(PATH)) {
|
||||
GSON.toJson(positions, DATA_TYPE, writer);
|
||||
}
|
||||
dirty = false;
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to save player positions to {}", PATH, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void store(UUID uuid, ResourceKey<Level> dimension, PlayerPosition position) {
|
||||
if (isExcluded(dimension)) return;
|
||||
|
||||
positions.computeIfAbsent(uuid.toString(), ignored -> new HashMap<>())
|
||||
.put(dimension.identifier().toString(), position);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
private boolean isExcluded(ResourceKey<Level> dimension) {
|
||||
return excludedDimension != null && excludedDimension.equals(dimension);
|
||||
}
|
||||
|
||||
private void purgeExcludedDimension() {
|
||||
if (excludedDimension == null) return;
|
||||
|
||||
String dimensionId = excludedDimension.identifier().toString();
|
||||
boolean changed = false;
|
||||
|
||||
for (Map<String, PlayerPosition> byDimension : positions.values()) {
|
||||
if (byDimension.remove(dimensionId) != null) changed = true;
|
||||
}
|
||||
if (positions.entrySet().removeIf(entry -> entry.getValue().isEmpty())) changed = true;
|
||||
|
||||
if (changed) {
|
||||
dirty = true;
|
||||
LobbySystemsMod.LOGGER.info("Removed persisted lobby positions for dimension {}.", dimensionId);
|
||||
}
|
||||
}
|
||||
|
||||
private record Snapshot(ResourceKey<Level> dimension, PlayerPosition position) {
|
||||
static Snapshot from(ServerPlayer player) {
|
||||
return new Snapshot(
|
||||
player.level().dimension(),
|
||||
new PlayerPosition(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package id.dartokloning.lobbysystems;
|
||||
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class SessionTracker {
|
||||
private final Map<UUID, ResourceKey<Level>> lastDimension = new HashMap<>();
|
||||
private final Map<UUID, Long> gameEnteredAtMillis = new HashMap<>();
|
||||
|
||||
public void onJoin(UUID id, ResourceKey<Level> dimension) {
|
||||
lastDimension.put(id, dimension);
|
||||
gameEnteredAtMillis.remove(id);
|
||||
}
|
||||
|
||||
public void onDisconnect(UUID id) {
|
||||
lastDimension.remove(id);
|
||||
gameEnteredAtMillis.remove(id);
|
||||
}
|
||||
|
||||
public void update(UUID id, ResourceKey<Level> current, ResourceKey<Level> lobby) {
|
||||
ResourceKey<Level> previous = lastDimension.put(id, current);
|
||||
if (previous == null || previous.equals(current)) return;
|
||||
if (current.equals(lobby)) {
|
||||
gameEnteredAtMillis.remove(id);
|
||||
} else {
|
||||
gameEnteredAtMillis.put(id, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
public long remainingSeconds(UUID id, int minimumSeconds) {
|
||||
Long enteredAt = gameEnteredAtMillis.get(id);
|
||||
if (enteredAt == null) return minimumSeconds;
|
||||
long elapsed = Math.max(0L, (System.currentTimeMillis() - enteredAt) / 1000L);
|
||||
return Math.max(0L, minimumSeconds - elapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
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) {
|
||||
return lockedUntilTick.getOrDefault(player.getUUID(), 0L) > currentTick;
|
||||
}
|
||||
|
||||
public boolean teleport(ServerPlayer player, ServerLevel target,
|
||||
double x, double y, double z, float yaw, float pitch) {
|
||||
if (isLocked(player)) {
|
||||
player.sendSystemMessage(Component.literal("§eTeleport sebelumnya masih diproses. Tunggu sebentar lalu coba lagi."));
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
lockedUntilTick.put(player.getUUID(), currentTick + TELEPORT_LOCK_TICKS);
|
||||
|
||||
TeleportTransition transition = new TeleportTransition(
|
||||
target,
|
||||
new Vec3(x, y, z),
|
||||
Vec3.ZERO,
|
||||
yaw,
|
||||
pitch,
|
||||
Set.<Relative>of(),
|
||||
TeleportTransition.DO_NOTHING
|
||||
);
|
||||
|
||||
LobbySystemsMod.LOGGER.info(
|
||||
"Teleporting {} ({}): {} -> {} @ [{}, {}, {}]",
|
||||
player.getName().getString(), player.getUUID(),
|
||||
from.identifier(), to.identifier(),
|
||||
String.format("%.2f", x), String.format("%.2f", y), String.format("%.2f", z)
|
||||
);
|
||||
|
||||
ServerPlayer result = player.teleport(transition);
|
||||
if (result == null) {
|
||||
lockedUntilTick.remove(player.getUUID());
|
||||
LobbySystemsMod.LOGGER.warn("Teleport failed for {}: {} -> {}", player.getUUID(), from.identifier(), to.identifier());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface LobbyTeleportHandler {
|
||||
void teleport(ServerPlayer player, MinecraftServer server);
|
||||
}
|
||||
|
||||
private record PendingLobbyTeleport(long executeAtTick) {}
|
||||
}
|
||||
Reference in New Issue
Block a user