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
|
### Reload feedback
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ chmod +x compile.sh release.sh
|
|||||||
```text
|
```text
|
||||||
============================================
|
============================================
|
||||||
Lobby Systems - Fabric 26.2 Build
|
Lobby Systems - Fabric 26.2 Build
|
||||||
Version: 1.3.3
|
Version: 1.4.0
|
||||||
Namespace: id.dartokloning.lobbysystems
|
Namespace: id.dartokloning.lobbysystems
|
||||||
Mod ID: dartokloning
|
Mod ID: dartokloning
|
||||||
============================================
|
============================================
|
||||||
@@ -49,7 +49,7 @@ chmod +x compile.sh release.sh
|
|||||||
Output utama:
|
Output utama:
|
||||||
|
|
||||||
```text
|
```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.
|
`*-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
|
```text
|
||||||
============================================
|
============================================
|
||||||
Lobby Systems - READY
|
Lobby Systems - READY
|
||||||
Version: 1.3.3
|
Version: 1.4.0
|
||||||
Minecraft: 26.2
|
Minecraft: 26.2
|
||||||
Config directory: config/lobby
|
Config directory: config/lobby
|
||||||
WorldEdit: available
|
WorldEdit: available
|
||||||
============================================
|
============================================
|
||||||
```
|
```
|
||||||
|
|
||||||
## Struktur konfigurasi v1.3.3
|
## Struktur konfigurasi v1.4.0
|
||||||
|
|
||||||
```text
|
```text
|
||||||
config/
|
config/
|
||||||
@@ -95,7 +95,8 @@ config/
|
|||||||
├── config.json
|
├── config.json
|
||||||
├── player-positions.json
|
├── player-positions.json
|
||||||
├── portals.json
|
├── portals.json
|
||||||
└── dimension-gamemodes.json
|
├── dimension-gamemodes.json
|
||||||
|
└── dimension-gamerules.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### `config/lobby/config.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`
|
### `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:
|
Contoh:
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ Gamemode yang didukung: `survival`, `creative`, `adventure`, dan `spectator`.
|
|||||||
|
|
||||||
## Migrasi dari v1.1.x
|
## Migrasi dari v1.1.x
|
||||||
|
|
||||||
Pada startup pertama v1.3.3:
|
Pada startup pertama v1.4.0:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
config/lobby-systems.json
|
config/lobby-systems.json
|
||||||
@@ -197,7 +198,7 @@ akan dimigrasikan ke:
|
|||||||
config/lobby/player-positions.json
|
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.
|
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>
|
||||||
/ls gamerule <dimension> --page <page>
|
/ls gamerule <dimension> --page <page>
|
||||||
|
/ls gamerule set <dimension> <rule> <value>
|
||||||
|
/ls gamerule reset <dimension> <rule>
|
||||||
|
|
||||||
/ls gamemode <dimension>
|
/ls gamemode <dimension>
|
||||||
/ls gamemode list
|
/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.
|
`/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:
|
Contoh:
|
||||||
|
|
||||||
@@ -268,6 +289,7 @@ Reload live:
|
|||||||
- `config/lobby/config.json`
|
- `config/lobby/config.json`
|
||||||
- `config/lobby/portals.json`
|
- `config/lobby/portals.json`
|
||||||
- `config/lobby/dimension-gamemodes.json`
|
- `config/lobby/dimension-gamemodes.json`
|
||||||
|
- `config/lobby/dimension-gamerules.json`
|
||||||
- lobby exclusion pada player-position store
|
- lobby exclusion pada player-position store
|
||||||
|
|
||||||
Tidak perlu restart untuk perubahan JSON tersebut.
|
Tidak perlu restart untuk perubahan JSON tersebut.
|
||||||
|
|||||||
+53
-10
@@ -179,9 +179,12 @@ if defined HAS_CHANGES (
|
|||||||
git status --short
|
git status --short
|
||||||
echo.
|
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 "COMMIT_MESSAGE="
|
||||||
set /p "COMMIT_MESSAGE=Commit message [Lobby Systems v%MOD_VERSION% build]: "
|
set /p "COMMIT_MESSAGE=Commit message [!SUGGESTED_COMMIT!]: "
|
||||||
if not defined COMMIT_MESSAGE set "COMMIT_MESSAGE=Lobby Systems v%MOD_VERSION% build"
|
if not defined COMMIT_MESSAGE set "COMMIT_MESSAGE=!SUGGESTED_COMMIT!"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [INFO] Menambahkan perubahan ke staging...
|
echo [INFO] Menambahkan perubahan ke staging...
|
||||||
@@ -217,18 +220,57 @@ if errorlevel 1 (
|
|||||||
goto :git_failed
|
goto :git_failed
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [INFO] Push ke origin/!CURRENT_BRANCH!...
|
call :push_branch_with_retry
|
||||||
git push origin "!CURRENT_BRANCH!"
|
if errorlevel 1 goto :git_failed
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Git push gagal.
|
|
||||||
echo [INFO] Commit lokal tetap tersimpan.
|
|
||||||
goto :git_failed
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [SUCCESS] Push ke Git/Gitea berhasil.
|
echo [SUCCESS] Push ke Git/Gitea berhasil.
|
||||||
goto :success
|
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
|
:skip_git
|
||||||
echo.
|
echo.
|
||||||
echo [INFO] Git commit/push dilewati.
|
echo [INFO] Git commit/push dilewati.
|
||||||
@@ -263,7 +305,8 @@ echo GIT FAILED
|
|||||||
echo ============================================
|
echo ============================================
|
||||||
echo.
|
echo.
|
||||||
echo [INFO] Build Lobby Systems tetap berhasil.
|
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.
|
echo.
|
||||||
pause
|
pause
|
||||||
exit /b 2
|
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'
|
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
|
if [[ ! -f "$GRADLE_PROPERTIES" ]]; then
|
||||||
error "gradle.properties tidak ditemukan."
|
error "gradle.properties tidak ditemukan."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -122,7 +170,6 @@ if [[ ! -x "$GRADLE_BIN" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
ok "Gradle $GRADLE_VERSION tersedia."
|
ok "Gradle $GRADLE_VERSION tersedia."
|
||||||
|
|
||||||
info "Menjalankan clean build Lobby Systems v$MOD_VERSION..."
|
info "Menjalankan clean build Lobby Systems v$MOD_VERSION..."
|
||||||
echo
|
echo
|
||||||
|
|
||||||
@@ -155,14 +202,12 @@ else
|
|||||||
echo "$JAR_PATH"
|
echo "$JAR_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# release.sh memakai mode ini supaya build tidak menampilkan prompt Git dua kali.
|
|
||||||
if [[ $BUILD_ONLY -eq 1 ]]; then
|
if [[ $BUILD_ONLY -eq 1 ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
read -r -p "Commit dan push perubahan ke Git/Gitea? [Y/N]: " GIT_CONFIRM
|
read -r -p "Commit dan push perubahan ke Git/Gitea? [Y/N]: " GIT_CONFIRM
|
||||||
|
|
||||||
case "$GIT_CONFIRM" in
|
case "$GIT_CONFIRM" in
|
||||||
y|Y|yes|YES|Yes) ;;
|
y|Y|yes|YES|Yes) ;;
|
||||||
*)
|
*)
|
||||||
@@ -208,12 +253,11 @@ if [[ -n "$CHANGES" ]]; then
|
|||||||
git status --short
|
git status --short
|
||||||
echo
|
echo
|
||||||
|
|
||||||
read -r -p "Commit message [Lobby Systems v$MOD_VERSION build]: " COMMIT_MESSAGE
|
suggest_commit_message
|
||||||
|
echo "[SUGGEST] Commit message: $SUGGESTED_COMMIT"
|
||||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
info "Tekan Enter untuk memakai saran tersebut, atau ketik pesan sendiri."
|
||||||
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
|
read -r -p "Commit message [$SUGGESTED_COMMIT]: " COMMIT_MESSAGE
|
||||||
COMMIT_MESSAGE="Lobby Systems v$MOD_VERSION build - $TIMESTAMP"
|
[[ -z "$COMMIT_MESSAGE" ]] && COMMIT_MESSAGE="$SUGGESTED_COMMIT"
|
||||||
fi
|
|
||||||
|
|
||||||
info "Menambahkan perubahan ke staging..."
|
info "Menambahkan perubahan ke staging..."
|
||||||
if ! git add -A; then
|
if ! git add -A; then
|
||||||
@@ -232,7 +276,6 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
CURRENT_BRANCH="$(git branch --show-current)"
|
CURRENT_BRANCH="$(git branch --show-current)"
|
||||||
|
|
||||||
if [[ -z "$CURRENT_BRANCH" ]]; then
|
if [[ -z "$CURRENT_BRANCH" ]]; then
|
||||||
error "Tidak dapat menentukan branch Git aktif."
|
error "Tidak dapat menentukan branch Git aktif."
|
||||||
exit 2
|
exit 2
|
||||||
@@ -252,12 +295,15 @@ fi
|
|||||||
|
|
||||||
REMOTE_URL="$(git remote get-url origin)"
|
REMOTE_URL="$(git remote get-url origin)"
|
||||||
info "Remote origin: $REMOTE_URL"
|
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
|
echo
|
||||||
error "Git push gagal."
|
separator
|
||||||
|
echo " GIT FAILED"
|
||||||
|
separator
|
||||||
|
info "Build Lobby Systems tetap berhasil."
|
||||||
info "Commit lokal tetap tersimpan."
|
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
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,6 @@ minecraft_version=26.2
|
|||||||
loader_version=0.19.3
|
loader_version=0.19.3
|
||||||
loom_version=1.17-SNAPSHOT
|
loom_version=1.17-SNAPSHOT
|
||||||
fabric_api_version=0.156.0+26.2
|
fabric_api_version=0.156.0+26.2
|
||||||
mod_version=1.3.3
|
mod_version=1.4.0
|
||||||
maven_group=id.dartokloning
|
maven_group=id.dartokloning
|
||||||
archives_base_name=lobby-systems
|
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 PLAYER_POSITIONS = DIRECTORY.resolve("player-positions.json");
|
||||||
public static final Path PORTALS = DIRECTORY.resolve("portals.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_GAMEMODES = DIRECTORY.resolve("dimension-gamemodes.json");
|
||||||
|
public static final Path DIMENSION_GAMERULES = DIRECTORY.resolve("dimension-gamerules.json");
|
||||||
|
|
||||||
private ConfigPaths() {}
|
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 com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import net.fabricmc.api.ModInitializer;
|
import net.fabricmc.api.ModInitializer;
|
||||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
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.ServerTickEvents;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
|
||||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.commands.Commands;
|
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 PortalManager PORTALS = new PortalManager();
|
||||||
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
|
private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator();
|
||||||
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
|
private static final DimensionGameModeManager DIMENSION_GAMEMODES = new DimensionGameModeManager();
|
||||||
|
private static final DimensionGameRuleManager DIMENSION_GAMERULES = new DimensionGameRuleManager();
|
||||||
private static int saveTicker;
|
private static int saveTicker;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -76,6 +79,36 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
"Configured dimensions: " + DIMENSION_GAMEMODES.all().size()
|
"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) -> {
|
ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> {
|
||||||
ServerPlayer player = listener.getPlayer();
|
ServerPlayer player = listener.getPlayer();
|
||||||
server.execute(() -> {
|
server.execute(() -> {
|
||||||
@@ -149,6 +182,34 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
true,
|
true,
|
||||||
true))))
|
true))))
|
||||||
.then(Commands.literal("gamerule")
|
.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())
|
.then(Commands.argument("dimension", IdentifierArgument.id())
|
||||||
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
|
.suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource(
|
||||||
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
|
ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder))
|
||||||
@@ -300,6 +361,14 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
DIMENSION_GAMEMODES.applyToDimension(server, dimension);
|
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) {
|
if (source.getEntity() instanceof ServerPlayer player) {
|
||||||
player.sendSystemMessage(Component.literal(
|
player.sendSystemMessage(Component.literal(
|
||||||
"§aLobby Systems direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
|
"§aLobby Systems direload. §7Lobby: §f" + CONFIG.get().lobbyDimension
|
||||||
@@ -312,6 +381,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
"Minimum play: " + CONFIG.get().minimumPlaySeconds + "s",
|
"Minimum play: " + CONFIG.get().minimumPlaySeconds + "s",
|
||||||
"Portals: " + PORTALS.size(),
|
"Portals: " + PORTALS.size(),
|
||||||
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size(),
|
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size(),
|
||||||
|
"Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied",
|
||||||
"Player positions lobby exclusion: active"
|
"Player positions lobby exclusion: active"
|
||||||
);
|
);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -362,11 +432,13 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
int start = (page - 1) * GAMERULES_PER_PAGE;
|
int start = (page - 1) * GAMERULES_PER_PAGE;
|
||||||
int end = Math.min(start + GAMERULES_PER_PAGE, entries.size());
|
int end = Math.min(start + GAMERULES_PER_PAGE, entries.size());
|
||||||
|
|
||||||
|
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
|
||||||
sendCommandMessage(source, Component.literal(
|
sendCommandMessage(source, Component.literal(
|
||||||
"§6§lLobby Systems Gamerules §r§7[§f" + page + "§7/§f" + totalPages + "§7]"
|
"§6§lLobby Systems Gamerules §r§7[§f" + page + "§7/§f" + totalPages + "§7]"
|
||||||
+ "\n§7Dimension: §f" + level.dimension().identifier()
|
+ "\n§7Dimension: §f" + level.dimension().identifier()
|
||||||
+ " §8| §7Rules: §f" + entries.size()
|
+ " §8| §7Rules: §f" + entries.size()
|
||||||
+ (skippedRules > 0 ? " §8| §7Skipped: §e" + skippedRules : "")));
|
+ (skippedRules > 0 ? " §8| §7Skipped: §e" + skippedRules : "")));
|
||||||
|
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
|
||||||
|
|
||||||
for (int i = start; i < end; i++) {
|
for (int i = start; i < end; i++) {
|
||||||
GameRuleEntry entry = entries.get(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• §f" + entry.name() + " §7= " + valueColor + entry.value()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendCommandMessage(source, Component.literal("§8§m----------------------------------------"));
|
||||||
if (page < totalPages) {
|
if (page < totalPages) {
|
||||||
int next = page + 1;
|
int next = page + 1;
|
||||||
sendCommandMessage(source, Component.literal(
|
sendCommandMessage(source, Component.literal(
|
||||||
"§7Halaman selanjutnya: §f/ls gamerule " + level.dimension().identifier() + " --page " + next));
|
"§7Halaman selanjutnya: §f/ls gamerule " + level.dimension().identifier() + " --page " + next));
|
||||||
|
} else if (page > 1) {
|
||||||
|
sendCommandMessage(source, Component.literal("§7Halaman terakhir."));
|
||||||
}
|
}
|
||||||
return entries.size();
|
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) {
|
private static int showDimensionGameMode(CommandSourceStack source, String dimensionId) {
|
||||||
String mode = DIMENSION_GAMEMODES.get(dimensionId);
|
String mode = DIMENSION_GAMEMODES.get(dimensionId);
|
||||||
if (mode == null) {
|
if (mode == null) {
|
||||||
@@ -670,6 +845,10 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ServerLevel findLevelForManager(MinecraftServer server, String dimensionId) {
|
||||||
|
return findLevel(server, dimensionId);
|
||||||
|
}
|
||||||
|
|
||||||
private static ServerLevel findLevel(MinecraftServer server, String dimensionId) {
|
private static ServerLevel findLevel(MinecraftServer server, String dimensionId) {
|
||||||
try {
|
try {
|
||||||
Identifier id = Identifier.parse(dimensionId.trim());
|
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",
|
"id": "dartokloning",
|
||||||
"version": "${version}",
|
"version": "${version}",
|
||||||
"name": "Lobby Systems",
|
"name": "Lobby Systems",
|
||||||
"description": "Server-side Fabric lobby routing, persistent world positions, minimum playtime, portals, gamerule inspection, and per-dimension player gamemodes.",
|
"description": "Server-side Fabric lobby routing, persistent world positions, portals, per-dimension gamemodes, and native persistent per-dimension gamerules.",
|
||||||
"authors": ["DartoKloning"],
|
"authors": [
|
||||||
|
"DartoKloning"
|
||||||
|
],
|
||||||
"license": "All Rights Reserved",
|
"license": "All Rights Reserved",
|
||||||
"environment": "server",
|
"environment": "server",
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"main": ["id.dartokloning.lobbysystems.LobbySystemsMod"]
|
"main": [
|
||||||
|
"id.dartokloning.lobbysystems.LobbySystemsMod"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"depends": {
|
"depends": {
|
||||||
"fabricloader": ">=0.19.3",
|
"fabricloader": ">=0.19.3",
|
||||||
@@ -18,5 +22,11 @@
|
|||||||
},
|
},
|
||||||
"suggests": {
|
"suggests": {
|
||||||
"worldedit": "*"
|
"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