Add gamerule per dimension

This commit is contained in:
Darto KLoning
2026-09-06 19:31:59 +07:00
parent e037fadfc9
commit ba4fc88538
11 changed files with 598 additions and 40 deletions
+10
View File
@@ -0,0 +1,10 @@
# Normalize line endings across Windows, Linux, and macOS
* text=auto
*.bat text eol=crlf
*.sh text eol=lf
*.java text eol=lf
*.json text eol=lf
*.md text eol=lf
*.gradle text eol=lf
*.properties text eol=lf
+35 -13
View File
@@ -1,8 +1,8 @@
# Lobby Systems v1.3.3 — Fabric 26.2
# Lobby Systems v1.4.0 — Fabric 26.2
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/`.
Server-side lobby routing untuk Minecraft 26.2/Fabric. v1.4.0 menambahkan **native persistent per-dimension gamerules** milik Lobby Systems, tetap mempertahankan portal antar-dimensi, last-position, minimum playtime, gamerule viewer ber-pagination, serta per-dimension player gamemode.
**v1.3.3 bugfix:** viewer gamerule sekarang membaca nilai gamerule Minecraft 26.x secara aman per-rule, melewati rule yang tidak aktif/tidak tersedia untuk feature set dimension, dan tidak lagi menggagalkan seluruh command ketika satu rule bermasalah. Versi ini juga mengganti lisensi project menjadi proprietary **All Rights Reserved** dengan penggunaan hanya berdasarkan izin tertulis dari DartoKloning.
**v1.4.0 feature release:** gamerule tidak lagi bergantung pada WorldGameRules. Lobby Systems membuat instance `GameRules` terpisah untuk setiap `ServerLevel`, menyimpan override ke `config/lobby/dimension-gamerules.json`, lalu menerapkannya kembali saat world load, server start, dan `/ls reload`. WorldGameRules harus dihapus karena kedua mod mengubah mekanisme gamerule per-world.
### Reload feedback
@@ -40,7 +40,7 @@ chmod +x compile.sh release.sh
```text
============================================
Lobby Systems - Fabric 26.2 Build
Version: 1.3.3
Version: 1.4.0
Namespace: id.dartokloning.lobbysystems
Mod ID: dartokloning
============================================
@@ -49,7 +49,7 @@ chmod +x compile.sh release.sh
Output utama:
```text
build/libs/lobby-systems-1.3.3.jar
build/libs/lobby-systems-1.4.0.jar
```
`*-sources.jar` hanya source archive dan tidak perlu dipasang ke server.
@@ -80,14 +80,14 @@ State penting Lobby Systems memakai separator agar mudah terlihat di server cons
```text
============================================
Lobby Systems - READY
Version: 1.3.3
Version: 1.4.0
Minecraft: 26.2
Config directory: config/lobby
WorldEdit: available
============================================
```
## Struktur konfigurasi v1.3.3
## Struktur konfigurasi v1.4.0
```text
config/
@@ -95,7 +95,8 @@ config/
├── config.json
├── player-positions.json
├── portals.json
└── dimension-gamemodes.json
├── dimension-gamemodes.json
└── dimension-gamerules.json
```
### `config/lobby/config.json`
@@ -157,7 +158,7 @@ Portal adalah region/cuboid, bukan harus Nether Portal asli. Anda bebas membuat
### `config/lobby/dimension-gamemodes.json`
Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems v1.3.3 menambahkan mapping dimension → gamemode dan menerapkannya ketika player masuk ke dimension tersebut. File ini dibuat otomatis dan dapat dikelola lewat command admin.
Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems v1.4.0 menambahkan mapping dimension → gamemode dan menerapkannya ketika player masuk ke dimension tersebut. File ini dibuat otomatis dan dapat dikelola lewat command admin.
Contoh:
@@ -173,7 +174,7 @@ Gamemode yang didukung: `survival`, `creative`, `adventure`, dan `spectator`.
## Migrasi dari v1.1.x
Pada startup pertama v1.3.3:
Pada startup pertama v1.4.0:
```text
config/lobby-systems.json
@@ -197,7 +198,7 @@ akan dimigrasikan ke:
config/lobby/player-positions.json
```
Nama file lama hanya digunakan oleh `LegacyConfigMigrator` untuk proses migrasi. Runtime v1.3.3 membaca dan menulis file baru di `config/lobby/`.
Nama file lama hanya digunakan oleh `LegacyConfigMigrator` untuk proses migrasi. Runtime v1.4.0 membaca dan menulis file baru di `config/lobby/`.
Jika file tujuan baru sudah ada, migrator tidak menimpanya.
@@ -220,6 +221,8 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya.
/ls gamerule <dimension>
/ls gamerule <dimension> --page <page>
/ls gamerule set <dimension> <rule> <value>
/ls gamerule reset <dimension> <rule>
/ls gamemode <dimension>
/ls gamemode list
@@ -236,9 +239,27 @@ 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
### Gamerule 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.
`/ls gamerule <dimension>` menampilkan gamerule aktual dari dimension yang sedang ter-load. Hasil ditampilkan 10 rule per halaman dan dipisahkan separator agar lebih mudah dibaca di chat.
Untuk perubahan yang harus **persist setelah restart**, gunakan `/ls gamerule set ...`. Perintah vanilla `/gamerule` dapat mengubah nilai runtime yang terlihat pada dimension aktif, tetapi perubahan tersebut tidak ditulis ke `dimension-gamerules.json`, sehingga bukan source of truth Lobby Systems.
> **Catatan Minecraft 26.2:** mayoritas gamerule gameplay server-side dapat diisolasi melalui `ServerLevel#getGameRules()`. Rule yang berhubungan dengan clock/weather seperti `minecraft:advance_time` dan `minecraft:advance_weather` memiliki mekanisme sinkronisasi tambahan di 26.x; uji keduanya secara khusus sebelum mengandalkannya untuk world yang membutuhkan waktu/cuaca benar-benar independen.
Gunakan command berikut untuk membuat override persistent:
```text
/ls gamerule set minecraft:lobby minecraft:spawn_mobs false
/ls gamerule set minecraft:lobby minecraft:mob_griefing false
/ls gamerule set minecraft:lobby minecraft:keep_inventory true
```
Untuk menghapus override dan mengembalikan rule ke default Minecraft:
```text
/ls gamerule reset minecraft:lobby minecraft:spawn_mobs
```
Contoh:
@@ -268,6 +289,7 @@ Reload live:
- `config/lobby/config.json`
- `config/lobby/portals.json`
- `config/lobby/dimension-gamemodes.json`
- `config/lobby/dimension-gamerules.json`
- lobby exclusion pada player-position store
Tidak perlu restart untuk perubahan JSON tersebut.
+53 -10
View File
@@ -179,9 +179,12 @@ if defined HAS_CHANGES (
git status --short
echo.
call :suggest_commit_message
echo [SUGGEST] Commit message: !SUGGESTED_COMMIT!
echo [INFO] Tekan Enter untuk memakai saran tersebut, atau ketik pesan sendiri.
set "COMMIT_MESSAGE="
set /p "COMMIT_MESSAGE=Commit message [Lobby Systems v%MOD_VERSION% build]: "
if not defined COMMIT_MESSAGE set "COMMIT_MESSAGE=Lobby Systems v%MOD_VERSION% build"
set /p "COMMIT_MESSAGE=Commit message [!SUGGESTED_COMMIT!]: "
if not defined COMMIT_MESSAGE set "COMMIT_MESSAGE=!SUGGESTED_COMMIT!"
echo.
echo [INFO] Menambahkan perubahan ke staging...
@@ -217,18 +220,57 @@ if errorlevel 1 (
goto :git_failed
)
echo [INFO] Push ke origin/!CURRENT_BRANCH!...
git push origin "!CURRENT_BRANCH!"
if errorlevel 1 (
echo [ERROR] Git push gagal.
echo [INFO] Commit lokal tetap tersimpan.
goto :git_failed
)
call :push_branch_with_retry
if errorlevel 1 goto :git_failed
echo.
echo [SUCCESS] Push ke Git/Gitea berhasil.
goto :success
:suggest_commit_message
set "SUGGESTED_COMMIT=Update Lobby Systems v%MOD_VERSION%"
set "HAS_SRC_CHANGE="
set "HAS_SCRIPT_CHANGE="
set "HAS_DOC_CHANGE="
for /f "delims=" %%F in ('git status --porcelain') do (
set "LINE=%%F"
echo !LINE! | findstr /I /C:"src/" >nul && set "HAS_SRC_CHANGE=1"
echo !LINE! | findstr /I /C:"compile.bat" /C:"compile.sh" /C:"release.bat" /C:"release.sh" >nul && set "HAS_SCRIPT_CHANGE=1"
echo !LINE! | findstr /I /C:"README.md" /C:"LICENSE" >nul && set "HAS_DOC_CHANGE=1"
)
if defined HAS_SRC_CHANGE set "SUGGESTED_COMMIT=Update Lobby Systems v%MOD_VERSION%"
if not defined HAS_SRC_CHANGE if defined HAS_SCRIPT_CHANGE set "SUGGESTED_COMMIT=Improve build and Git workflow"
if not defined HAS_SRC_CHANGE if not defined HAS_SCRIPT_CHANGE if defined HAS_DOC_CHANGE set "SUGGESTED_COMMIT=Update Lobby Systems documentation"
exit /b 0
:push_branch_with_retry
set /a PUSH_ROUND=1
:push_round_start
set /a PUSH_ATTEMPT=1
:push_attempt_start
echo.
echo [INFO] Push attempt !PUSH_ATTEMPT!/3 ^(round !PUSH_ROUND!^) ke origin/!CURRENT_BRANCH!...
git push origin "!CURRENT_BRANCH!"
if not errorlevel 1 exit /b 0
echo [WARN] Push attempt !PUSH_ATTEMPT!/3 gagal.
if !PUSH_ATTEMPT! LSS 3 (
set /a PUSH_ATTEMPT+=1
echo [INFO] Mencoba lagi dalam 2 detik...
timeout /t 2 /nobreak >nul
goto :push_attempt_start
)
echo.
echo ============================================
echo GIT PUSH FAILED - 3 ATTEMPTS
echo ============================================
echo [INFO] Commit lokal tetap aman dan tidak perlu compile ulang.
choice /C YN /N /M "Coba push 3x lagi? [Y/N]: "
if errorlevel 2 exit /b 1
set /a PUSH_ROUND+=1
goto :push_round_start
:skip_git
echo.
echo [INFO] Git commit/push dilewati.
@@ -263,7 +305,8 @@ echo GIT FAILED
echo ============================================
echo.
echo [INFO] Build Lobby Systems tetap berhasil.
echo [INFO] Hanya proses Git yang mengalami kegagalan.
echo [INFO] Commit lokal tetap tersimpan.
echo [INFO] Anda bisa menjalankan compile.bat lagi dan memilih Y; jika tidak ada perubahan, script langsung mencoba push commit yang tertunda.
echo.
pause
exit /b 2
+59 -13
View File
@@ -30,6 +30,54 @@ read_property() {
grep -E "^${key}=" "$GRADLE_PROPERTIES" | head -n 1 | cut -d'=' -f2- | tr -d '\r'
}
suggest_commit_message() {
local status
status="$(git status --porcelain)"
SUGGESTED_COMMIT="Update Lobby Systems v$MOD_VERSION"
if grep -qiE '(^|[[:space:]])src/' <<<"$status"; then
SUGGESTED_COMMIT="Update Lobby Systems v$MOD_VERSION"
elif grep -qiE 'compile\.(bat|sh)|release\.(bat|sh)' <<<"$status"; then
SUGGESTED_COMMIT="Improve build and Git workflow"
elif grep -qiE 'README\.md|LICENSE' <<<"$status"; then
SUGGESTED_COMMIT="Update Lobby Systems documentation"
fi
}
push_branch_with_retry() {
local round=1
local attempt
while true; do
attempt=1
while [[ $attempt -le 3 ]]; do
echo
info "Push attempt $attempt/3 (round $round) ke origin/$CURRENT_BRANCH..."
if git push origin "$CURRENT_BRANCH"; then
return 0
fi
warn "Push attempt $attempt/3 gagal."
if [[ $attempt -lt 3 ]]; then
info "Mencoba lagi dalam 2 detik..."
sleep 2
fi
attempt=$((attempt + 1))
done
echo
separator
echo " GIT PUSH FAILED - 3 ATTEMPTS"
separator
info "Commit lokal tetap aman dan tidak perlu compile ulang."
read -r -p "Coba push 3x lagi? [Y/N]: " RETRY_CONFIRM
case "$RETRY_CONFIRM" in
y|Y|yes|YES|Yes) round=$((round + 1)) ;;
*) return 1 ;;
esac
done
}
if [[ ! -f "$GRADLE_PROPERTIES" ]]; then
error "gradle.properties tidak ditemukan."
exit 1
@@ -122,7 +170,6 @@ if [[ ! -x "$GRADLE_BIN" ]]; then
fi
ok "Gradle $GRADLE_VERSION tersedia."
info "Menjalankan clean build Lobby Systems v$MOD_VERSION..."
echo
@@ -155,14 +202,12 @@ else
echo "$JAR_PATH"
fi
# release.sh memakai mode ini supaya build tidak menampilkan prompt Git dua kali.
if [[ $BUILD_ONLY -eq 1 ]]; then
exit 0
fi
echo
read -r -p "Commit dan push perubahan ke Git/Gitea? [Y/N]: " GIT_CONFIRM
case "$GIT_CONFIRM" in
y|Y|yes|YES|Yes) ;;
*)
@@ -208,12 +253,11 @@ if [[ -n "$CHANGES" ]]; then
git status --short
echo
read -r -p "Commit message [Lobby Systems v$MOD_VERSION build]: " COMMIT_MESSAGE
if [[ -z "$COMMIT_MESSAGE" ]]; then
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
COMMIT_MESSAGE="Lobby Systems v$MOD_VERSION build - $TIMESTAMP"
fi
suggest_commit_message
echo "[SUGGEST] Commit message: $SUGGESTED_COMMIT"
info "Tekan Enter untuk memakai saran tersebut, atau ketik pesan sendiri."
read -r -p "Commit message [$SUGGESTED_COMMIT]: " COMMIT_MESSAGE
[[ -z "$COMMIT_MESSAGE" ]] && COMMIT_MESSAGE="$SUGGESTED_COMMIT"
info "Menambahkan perubahan ke staging..."
if ! git add -A; then
@@ -232,7 +276,6 @@ else
fi
CURRENT_BRANCH="$(git branch --show-current)"
if [[ -z "$CURRENT_BRANCH" ]]; then
error "Tidak dapat menentukan branch Git aktif."
exit 2
@@ -252,12 +295,15 @@ fi
REMOTE_URL="$(git remote get-url origin)"
info "Remote origin: $REMOTE_URL"
info "Push ke origin/$CURRENT_BRANCH..."
if ! git push origin "$CURRENT_BRANCH"; then
if ! push_branch_with_retry; then
echo
error "Git push gagal."
separator
echo " GIT FAILED"
separator
info "Build Lobby Systems tetap berhasil."
info "Commit lokal tetap tersimpan."
info "Jalankan compile.sh lagi dan pilih Y; bila tidak ada perubahan, script langsung mencoba push commit yang tertunda."
exit 2
fi
+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.3.3
mod_version=1.4.0
maven_group=id.dartokloning
archives_base_name=lobby-systems
@@ -10,6 +10,7 @@ public final class ConfigPaths {
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");
public static final Path DIMENSION_GAMERULES = DIRECTORY.resolve("dimension-gamerules.json");
private ConfigPaths() {}
}
@@ -0,0 +1,201 @@
package id.dartokloning.lobbysystems;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.gamerules.GameRule;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Persistent per-dimension gamerule overrides owned by Lobby Systems.
*
* Values are stored in config/lobby/dimension-gamerules.json and are applied
* directly to each ServerLevel's independent GameRules instance.
*/
public final class DimensionGameRuleManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final Map<String, Map<String, String>> configured = new LinkedHashMap<>();
public boolean load() {
configured.clear();
try {
Files.createDirectories(ConfigPaths.DIRECTORY);
if (!Files.exists(ConfigPaths.DIMENSION_GAMERULES)) {
save();
return true;
}
String json = Files.readString(ConfigPaths.DIMENSION_GAMERULES, StandardCharsets.UTF_8);
if (json.isBlank()) {
save();
return true;
}
JsonElement rootElement = JsonParser.parseString(json);
if (!rootElement.isJsonObject()) {
LobbySystemsMod.LOGGER.error("dimension-gamerules.json root must be a JSON object");
return false;
}
for (Map.Entry<String, JsonElement> dimensionEntry : rootElement.getAsJsonObject().entrySet()) {
if (!dimensionEntry.getValue().isJsonObject()) {
LobbySystemsMod.LOGGER.warn("Ignoring gamerule config for '{}': expected an object", dimensionEntry.getKey());
continue;
}
Map<String, String> rules = new LinkedHashMap<>();
for (Map.Entry<String, JsonElement> ruleEntry : dimensionEntry.getValue().getAsJsonObject().entrySet()) {
JsonElement value = ruleEntry.getValue();
if (!value.isJsonPrimitive()) {
LobbySystemsMod.LOGGER.warn("Ignoring gamerule '{} / {}': value must be boolean, number, or string",
dimensionEntry.getKey(), ruleEntry.getKey());
continue;
}
rules.put(ruleEntry.getKey(), value.getAsString());
}
configured.put(dimensionEntry.getKey(), rules);
}
return true;
} catch (Exception e) {
LobbySystemsMod.LOGGER.error("Failed to load {}", ConfigPaths.DIMENSION_GAMERULES, e);
return false;
}
}
public boolean save() {
try {
Files.createDirectories(ConfigPaths.DIRECTORY);
JsonObject root = new JsonObject();
configured.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(dimensionEntry -> {
JsonObject rules = new JsonObject();
dimensionEntry.getValue().entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(ruleEntry -> {
String value = ruleEntry.getValue();
if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
rules.addProperty(ruleEntry.getKey(), Boolean.parseBoolean(value));
} else {
try {
rules.addProperty(ruleEntry.getKey(), Integer.parseInt(value));
} catch (NumberFormatException ignored) {
rules.addProperty(ruleEntry.getKey(), value);
}
}
});
root.add(dimensionEntry.getKey(), rules);
});
Files.writeString(ConfigPaths.DIMENSION_GAMERULES, GSON.toJson(root) + System.lineSeparator(), StandardCharsets.UTF_8);
return true;
} catch (IOException e) {
LobbySystemsMod.LOGGER.error("Failed to save {}", ConfigPaths.DIMENSION_GAMERULES, e);
return false;
}
}
public Map<String, Map<String, String>> all() {
Map<String, Map<String, String>> copy = new LinkedHashMap<>();
configured.forEach((dimension, rules) -> copy.put(dimension, Map.copyOf(rules)));
return Map.copyOf(copy);
}
public boolean set(String dimensionId, String ruleId, String serializedValue) {
configured.computeIfAbsent(dimensionId, ignored -> new LinkedHashMap<>()).put(ruleId, serializedValue);
return save();
}
public boolean remove(String dimensionId, String ruleId) {
Map<String, String> rules = configured.get(dimensionId);
if (rules == null || rules.remove(ruleId) == null) {
return false;
}
if (rules.isEmpty()) {
configured.remove(dimensionId);
}
return save();
}
public ApplySummary applyAll(MinecraftServer server) {
int dimensions = 0;
int applied = 0;
int skipped = 0;
for (Map.Entry<String, Map<String, String>> dimensionEntry : configured.entrySet()) {
ServerLevel level = LobbySystemsMod.findLevelForManager(server, dimensionEntry.getKey());
if (level == null) {
skipped += dimensionEntry.getValue().size();
continue;
}
dimensions++;
ApplySummary summary = applyToLevel(level);
applied += summary.applied();
skipped += summary.skipped();
}
return new ApplySummary(dimensions, applied, skipped);
}
public ApplySummary applyToLevel(ServerLevel level) {
Map<String, String> rules = configured.get(level.dimension().identifier().toString());
if (rules == null || rules.isEmpty()) {
return new ApplySummary(1, 0, 0);
}
int applied = 0;
int skipped = 0;
for (Map.Entry<String, String> entry : rules.entrySet()) {
GameRule<?> rule = findRule(entry.getKey());
if (rule == null || !rule.isEnabled(level.enabledFeatures())) {
LobbySystemsMod.LOGGER.warn("Skipping configured gamerule '{}' for '{}': rule unavailable",
entry.getKey(), level.dimension().identifier());
skipped++;
continue;
}
if (applySerialized(level, rule, entry.getValue())) {
applied++;
} else {
LobbySystemsMod.LOGGER.warn("Skipping configured gamerule '{}'='{}' for '{}': invalid value",
entry.getKey(), entry.getValue(), level.dimension().identifier());
skipped++;
}
}
return new ApplySummary(1, applied, skipped);
}
public static GameRule<?> findRule(String rawId) {
String normalized = rawId.contains(":") ? rawId : "minecraft:" + rawId;
for (GameRule<?> rule : BuiltInRegistries.GAME_RULE) {
Identifier id = rule.getIdentifierWithFallback();
if (id.toString().equals(normalized) || rule.id().equals(rawId)) {
return rule;
}
}
return null;
}
public static boolean applySerialized(ServerLevel level, GameRule<?> rawRule, String serializedValue) {
return applySerializedTyped(level, rawRule, serializedValue);
}
private static <T> boolean applySerializedTyped(ServerLevel level, GameRule<T> rule, String serializedValue) {
var result = rule.deserialize(serializedValue).result();
if (result.isEmpty()) {
return false;
}
level.getGameRules().set(rule, result.get(), level.getServer());
return true;
}
public static <T> void resetToDefault(ServerLevel level, GameRule<T> rule) {
level.getGameRules().set(rule, rule.defaultValue(), level.getServer());
}
public record ApplySummary(int dimensions, int applied, int skipped) {}
}
@@ -4,7 +4,9 @@ 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;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
@@ -39,6 +41,7 @@ public final class LobbySystemsMod implements ModInitializer {
private static final PortalManager PORTALS = new PortalManager();
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
private static final DimensionGameRuleManager DIMENSION_GAMERULES = new DimensionGameRuleManager();
private static int saveTicker;
@Override
@@ -76,6 +79,36 @@ public final class LobbySystemsMod implements ModInitializer {
"Configured dimensions: " + DIMENSION_GAMEMODES.all().size()
);
boolean gameRulesLoaded = DIMENSION_GAMERULES.load();
LobbyLog.state(
gameRulesLoaded ? "DIMENSION GAMERULES LOADED" : "DIMENSION GAMERULES LOAD FAILED",
"File: " + ConfigPaths.DIMENSION_GAMERULES,
"Configured dimensions: " + DIMENSION_GAMERULES.all().size(),
"Mode: native per-dimension"
);
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
DimensionGameRuleManager.ApplySummary summary = DIMENSION_GAMERULES.applyAll(server);
LobbyLog.state(
"DIMENSION GAMERULES APPLIED",
"Configured dimensions loaded: " + summary.dimensions(),
"Rules applied: " + summary.applied(),
"Rules skipped: " + summary.skipped()
);
});
ServerLevelEvents.LOAD.register((server, world) -> {
DimensionGameRuleManager.ApplySummary summary = DIMENSION_GAMERULES.applyToLevel(world);
if (summary.applied() > 0 || summary.skipped() > 0) {
LobbyLog.state(
"DIMENSION GAMERULES WORLD LOAD",
"Dimension: " + world.dimension().identifier(),
"Rules applied: " + summary.applied(),
"Rules skipped: " + summary.skipped()
);
}
});
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
ServerPlayer player = listener.getPlayer();
server.execute(() -> {
@@ -149,6 +182,34 @@ public final class LobbySystemsMod implements ModInitializer {
true,
true))))
.then(Commands.literal("gamerule")
.then(Commands.literal("set")
.then(Commands.argument("dimension", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.then(Commands.argument("rule", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
BuiltInRegistries.GAME_RULE.stream().map(GameRule::getIdentifierWithFallback), builder))
.then(Commands.argument("value", StringArgumentType.word())
.suggests((ctx, builder) -> suggestGameRuleValues(
IdentifierArgument.getId(ctx, "rule").toString(), builder))
.executes(ctx -> setDimensionGameRule(
ctx.getSource(),
ctx.getSource().getServer(),
IdentifierArgument.getId(ctx, "dimension").toString(),
IdentifierArgument.getId(ctx, "rule").toString(),
StringArgumentType.getString(ctx, "value")))))))
.then(Commands.literal("reset")
.then(Commands.argument("dimension", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
.then(Commands.argument("rule", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
BuiltInRegistries.GAME_RULE.stream().map(GameRule::getIdentifierWithFallback), builder))
.executes(ctx -> resetDimensionGameRule(
ctx.getSource(),
ctx.getSource().getServer(),
IdentifierArgument.getId(ctx, "dimension").toString(),
IdentifierArgument.getId(ctx, "rule").toString())))))
.then(Commands.argument("dimension", IdentifierArgument.id())
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
@@ -300,6 +361,14 @@ public final class LobbySystemsMod implements ModInitializer {
DIMENSION_GAMEMODES.applyToDimension(server, dimension);
}
boolean gameRulesOk = DIMENSION_GAMERULES.load();
if (!gameRulesOk) {
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension-gamerules.json bermasalah. Cek console."));
LobbyLog.state("RELOAD FAILED", "Reason: dimension-gamerules.json gagal dibaca");
return 0;
}
DimensionGameRuleManager.ApplySummary gameRuleSummary = DIMENSION_GAMERULES.applyAll(server);
if (source.getEntity() instanceof ServerPlayer player) {
player.sendSystemMessage(Component.literal(
"§aLobby Systems direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
@@ -312,6 +381,7 @@ public final class LobbySystemsMod implements ModInitializer {
"Minimum play: " + CONFIG.get().minimumPlaySeconds + "s",
"Portals: " + PORTALS.size(),
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size(),
"Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied",
"Player positions lobby exclusion: active"
);
return 1;
@@ -362,11 +432,13 @@ public final class LobbySystemsMod implements ModInitializer {
int start = (page - 1) * GAMERULES_PER_PAGE;
int end = Math.min(start + GAMERULES_PER_PAGE, entries.size());
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
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()
+ (skippedRules > 0 ? " §8| §7Skipped: §e" + skippedRules : "")));
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
for (int i = start; i < end; i++) {
GameRuleEntry entry = entries.get(i);
@@ -374,10 +446,13 @@ public final class LobbySystemsMod implements ModInitializer {
sendCommandMessage(source, Component.literal("§8• §f" + entry.name() + " §7= " + valueColor + entry.value()));
}
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
if (page < totalPages) {
int next = page + 1;
sendCommandMessage(source, Component.literal(
"§7Halaman selanjutnya: §f/ls gamerule " + level.dimension().identifier() + " --page " + next));
} else if (page > 1) {
sendCommandMessage(source, Component.literal("§7Halaman terakhir."));
}
return entries.size();
}
@@ -395,6 +470,106 @@ public final class LobbySystemsMod implements ModInitializer {
}
}
private static java.util.concurrent.CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestGameRuleValues(
String ruleId,
com.mojang.brigadier.suggestion.SuggestionsBuilder builder) {
GameRule<?> rule = DimensionGameRuleManager.findRule(ruleId);
if (rule != null && rule.valueClass() == Boolean.class) {
return SharedSuggestionProvider.suggest(new String[]{"true", "false"}, builder);
}
if (rule != null) {
return SharedSuggestionProvider.suggest(new String[]{String.valueOf(rule.defaultValue())}, builder);
}
return builder.buildFuture();
}
private static int setDimensionGameRule(
CommandSourceStack source,
MinecraftServer server,
String dimensionId,
String ruleId,
String value) {
ServerLevel level = findLevel(server, dimensionId);
if (level == null) {
source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
return 0;
}
GameRule<?> rule = DimensionGameRuleManager.findRule(ruleId);
if (rule == null || !rule.isEnabled(level.enabledFeatures())) {
source.sendFailure(Component.literal("§cGamerule '" + ruleId + "' tidak ditemukan/tersedia di dimension ini."));
return 0;
}
if (!DimensionGameRuleManager.applySerialized(level, rule, value)) {
source.sendFailure(Component.literal("§cValue '" + value + "' tidak valid untuk gamerule " + rule.getIdentifierWithFallback() + "."));
return 0;
}
String normalizedDimension = level.dimension().identifier().toString();
String normalizedRule = rule.getIdentifierWithFallback().toString();
String actualValue = gameRuleValueUnchecked(level, rule);
if (!DIMENSION_GAMERULES.set(normalizedDimension, normalizedRule, actualValue)) {
source.sendFailure(Component.literal("§cGamerule sudah diterapkan di memory, tetapi gagal menyimpan dimension-gamerules.json."));
return 0;
}
sendCommandMessage(source, Component.literal(
"§aGamerule dimension disimpan. §7" + normalizedDimension
+ " §8| §f" + normalizedRule + " §7= §f" + actualValue));
LobbyLog.state(
"DIMENSION GAMERULE UPDATED",
"Dimension: " + normalizedDimension,
"Rule: " + normalizedRule,
"Value: " + actualValue,
"Persistent: yes"
);
return 1;
}
private static int resetDimensionGameRule(
CommandSourceStack source,
MinecraftServer server,
String dimensionId,
String ruleId) {
ServerLevel level = findLevel(server, dimensionId);
if (level == null) {
source.sendFailure(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load."));
return 0;
}
GameRule<?> rule = DimensionGameRuleManager.findRule(ruleId);
if (rule == null || !rule.isEnabled(level.enabledFeatures())) {
source.sendFailure(Component.literal("§cGamerule '" + ruleId + "' tidak ditemukan/tersedia di dimension ini."));
return 0;
}
resetGameRuleUnchecked(level, rule);
String normalizedDimension = level.dimension().identifier().toString();
String normalizedRule = rule.getIdentifierWithFallback().toString();
boolean removed = DIMENSION_GAMERULES.remove(normalizedDimension, normalizedRule);
String actualValue = gameRuleValueUnchecked(level, rule);
sendCommandMessage(source, Component.literal(
"§aGamerule dimension di-reset ke default. §7" + normalizedDimension
+ " §8| §f" + normalizedRule + " §7= §f" + actualValue
+ (removed ? "" : " §8(config override sebelumnya tidak ada)")));
LobbyLog.state(
"DIMENSION GAMERULE RESET",
"Dimension: " + normalizedDimension,
"Rule: " + normalizedRule,
"Default: " + actualValue
);
return 1;
}
private static <T> void resetGameRuleUnchecked(ServerLevel level, GameRule<T> rule) {
DimensionGameRuleManager.resetToDefault(level, rule);
}
private static <T> String gameRuleValueUnchecked(ServerLevel level, GameRule<T> rule) {
return rule.serialize(level.getGameRules().get(rule));
}
private static int showDimensionGameMode(CommandSourceStack source, String dimensionId) {
String mode = DIMENSION_GAMEMODES.get(dimensionId);
if (mode == null) {
@@ -670,6 +845,10 @@ public final class LobbySystemsMod implements ModInitializer {
return 1;
}
static ServerLevel findLevelForManager(MinecraftServer server, String dimensionId) {
return findLevel(server, dimensionId);
}
private static ServerLevel findLevel(MinecraftServer server, String dimensionId) {
try {
Identifier id = Identifier.parse(dimensionId.trim());
@@ -0,0 +1,35 @@
package id.dartokloning.lobbysystems.mixin;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.gamerules.GameRules;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Vanilla Minecraft 26.2 stores gamerule values in one server-wide GameRuleMap.
* Lobby Systems gives every ServerLevel its own copy so rules can differ by
* dimension without leaking into other dimensions.
*/
@Mixin(ServerLevel.class)
public abstract class ServerLevelGameRulesMixin {
@Unique
private GameRules lobbySystems$dimensionGameRules;
@Inject(method = "getGameRules", at = @At("HEAD"), cancellable = true)
private void lobbySystems$getDimensionGameRules(CallbackInfoReturnable<GameRules> cir) {
if (lobbySystems$dimensionGameRules == null) {
ServerLevel self = (ServerLevel) (Object) this;
MinecraftServer server = self.getServer();
if (server != null) {
lobbySystems$dimensionGameRules = server.getGameRules().copy(self.enabledFeatures());
}
}
if (lobbySystems$dimensionGameRules != null) {
cir.setReturnValue(lobbySystems$dimensionGameRules);
}
}
}
+13 -3
View File
@@ -3,12 +3,16 @@
"id": "dartokloning",
"version": "${version}",
"name": "Lobby Systems",
"description": "Server-side Fabric lobby routing, persistent world positions, minimum playtime, portals, gamerule inspection, and per-dimension player gamemodes.",
"authors": ["DartoKloning"],
"description": "Server-side Fabric lobby routing, persistent world positions, portals, per-dimension gamemodes, and native persistent per-dimension gamerules.",
"authors": [
"DartoKloning"
],
"license": "All Rights Reserved",
"environment": "server",
"entrypoints": {
"main": ["id.dartokloning.lobbysystems.LobbySystemsMod"]
"main": [
"id.dartokloning.lobbysystems.LobbySystemsMod"
]
},
"depends": {
"fabricloader": ">=0.19.3",
@@ -18,5 +22,11 @@
},
"suggests": {
"worldedit": "*"
},
"mixins": [
"lobby-systems.mixins.json"
],
"breaks": {
"world-gamerules": "*"
}
}
@@ -0,0 +1,11 @@
{
"required": true,
"package": "id.dartokloning.lobbysystems.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"ServerLevelGameRulesMixin"
],
"injectors": {
"defaultRequire": 1
}
}