Add gamerule per dimension
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user