Add dimension gamemode and read gamerule list

This commit is contained in:
Darto KLoning
2026-09-06 15:55:55 +07:00
parent db84a0e4dd
commit 5358bf9a8e
6 changed files with 429 additions and 12 deletions
+63 -10
View File
@@ -1,8 +1,8 @@
# Lobby Systems v1.2.2 — Fabric 26.2
# Lobby Systems v1.3.0 — Fabric 26.2
Server-side lobby routing untuk Minecraft 26.2/Fabric. Versi 1.2.2 menambahkan portal antar-dimensi berbasis WorldEdit selection, last-position per player/per dimension, minimum playtime sebelum kembali ke lobby, dan konfigurasi terpusat di `config/lobby/`.
Server-side lobby routing untuk Minecraft 26.2/Fabric. Versi 1.3.0 menambahkan viewer gamerule per-dimension dengan pagination dan pengelolaan gamemode player berdasarkan dimension, sambil mempertahankan portal antar-dimensi, last-position, minimum playtime, dan konfigurasi terpusat di `config/lobby/`.
### Reload feedback v1.2.2
### Reload feedback
`/ls reload` menulis banner `RELOAD START` dan `RELOAD COMPLETE` hanya ke console server. Ringkasan reload satu baris dikirim langsung hanya ke player admin/operator yang menjalankan command; ringkasan tersebut tidak dibroadcast ke player lain dan tidak dikirim melalui command feedback ke console.
@@ -38,7 +38,7 @@ chmod +x compile.sh release.sh
```text
============================================
Lobby Systems - Fabric 26.2 Build
Version: 1.2.2
Version: 1.3.0
Namespace: id.dartokloning.lobbysystems
Mod ID: dartokloning
============================================
@@ -47,7 +47,7 @@ chmod +x compile.sh release.sh
Output utama:
```text
build/libs/lobby-systems-1.2.2.jar
build/libs/lobby-systems-1.3.0.jar
```
`*-sources.jar` hanya source archive dan tidak perlu dipasang ke server.
@@ -78,21 +78,22 @@ State penting Lobby Systems memakai separator agar mudah terlihat di server cons
```text
============================================
Lobby Systems - READY
Version: 1.2.2
Version: 1.3.0
Minecraft: 26.2
Config directory: config/lobby
WorldEdit: available
============================================
```
## Struktur konfigurasi v1.2.2
## Struktur konfigurasi v1.3.0
```text
config/
└── lobby/
├── config.json
├── player-positions.json
└── portals.json
├── portals.json
└── dimension-gamemodes.json
```
### `config/lobby/config.json`
@@ -151,9 +152,26 @@ Portal dibuat lewat command dan tersimpan otomatis. Contoh:
Portal adalah region/cuboid, bukan harus Nether Portal asli. Anda bebas membuat bentuk doorway, arch, lantai, gateway, dan sebagainya di dalam region tersebut.
### `config/lobby/dimension-gamemodes.json`
Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems v1.3.0 menambahkan mapping dimension → gamemode dan menerapkannya ketika player masuk ke dimension tersebut. File ini dibuat otomatis dan dapat dikelola lewat command admin.
Contoh:
```json
{
"minecraft:lobby": "adventure",
"minecraft:worldcreative": "creative",
"minecraft:overworld": "survival"
}
```
Gamemode yang didukung: `survival`, `creative`, `adventure`, dan `spectator`.
## Migrasi dari v1.1.x
Pada startup pertama v1.2.2:
Pada startup pertama v1.3.0:
```text
config/lobby-systems.json
@@ -177,7 +195,7 @@ akan dimigrasikan ke:
config/lobby/player-positions.json
```
Nama file lama hanya digunakan oleh `LegacyConfigMigrator` untuk proses migrasi. Runtime v1.2.2 membaca dan menulis file baru di `config/lobby/`.
Nama file lama hanya digunakan oleh `LegacyConfigMigrator` untuk proses migrasi. Runtime v1.3.0 membaca dan menulis file baru di `config/lobby/`.
Jika file tujuan baru sudah ada, migrator tidak menimpanya.
@@ -198,6 +216,14 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya.
/ls reload
/ls tp <dimension>
/ls gamerule <dimension>
/ls gamerule <dimension> --page <page>
/ls gamemode <dimension>
/ls gamemode list
/ls gamemode set <dimension> <survival|creative|adventure|spectator>
/ls gamemode remove <dimension>
/ls portal create <name> <target_dimension>
/ls portal delete <name>
/ls portal list
@@ -207,12 +233,39 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya.
`/ls tp` tetap command maintenance/admin. Player biasa tidak memerlukannya karena flow normal menggunakan portal.
### Gamerule viewer per dimension
`/ls gamerule <dimension>` menampilkan gamerule aktual dari dimension yang sedang ter-load. Viewer ini bersifat read-only dan cocok dipakai bersama WorldGameRules. Hasil ditampilkan 10 rule per halaman.
Contoh:
```text
/ls gamerule minecraft:lobby
/ls gamerule minecraft:lobby --page 2
```
Header chat menampilkan `halaman/total halaman`, nama dimension, dan jumlah gamerule. Jika masih ada halaman berikutnya, command untuk membuka halaman selanjutnya ikut ditampilkan.
### Gamemode per dimension
Gunakan mapping gamemode agar player otomatis mendapat gamemode yang sesuai saat memasuki dimension. Contoh:
```text
/ls gamemode set minecraft:lobby adventure
/ls gamemode set minecraft:worldcreative creative
/ls gamemode set minecraft:overworld survival
```
`/ls gamemode <dimension>` melihat mapping satu dimension, `/ls gamemode list` melihat semua mapping, dan `/ls gamemode remove <dimension>` menghapus mapping. Mapping disimpan ke `config/lobby/dimension-gamemodes.json` dan ikut direload oleh `/ls reload`.
### `/ls reload`
Reload live:
- `config/lobby/config.json`
- `config/lobby/portals.json`
- `config/lobby/dimension-gamemodes.json`
- lobby exclusion pada player-position store
Tidak perlu restart untuk perubahan JSON tersebut.
+1 -1
View File
@@ -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.2.2
mod_version=1.3.0
maven_group=id.dartokloning
archives_base_name=lobby-systems
@@ -9,6 +9,7 @@ public final class ConfigPaths {
public static final Path CONFIG = DIRECTORY.resolve("config.json");
public static final Path PLAYER_POSITIONS = DIRECTORY.resolve("player-positions.json");
public static final Path PORTALS = DIRECTORY.resolve("portals.json");
public static final Path DIMENSION_GAMEMODES = DIRECTORY.resolve("dimension-gamemodes.json");
private ConfigPaths() {}
}
@@ -0,0 +1,164 @@
package id.dartokloning.lobbysystems;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.GameType;
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.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Mengelola gamemode yang diterapkan ketika player memasuki dimension tertentu.
* Minecraft menyimpan gamemode pada player, bukan pada dimension, jadi Lobby Systems
* menerapkan mapping dimension -> gamemode setiap kali perpindahan dimension terdeteksi.
*/
public final class DimensionGameModeManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Type MAP_TYPE = new TypeToken<LinkedHashMap<String, String>>() {}.getType();
private static final Path PATH = ConfigPaths.DIMENSION_GAMEMODES;
private final Map<String, String> configured = new LinkedHashMap<>();
private final Map<UUID, String> lastDimension = new ConcurrentHashMap<>();
public boolean load() {
if (!Files.exists(PATH)) {
save();
return true;
}
try (Reader reader = Files.newBufferedReader(PATH)) {
Map<String, String> loaded = GSON.fromJson(reader, MAP_TYPE);
configured.clear();
if (loaded != null) {
for (Map.Entry<String, String> entry : loaded.entrySet()) {
String dimension = normalizeDimension(entry.getKey());
String mode = normalizeMode(entry.getValue());
if (dimension.isBlank()) {
LobbySystemsMod.LOGGER.warn("Skipping blank dimension in {}", PATH);
continue;
}
if (parseMode(mode) == null) {
LobbySystemsMod.LOGGER.warn("Skipping invalid gamemode '{}' for dimension '{}' in {}", entry.getValue(), dimension, PATH);
continue;
}
configured.put(dimension, mode);
}
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to load {}. Keeping previous dimension gamemode config.", PATH, e);
return false;
}
}
public boolean save() {
try {
Files.createDirectories(PATH.getParent());
try (Writer writer = Files.newBufferedWriter(PATH)) {
GSON.toJson(configured, MAP_TYPE, writer);
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to save {}", PATH, e);
return false;
}
}
public void tick(MinecraftServer server) {
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
String dimension = player.level().dimension().identifier().toString();
String previous = lastDimension.put(player.getUUID(), dimension);
if (!dimension.equals(previous)) {
applyForPlayer(player, dimension, true);
}
}
}
public void onDisconnect(UUID uuid) {
lastDimension.remove(uuid);
}
public String get(String dimension) {
return configured.get(normalizeDimension(dimension));
}
public Map<String, String> all() {
return Map.copyOf(configured);
}
public boolean set(String dimension, String mode) {
String normalizedDimension = normalizeDimension(dimension);
String normalizedMode = normalizeMode(mode);
if (normalizedDimension.isBlank() || parseMode(normalizedMode) == null) return false;
configured.put(normalizedDimension, normalizedMode);
return save();
}
public boolean remove(String dimension) {
String key = normalizeDimension(dimension);
if (configured.remove(key) == null) return false;
save();
return true;
}
public int applyToDimension(MinecraftServer server, String dimension) {
int changed = 0;
String normalized = normalizeDimension(dimension);
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
if (player.level().dimension().identifier().toString().equals(normalized)) {
if (applyForPlayer(player, normalized, false)) changed++;
lastDimension.put(player.getUUID(), normalized);
}
}
return changed;
}
private boolean applyForPlayer(ServerPlayer player, String dimension, boolean log) {
String configuredMode = configured.get(normalizeDimension(dimension));
if (configuredMode == null) return false;
GameType gameType = parseMode(configuredMode);
if (gameType == null) return false;
boolean changed = player.setGameMode(gameType);
if (log && changed) {
LobbyLog.state(
"DIMENSION GAMEMODE APPLIED",
"Player: " + player.getName().getString(),
"Dimension: " + dimension,
"Gamemode: " + configuredMode
);
}
return changed;
}
public static GameType parseMode(String raw) {
if (raw == null) return null;
return switch (raw.trim().toLowerCase()) {
case "survival" -> GameType.SURVIVAL;
case "creative" -> GameType.CREATIVE;
case "adventure" -> GameType.ADVENTURE;
case "spectator" -> GameType.SPECTATOR;
default -> null;
};
}
private static String normalizeMode(String raw) {
return raw == null ? "" : raw.trim().toLowerCase();
}
private static String normalizeDimension(String raw) {
return raw == null ? "" : raw.trim().toLowerCase();
}
}
@@ -1,5 +1,6 @@
package id.dartokloning.lobbysystems;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
@@ -9,6 +10,7 @@ import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
@@ -17,9 +19,15 @@ 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.gamerules.GameRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public final class LobbySystemsMod implements ModInitializer {
public static final String MOD_ID = "dartokloning";
public static final Logger LOGGER = LoggerFactory.getLogger("Lobby Systems");
@@ -29,6 +37,7 @@ public final class LobbySystemsMod implements ModInitializer {
private static final PositionManager POSITIONS = new PositionManager();
private static final PortalManager PORTALS = new PortalManager();
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
private static int saveTicker;
@Override
@@ -59,6 +68,13 @@ public final class LobbySystemsMod implements ModInitializer {
"Portals: " + PORTALS.size()
);
boolean gameModesLoaded = DIMENSION_GAMEMODES.load();
LobbyLog.state(
gameModesLoaded ? "DIMENSION GAMEMODES LOADED" : "DIMENSION GAMEMODES LOAD FAILED",
"File: " + ConfigPaths.DIMENSION_GAMEMODES,
"Configured dimensions: " + DIMENSION_GAMEMODES.all().size()
);
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
ServerPlayer player = listener.getPlayer();
server.execute(() -> {
@@ -79,6 +95,7 @@ public final class LobbySystemsMod implements ModInitializer {
SESSIONS.onDisconnect(player.getUUID());
TELEPORTS.onDisconnect(player.getUUID());
PORTALS.onDisconnect(player.getUUID());
DIMENSION_GAMEMODES.onDisconnect(player.getUUID());
LobbyLog.state(
"PLAYER DISCONNECT",
"Player: " + player.getName().getString(),
@@ -94,6 +111,8 @@ public final class LobbySystemsMod implements ModInitializer {
}
});
DIMENSION_GAMEMODES.tick(server);
ResourceKey<Level> lobby = lobbyKey();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
POSITIONS.update(player);
@@ -128,6 +147,50 @@ public final class LobbySystemsMod implements ModInitializer {
StringArgumentType.getString(ctx, "dimension"),
true,
true))))
.then(Commands.literal("gamerule")
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> listGameRules(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "dimension"),
1))
.then(Commands.literal("--page")
.then(Commands.argument("page", IntegerArgumentType.integer(1))
.executes(ctx -> listGameRules(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "dimension"),
IntegerArgumentType.getInteger(ctx, "page")))))))
.then(Commands.literal("gamemode")
.then(Commands.literal("list")
.executes(ctx -> listDimensionGameModes(ctx.getSource())))
.then(Commands.literal("set")
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggest(
new String[]{"survival", "creative", "adventure", "spectator"}, builder))
.executes(ctx -> setDimensionGameMode(
ctx.getSource(),
ctx.getSource().getServer(),
StringArgumentType.getString(ctx, "dimension"),
StringArgumentType.getString(ctx, "mode"))))))
.then(Commands.literal("remove")
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> removeDimensionGameMode(
ctx.getSource(),
StringArgumentType.getString(ctx, "dimension")))))
.then(Commands.argument("dimension", StringArgumentType.word())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.executes(ctx -> showDimensionGameMode(
ctx.getSource(),
StringArgumentType.getString(ctx, "dimension")))))
.then(Commands.literal("portal")
.then(Commands.literal("create")
.then(Commands.argument("name", StringArgumentType.word())
@@ -226,6 +289,16 @@ public final class LobbySystemsMod implements ModInitializer {
return 0;
}
boolean gameModesOk = DIMENSION_GAMEMODES.load();
if (!gameModesOk) {
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension-gamemodes.json bermasalah. Cek console."));
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 (source.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(Component.literal(
"§aLobby Systems direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
@@ -237,11 +310,137 @@ 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(),
"Player positions lobby exclusion: active"
);
return 1;
}
private static void sendCommandMessage(CommandSourceStack source, Component message) {
if (source.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(message);
} else {
source.sendSuccess(() -> message, false);
}
}
private static final int GAMERULES_PER_PAGE = 10;
private static int listGameRules(CommandSourceStack source, MinecraftServer server, String dimensionId, int requestedPage) {
ServerLevel level = findLevel(server, dimensionId);
if (level == null) {
source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
return 0;
}
List<GameRuleEntry> entries = new ArrayList<>();
for (GameRule<?> rule : BuiltInRegistries.GAME_RULE) {
Identifier id = BuiltInRegistries.GAME_RULE.getKey(rule);
if (id == null) continue;
entries.add(new GameRuleEntry(id.toString(), gameRuleValue(level, rule)));
}
entries.sort(Comparator.comparing(GameRuleEntry::name));
int totalPages = Math.max(1, (entries.size() + GAMERULES_PER_PAGE - 1) / GAMERULES_PER_PAGE);
int page = Math.min(Math.max(1, requestedPage), totalPages);
int start = (page - 1) * GAMERULES_PER_PAGE;
int end = Math.min(start + GAMERULES_PER_PAGE, entries.size());
sendCommandMessage(source, Component.literal(
"§6§lLobby Systems Gamerules §r§7[§f" + page + "§7/§f" + totalPages + "§7]"
+ "\n§7Dimension: §f" + level.dimension().identifier()
+ " §8| §7Rules: §f" + entries.size()));
for (int i = start; i < end; i++) {
GameRuleEntry entry = entries.get(i);
String valueColor = "true".equals(entry.value()) ? "§a" : "false".equals(entry.value()) ? "§c" : "§e";
sendCommandMessage(source, Component.literal("§8• §f" + entry.name() + " §7= " + valueColor + entry.value()));
}
if (page < totalPages) {
int next = page + 1;
sendCommandMessage(source, Component.literal(
"§7Halaman selanjutnya: §f/ls gamerule " + level.dimension().identifier() + " --page " + next));
}
return entries.size();
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static String gameRuleValue(ServerLevel level, GameRule<?> rule) {
Object value = level.getGameRules().get((GameRule) rule);
return String.valueOf(value);
}
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<none>"
+ "\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<String, String> all = DIMENSION_GAMEMODES.all();
if (all.isEmpty()) {
sendCommandMessage(source, Component.literal(
"§eBelum ada gamemode dimension yang dikonfigurasi.\n§7Gunakan §f/ls gamemode set <dimension> <mode>"));
return 1;
}
sendCommandMessage(source, Component.literal("§6§lLobby Systems Dimension Gamemodes §r§7(" + all.size() + ")"));
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) {
ServerLevel level = findLevel(server, dimensionId);
if (level == null) {
source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
return 0;
}
if (DimensionGameModeManager.parseMode(rawMode) == null) {
source.sendFailure(Component.literal("§cGamemode tidak valid. Pilih survival, creative, adventure, atau spectator."));
return 0;
}
String normalizedDimension = level.dimension().identifier().toString();
String normalizedMode = rawMode.trim().toLowerCase();
if (!DIMENSION_GAMEMODES.set(normalizedDimension, normalizedMode)) {
source.sendFailure(Component.literal("§cGagal menyimpan dimension-gamemodes.json. Cek console."));
return 0;
}
int applied = DIMENSION_GAMEMODES.applyToDimension(server, normalizedDimension);
sendCommandMessage(source, Component.literal(
"§aGamemode dimension disimpan. §7" + normalizedDimension + " §8→ §f" + normalizedMode
+ " §7| Player diterapkan sekarang: §f" + applied));
LobbyLog.state(
"DIMENSION GAMEMODE UPDATED",
"Dimension: " + normalizedDimension,
"Gamemode: " + normalizedMode,
"Players applied now: " + applied
);
return 1;
}
private static int removeDimensionGameMode(CommandSourceStack source, String dimensionId) {
if (!DIMENSION_GAMEMODES.remove(dimensionId)) {
source.sendFailure(Component.literal("§cTidak ada mapping gamemode untuk dimension '" + dimensionId + "'."));
return 0;
}
sendCommandMessage(source, Component.literal(
"§aMapping gamemode dimension dihapus: §f" + dimensionId
+ "\n§7Player tidak diubah sampai berpindah dimension atau mapping baru dibuat."));
LobbyLog.state("DIMENSION GAMEMODE REMOVED", "Dimension: " + dimensionId);
return 1;
}
private record GameRuleEntry(String name, String value) {}
private static int createPortal(ServerPlayer player, MinecraftServer server, String rawName, String rawTarget) {
String name = PortalManager.normalizeName(rawName);
if (!name.matches("[a-z0-9_-]+")) {
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "dartokloning",
"version": "${version}",
"name": "Lobby Systems",
"description": "Server-side Fabric lobby routing, persistent world positions, minimum playtime, and WorldEdit-selected dimension portals.",
"description": "Server-side Fabric lobby routing, persistent world positions, minimum playtime, portals, gamerule inspection, and per-dimension player gamemodes.",
"authors": ["DartoKloning"],
"license": "MIT",
"environment": "server",