config restructure
This commit is contained in:
@@ -1,21 +1,21 @@
|
|||||||
# Lobby Systems v1.8.5 — Fabric 26.2
|
# Lobby Systems v1.9.0 — Fabric 26.2
|
||||||
|
|
||||||
Server-side lobby framework untuk Minecraft Fabric 26.2. Lobby Systems menangani lobby placement, portal antar-dimensi, gamemode/gamerule per dimensi, Starter Kit, penyimpanan posisi player, dan hybrid authentication untuk premium + local/cracked player.
|
Server-side lobby framework untuk Minecraft Fabric 26.2. Lobby Systems menangani pre-login lobby placement, portal antar-dimensi, gamemode/gamerule per dimension, Starter Kit, persistent player data, serta hybrid authentication untuk premium dan local/cracked player.
|
||||||
|
|
||||||
## Target
|
## Target
|
||||||
|
|
||||||
- Minecraft: **26.2**
|
- Minecraft **26.2**
|
||||||
- Java: **25**
|
- Java **25**
|
||||||
- Fabric Loader: **0.19.3**
|
- Fabric Loader **0.19.3**
|
||||||
- Fabric API: **0.156.0+26.2**
|
- Fabric API **0.156.0+26.2**
|
||||||
- Fabric Loom: **1.17**
|
- Fabric Loom **1.17**
|
||||||
- Mod ID: `dartokloning`
|
- Mod ID `dartokloning`
|
||||||
- Namespace: `id.dartokloning.lobbysystems`
|
- Java namespace `id.dartokloning.lobbysystems`
|
||||||
- License: All Rights Reserved
|
- License **All Rights Reserved**
|
||||||
|
|
||||||
Lobby Systems adalah mod server-side. WorldEdit Fabric diperlukan untuk workflow pembuatan portal dari selection.
|
Lobby Systems adalah mod server-side. WorldEdit Fabric digunakan untuk workflow pembuatan portal dari selection.
|
||||||
|
|
||||||
## Build
|
## Build dan release
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
@@ -29,32 +29,54 @@ Linux:
|
|||||||
./compile.sh
|
./compile.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Script membaca versi dari `gradle.properties`, menggunakan Java 25, dan menyediakan integrasi Git setelah build. `release.bat` / `release.sh` digunakan untuk build release, push branch/tag, membuat Gitea release, dan mengunggah JAR.
|
Script membaca versi dari `gradle.properties` dan menggunakan Java 25. `release.bat` / `release.sh` menangani build release, Git push/tag, Gitea release, dan upload JAR.
|
||||||
|
|
||||||
## Struktur konfigurasi
|
## Struktur data dan konfigurasi
|
||||||
|
|
||||||
```text
|
```text
|
||||||
config/lobby/
|
config/lobby/
|
||||||
├── config.json
|
├── config.json
|
||||||
├── player-positions.json
|
├── player.json
|
||||||
├── portals.json
|
├── portals.json
|
||||||
├── dimension-gamemodes.json
|
├── auth/
|
||||||
├── dimension-gamerules.json
|
│ ├── config.json
|
||||||
├── starterkits.json
|
│ └── rules.json
|
||||||
├── starterkit-recipients.json
|
├── dimension/
|
||||||
└── auth/
|
│ ├── gamemodes.json
|
||||||
├── config.json
|
│ └── gamerules.json
|
||||||
├── rules.json
|
└── starterkit/
|
||||||
└── accounts.json
|
├── list.json
|
||||||
|
└── recipients.json
|
||||||
```
|
```
|
||||||
|
|
||||||
`/ls reload` me-reload **seluruh konfigurasi Lobby Systems**, termasuk config utama, Auth, portal, dimension gamemode, dimension gamerule, dan Starter Kit. Perubahan Auth juga diterapkan ke session player yang sedang online. Perubahan Auth, termasuk `rules.json`, `requireLoginEveryJoin`, dan `enabled`, tidak memerlukan restart server.
|
`config.json` adalah konfigurasi utama Lobby. `player.json` adalah database persistent player bersama yang dipakai Auth dan last-position. File konfigurasi subsystem dikelompokkan ke folder `auth`, `dimension`, dan `starterkit`.
|
||||||
|
|
||||||
## Lobby dan login placement
|
v1.9.0 ditujukan untuk development layout baru dan **tidak melakukan automatic migration**. Pada development server, hapus data `config/lobby` lama sebelum first run v1.9.0 jika ingin generated defaults yang bersih.
|
||||||
|
|
||||||
Player ditempatkan ke Lobby pada fase awal login agar world gameplay sebelumnya tidak sempat menjadi world awal yang dikirim ke client. Lobby sebaiknya merupakan dimensi yang safe dan secure karena player yang belum selesai authentication ditahan di posisi login sampai proses selesai.
|
## `/ls reload`
|
||||||
|
|
||||||
`/lobby` hanya tersedia dari luar Lobby dan mengikuti minimum gameplay time yang dikonfigurasi. Posisi terakhir per gameplay dimension disimpan terpisah; Lobby dikecualikan dari penyimpanan posisi gameplay.
|
`/ls reload` adalah satu pintu untuk me-reload seluruh runtime configuration/data Lobby Systems. Reload mencakup:
|
||||||
|
|
||||||
|
1. `config/lobby/config.json`
|
||||||
|
2. `config/lobby/player.json` melalui Auth/player-data subsystem
|
||||||
|
3. `config/lobby/auth/config.json`
|
||||||
|
4. `config/lobby/auth/rules.json`
|
||||||
|
5. `config/lobby/portals.json` dan portal visual repair
|
||||||
|
6. `config/lobby/dimension/gamemodes.json` lalu apply ke dimension/player
|
||||||
|
7. `config/lobby/dimension/gamerules.json` lalu apply ke loaded dimensions
|
||||||
|
8. `config/lobby/starterkit/list.json`
|
||||||
|
9. `config/lobby/starterkit/recipients.json`
|
||||||
|
10. refresh runtime state yang terkait
|
||||||
|
|
||||||
|
Perubahan Auth diterapkan ke session player yang sedang online. Console menampilkan status reload dan admin yang menjalankan command menerima ringkasan hasilnya.
|
||||||
|
|
||||||
|
## Lobby dan player position
|
||||||
|
|
||||||
|
Player ditempatkan ke Lobby pada fase awal login agar gameplay dimension sebelumnya tidak sempat menjadi initial world yang dikirim ke client. Lobby sebaiknya merupakan dimension yang safe dan secure karena Rules/Register/Login berjalan setelah player masuk server.
|
||||||
|
|
||||||
|
`/lobby` hanya tersedia dari luar Lobby dan mengikuti minimum gameplay time pada `config.json`.
|
||||||
|
|
||||||
|
Last-position per gameplay dimension disimpan pada section `positions` di `player.json`. Lobby dikecualikan sehingga posisi Lobby tidak menggantikan posisi gameplay yang harus dipulihkan.
|
||||||
|
|
||||||
## Authentication System
|
## Authentication System
|
||||||
|
|
||||||
@@ -64,54 +86,45 @@ Server tetap menggunakan:
|
|||||||
online-mode=true
|
online-mode=true
|
||||||
```
|
```
|
||||||
|
|
||||||
Lobby Systems kemudian menyediakan hybrid authentication:
|
Hybrid authentication Lobby Systems:
|
||||||
|
|
||||||
- **Premium**: tetap melalui official Mojang/vanilla authentication.
|
- Premium player tetap melewati official Mojang/vanilla authentication.
|
||||||
- **Local/cracked**: menggunakan deterministic offline UUID dan password Lobby Systems.
|
- Local/cracked player memakai deterministic offline UUID dan password Lobby Systems.
|
||||||
- Username premium tidak boleh diregistrasikan sebagai local account.
|
- Premium username dilindungi agar tidak dapat diregistrasikan sebagai local identity.
|
||||||
- Password disimpan sebagai PBKDF2-HMAC-SHA256 dengan random salt; plaintext password tidak disimpan.
|
- Password disimpan menggunakan PBKDF2-HMAC-SHA256 + random salt; plaintext password tidak disimpan.
|
||||||
|
- Metadata account, password hash/salt, Rules acceptance, dan last-position sama-sama disimpan di `config/lobby/player.json` pada section masing-masing.
|
||||||
|
|
||||||
### Dialog flow
|
### Dialog flow
|
||||||
|
|
||||||
Authentication v1.8 menggunakan vanilla Minecraft Dialog UI sehingga normal player tidak perlu mengetik `/register` atau `/login`.
|
Rules/Register/Login menggunakan vanilla Minecraft Dialog UI sehingga normal player tidak perlu mengetik command.
|
||||||
|
|
||||||
First join dapat mengikuti:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Connect
|
Connect
|
||||||
-> Server Rules (jika aktif/belum accepted)
|
-> Server Rules (jika enabled dan belum accepted)
|
||||||
-> Create Account (jika local password diperlukan)
|
-> Premium auto-login ATAU Create Account / Sign In
|
||||||
-> Authenticated
|
-> Authenticated
|
||||||
```
|
```
|
||||||
|
|
||||||
Returning local player:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Connect -> Rules jika versi berubah -> Sign In -> Authenticated
|
|
||||||
```
|
|
||||||
|
|
||||||
`/register` dan `/login` tetap tersedia sebagai fallback compatibility.
|
`/register` dan `/login` tetap tersedia sebagai fallback compatibility.
|
||||||
|
|
||||||
### `requireLoginEveryJoin`
|
### Premium policy
|
||||||
|
|
||||||
Default:
|
`auth/config.json` menyediakan `premiumAutoLogin` dan `requireLoginEveryJoin`.
|
||||||
|
|
||||||
```json
|
- `premiumAutoLogin=true` + `requireLoginEveryJoin=false`: premium cukup dengan Mojang authentication dan auto-login.
|
||||||
"requireLoginEveryJoin": false
|
- `premiumAutoLogin=false`: premium tetap diverifikasi Mojang tetapi juga harus mempunyai password Lobby Systems dan melewati Register/Login Dialog.
|
||||||
```
|
- `requireLoginEveryJoin=true`: premium wajib melewati password Lobby Systems setiap connection.
|
||||||
|
- Local/cracked selalu memakai password Lobby Systems setiap connection.
|
||||||
|
|
||||||
Semantik `premiumAutoLogin` dan `requireLoginEveryJoin`:
|
Password Lobby Systems pada premium account adalah authentication layer tambahan dan tidak menggantikan Mojang authentication.
|
||||||
|
|
||||||
- `premiumAutoLogin=true` + `requireLoginEveryJoin=false`: premium player cukup dengan Mojang authentication dan auto-login.
|
### Authentication timeout
|
||||||
- `premiumAutoLogin=false`: premium player tetap diverifikasi Mojang, tetapi wajib membuat/memasukkan password Lobby Systems.
|
|
||||||
- `requireLoginEveryJoin=true`: premium player juga wajib password Lobby Systems setiap connection, terlepas dari auto-login policy.
|
|
||||||
- Local/cracked player selalu memakai password Lobby Systems setiap connection.
|
|
||||||
|
|
||||||
Password Lobby Systems untuk premium adalah lapisan tambahan; Mojang authentication tetap dipertahankan.
|
Register/Login menggunakan `authenticationTimeoutSeconds` dari `config/lobby/auth/config.json`. Timeout tidak ditampilkan sebagai visual countdown karena Minecraft Dialog berada pada modal UI layer. Jika batas waktu habis, player di-disconnect dengan message yang dapat dikustomisasi dari Auth config.
|
||||||
|
|
||||||
### Server Rules
|
### Server Rules
|
||||||
|
|
||||||
Rules dipisahkan dari Auth config dan dibuat otomatis sebagai `config/lobby/auth/rules.json`. File ini adalah sumber isi window Rules:
|
Rules berasal dari `config/lobby/auth/rules.json`. Contoh default:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -134,19 +147,48 @@ Rules dipisahkan dari Auth config dan dibuat otomatis sebagai `config/lobby/auth
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Acceptance disimpan per account. `version` hanya perlu dinaikkan ketika perubahan rules memang harus diminta untuk diterima ulang oleh seluruh player. Mengedit teks tanpa menaikkan `version` tidak menghapus acceptance lama. `/ls reload` membaca ulang file ini dan menerapkan perubahan runtime.
|
`enabled=false` melewati Rules Dialog. `version` adalah metadata internal: naikkan hanya jika semua player harus menerima ulang rules yang baru. Nomor version tidak ditampilkan pada Dialog.
|
||||||
|
|
||||||
### Auth account data
|
`timeoutSeconds` membatasi berapa lama Rules Dialog boleh dibiarkan tanpa acceptance. Tidak ada visual countdown; ketika timeout habis player menerima disconnect reason dari `timeoutMessage`. Placeholder `{seconds}` diisi dari nilai `timeoutSeconds`.
|
||||||
|
|
||||||
`config/lobby/auth/accounts.json` menyimpan metadata account, rules acceptance, salt, password hash, dan PBKDF2 iteration count. File ini bukan tempat plaintext password.
|
## `player.json`
|
||||||
|
|
||||||
|
`player.json` adalah satu persistent database untuk data player. Saat ini mempunyai dua logical section:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"player-name": {
|
||||||
|
"salt": "...",
|
||||||
|
"hash": "...",
|
||||||
|
"iterations": 210000,
|
||||||
|
"accountType": "PREMIUM",
|
||||||
|
"officialUuid": "...",
|
||||||
|
"rulesVersionAccepted": 1,
|
||||||
|
"rulesAcceptedAt": "..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"player-uuid": {
|
||||||
|
"minecraft:overworld": {
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 64.0,
|
||||||
|
"z": 0.5,
|
||||||
|
"yaw": 0.0,
|
||||||
|
"pitch": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Kedua subsystem menggunakan file yang sama dan writer mempertahankan section milik subsystem lain agar update Auth tidak menghapus positions dan sebaliknya.
|
||||||
|
|
||||||
## Portal System
|
## Portal System
|
||||||
|
|
||||||
Portal dibuat dari WorldEdit selection dan disimpan di `portals.json`. Visual portal menggunakan vanilla `minecraft:nether_portal` blocks, tetapi registered Lobby Systems portal mencegah vanilla Nether portal routing mengambil alih.
|
Portal dibuat dari WorldEdit selection dan disimpan di `config/lobby/portals.json`. Visual menggunakan vanilla `minecraft:nether_portal`, tetapi registered Lobby Systems portal mencegah vanilla Nether routing mengambil alih.
|
||||||
|
|
||||||
Destination dapat mengarah ke dimensi lain dan teleport menggunakan `TeleportTransition`, chunk preload, serta teleport lock. Portal visual dapat diperbaiki kembali saat reload/startup.
|
Teleport menggunakan `TeleportTransition`, chunk preload, dan teleport lock. Portal visual diperbaiki saat startup/reload bila diperlukan.
|
||||||
|
|
||||||
Gunakan command `/ls portal ...` yang tersedia melalui command completion untuk create/delete/info dan maintenance portal.
|
|
||||||
|
|
||||||
## Dimension Gamemode
|
## Dimension Gamemode
|
||||||
|
|
||||||
@@ -158,26 +200,17 @@ Gamemode per dimension bersifat authoritative dan selalu aktif.
|
|||||||
/ls gamemode remove <dimension>
|
/ls gamemode remove <dimension>
|
||||||
```
|
```
|
||||||
|
|
||||||
Mapping disimpan di `dimension-gamemodes.json` dan diterapkan pada join serta perpindahan dimension.
|
Mapping disimpan di `config/lobby/dimension/gamemodes.json` dan diterapkan pada join serta dimension change.
|
||||||
|
|
||||||
Contoh:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/ls gamemode set minecraft:lobby adventure
|
|
||||||
/ls gamemode set minecraft:creative creative
|
|
||||||
/ls gamemode set minecraft:flat_survival survival
|
|
||||||
/ls gamemode set minecraft:flat_creative creative
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dimension Gamerules
|
## Dimension Gamerules
|
||||||
|
|
||||||
Gamerule dapat diatur per dimension dan disimpan di `dimension-gamerules.json`. Nilainya diterapkan ke native gamerules masing-masing `ServerLevel`, sehingga tidak bergantung pada global gamerule saja.
|
Gamerule per dimension disimpan di `config/lobby/dimension/gamerules.json` dan diterapkan ke native gamerules masing-masing `ServerLevel`.
|
||||||
|
|
||||||
Gunakan `/ls gamerule ...` beserta command completion untuk melihat dan mengubah mapping.
|
Gunakan `/ls gamerule ...` beserta command completion untuk melihat dan mengubah mapping.
|
||||||
|
|
||||||
## Starter Kit
|
## Starter Kit
|
||||||
|
|
||||||
Starter Kit adalah snapshot inventory/equipment per dimension. Hanya satu kit yang dapat aktif untuk satu dimension, dan eligibility automatic delivery dicatat per **player + dimension**.
|
Starter Kit adalah snapshot inventory/equipment per dimension. Definitions berada di `config/lobby/starterkit/list.json`, sedangkan automatic-delivery history berada di `config/lobby/starterkit/recipients.json`.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/ls starterkit set <name> <dimension>
|
/ls starterkit set <name> <dimension>
|
||||||
@@ -192,7 +225,7 @@ Starter Kit adalah snapshot inventory/equipment per dimension. Hanya satu kit ya
|
|||||||
/ls starterkit delete <name>
|
/ls starterkit delete <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
Automatic delivery dijalankan setelah dimension change dengan delay agar kompatibel dengan mod inventory-per-dimension seperti Dimensional Inventories. `/ls starterkit give` adalah forced admin give dan tidak mengubah recipient history.
|
Satu dimension hanya mempunyai satu active kit. Eligibility automatic delivery dicatat per player + dimension. Forced admin give tidak mengubah recipient history.
|
||||||
|
|
||||||
## Command utama
|
## Command utama
|
||||||
|
|
||||||
@@ -208,63 +241,19 @@ Automatic delivery dijalankan setelah dimension change dengan delay agar kompati
|
|||||||
|
|
||||||
Command `/ls` administratif memerlukan operator permission sesuai implementasi command.
|
Command `/ls` administratif memerlukan operator permission sesuai implementasi command.
|
||||||
|
|
||||||
## `/ls reload`
|
## v1.9.0 — Config restructure
|
||||||
|
|
||||||
Reload adalah satu pintu untuk seluruh runtime configuration. Urutannya mencakup:
|
- Config/data dikelompokkan menjadi folder `auth`, `dimension`, dan `starterkit`.
|
||||||
|
- Persistent Auth data dan last-position memakai satu `config/lobby/player.json`.
|
||||||
1. `config.json`
|
- Starter Kit definitions memakai `starterkit/list.json`; recipient history memakai `starterkit/recipients.json`.
|
||||||
2. Auth `config.json` + `rules.json` + `accounts.json`
|
- Dimension gamemode/gamerule memakai `dimension/gamemodes.json` dan `dimension/gamerules.json`.
|
||||||
3. `portals.json` + portal visual repair
|
- `/ls reload`, startup loader, save path, error message, dan logging menggunakan layout v1.9.0.
|
||||||
4. `dimension-gamemodes.json` + apply ke dimension/player
|
- Tidak ada automatic migration karena v1.9.0 masih berada dalam development workflow.
|
||||||
5. `dimension-gamerules.json` + apply ke loaded dimensions
|
|
||||||
6. Starter Kit data
|
|
||||||
7. Refresh runtime state yang terkait
|
|
||||||
|
|
||||||
Console menampilkan `RELOAD START` dan `RELOAD COMPLETE`. Player admin yang menjalankan command menerima ringkasan hasil reload tanpa broadcast ke player lain.
|
|
||||||
|
|
||||||
## Catatan v1.8.2
|
|
||||||
|
|
||||||
- Memperbaiki crash `ConcurrentModificationException` ketika authentication timeout men-disconnect player dari dalam server tick. Auth sekarang mengiterasi snapshot player list, bukan live `ArrayList`.
|
|
||||||
- Initial dialog ditunda singkat sampai player benar-benar tersedia sebagai target command; ini menghilangkan kegagalan awal `No player was found`.
|
|
||||||
- Dialog tidak lagi dibuka ulang setiap `reminderIntervalSeconds`, karena itu dapat menghapus password yang sedang diketik.
|
|
||||||
- Rules dan Register/Login memakai timeout independen: Rules memakai `rules.json -> timeoutSeconds`, sedangkan Register/Login memakai `auth/config.json -> authenticationTimeoutSeconds`. Timer Register/Login dimulai ulang dari penuh setelah Rules diterima.
|
|
||||||
- Rules dipindahkan ke `config/lobby/auth/rules.json` dan Rules dialog dibangun dari file tersebut.
|
|
||||||
- `rulesEnabled` dan `rulesVersion` lama dipindahkan menjadi `enabled` dan `version` di `rules.json`.
|
|
||||||
- `/ls reload` juga me-reload `rules.json` dan mengevaluasi ulang acceptance/session player online.
|
|
||||||
|
|
||||||
## Known compatibility note
|
## Known compatibility note
|
||||||
|
|
||||||
Local/cracked identity tidak memiliki Mojang profile public key. Jika server mempertahankan `enforce-secure-profile=true`, vanilla secure-chat dapat menolak normal chat local player dengan pesan missing profile public key. Masalah secure-chat ini terpisah dari keberhasilan login/authentication dan perlu compatibility handling khusus.
|
Local/cracked identity tidak mempunyai Mojang profile public key. Dengan `enforce-secure-profile=true`, vanilla secure-chat dapat menolak normal chat local player karena missing profile public key. Ini terpisah dari Auth login dan memerlukan compatibility handling khusus.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All Rights Reserved. Source dan asset Lobby Systems tidak boleh didistribusikan atau digunakan ulang di luar izin pemilik project.
|
All Rights Reserved. Source dan asset Lobby Systems tidak boleh didistribusikan atau digunakan ulang di luar izin pemilik project.
|
||||||
|
|
||||||
|
|
||||||
## Catatan v1.8.3
|
|
||||||
|
|
||||||
- Memperbaiki compile error Minecraft 26.2 karena `ServerPlayer.displayClientMessage(Component, boolean)` sudah tidak tersedia pada mapping/API target ini.
|
|
||||||
- Implementasi action-bar countdown yang diperkenalkan sementara pada v1.8.3 kemudian dihentikan pada v1.8.5 karena Dialog bersifat modal dan countdown HUD tidak memberi UX yang konsisten.
|
|
||||||
- Seluruh perubahan v1.8.2 (safe timeout iteration, delayed initial dialog, `rules.json`, dan global `/ls reload`) tetap dipertahankan.
|
|
||||||
|
|
||||||
|
|
||||||
## v1.8.4 — Two-stage auth timer
|
|
||||||
|
|
||||||
- Informasi `Rules version: N` dihapus dari UI Rules; version tetap disimpan sebagai metadata internal untuk mekanisme re-accept.
|
|
||||||
- `rules.json` memiliki `timeoutSeconds` (default `60`) untuk batas waktu menerima Rules.
|
|
||||||
- Register/Login tetap memakai `authenticationTimeoutSeconds` (default `120`).
|
|
||||||
- Setelah Rules diterima, timer Rules dihentikan dan timer Register/Login dimulai dari awal.
|
|
||||||
- Tidak ada countdown visual pada HUD/action bar; timeout berjalan di server dan hanya muncul sebagai alasan disconnect jika waktunya habis.
|
|
||||||
- Timeout Rules dan timeout authentication memiliki pesan disconnect yang berbeda.
|
|
||||||
- `/ls reload` tetap memuat ulang seluruh konfigurasi Lobby Systems, termasuk `auth/rules.json` dan `auth/config.json`.
|
|
||||||
|
|
||||||
|
|
||||||
## v1.8.5 — Auth timeout messages & premium login policy
|
|
||||||
|
|
||||||
- Menghapus countdown visual dari action bar. Dialog Rules/Register/Login tetap bersih tanpa HUD timer yang tertutup layer Dialog.
|
|
||||||
- Two-stage timeout tetap dipertahankan secara server-side: Rules memakai `rules.json -> timeoutSeconds`, sedangkan Register/Login memakai `auth/config.json -> authenticationTimeoutSeconds`.
|
|
||||||
- `rules.json` sekarang memiliki `timeoutMessage`. Placeholder `{seconds}` otomatis diganti dengan nilai `timeoutSeconds`.
|
|
||||||
- `auth/config.json` sekarang memiliki `registrationTimeoutMessage`, `loginTimeoutMessage`, dan fallback `authenticationTimeoutMessage`; placeholder `{seconds}` memakai `authenticationTimeoutSeconds`.
|
|
||||||
- `premiumAutoLogin=false` sekarang benar-benar mematikan auto-login Lobby Systems untuk premium player. Mojang authentication tetap berjalan, kemudian premium player diarahkan ke Register/Login Dialog dan membutuhkan password Lobby Systems.
|
|
||||||
- `premiumAutoLogin=true` + `requireLoginEveryJoin=false` tetap merupakan mode premium auto-login. `requireLoginEveryJoin=true` tetap memaksa password Lobby Systems untuk premium player setiap join.
|
|
||||||
- `/ls reload` memuat ulang policy dan pesan timeout Auth/Rules tanpa restart server.
|
|
||||||
|
|||||||
+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.8.5
|
mod_version=1.9.0
|
||||||
maven_group=id.dartokloning
|
maven_group=id.dartokloning
|
||||||
archives_base_name=lobby-systems
|
archives_base_name=lobby-systems
|
||||||
|
|||||||
@@ -62,17 +62,11 @@ public final class AuthManager {
|
|||||||
}
|
}
|
||||||
saveRules(); // creates rules.json on first run and writes newly-added defaults
|
saveRules(); // creates rules.json on first run and writes newly-added defaults
|
||||||
|
|
||||||
if (Files.exists(ConfigPaths.AUTH_ACCOUNTS)) {
|
Map<String, Account> loadedAccounts = PlayerDataStore.loadAuth(
|
||||||
try (Reader r = Files.newBufferedReader(ConfigPaths.AUTH_ACCOUNTS)) {
|
new TypeToken<Map<String, Account>>(){}.getType(), new HashMap<>());
|
||||||
Map<String, Account> loaded = GSON.fromJson(r, new TypeToken<Map<String, Account>>(){}.getType());
|
accounts.clear();
|
||||||
if (loaded != null) {
|
if (loadedAccounts != null) accounts.putAll(loadedAccounts);
|
||||||
accounts.clear();
|
if (!Files.exists(ConfigPaths.PLAYER)) saveAccounts();
|
||||||
accounts.putAll(loaded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
saveAccounts();
|
|
||||||
}
|
|
||||||
migrateAccounts();
|
migrateAccounts();
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -563,11 +557,8 @@ public final class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void saveAccounts() {
|
private void saveAccounts() {
|
||||||
try {
|
synchronized (accounts) {
|
||||||
Files.createDirectories(ConfigPaths.AUTH_DIRECTORY);
|
PlayerDataStore.saveAuth(accounts, new TypeToken<Map<String, Account>>(){}.getType());
|
||||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); }
|
|
||||||
} catch (Exception e) {
|
|
||||||
LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,20 @@ import java.nio.file.Path;
|
|||||||
public final class ConfigPaths {
|
public final class ConfigPaths {
|
||||||
public static final Path DIRECTORY = FabricLoader.getInstance().getConfigDir().resolve("lobby");
|
public static final Path DIRECTORY = FabricLoader.getInstance().getConfigDir().resolve("lobby");
|
||||||
public static final Path CONFIG = DIRECTORY.resolve("config.json");
|
public static final Path CONFIG = DIRECTORY.resolve("config.json");
|
||||||
public static final Path PLAYER_POSITIONS = DIRECTORY.resolve("player-positions.json");
|
public static final Path PLAYER = DIRECTORY.resolve("player.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_GAMERULES = DIRECTORY.resolve("dimension-gamerules.json");
|
|
||||||
public static final Path STARTER_KITS = DIRECTORY.resolve("starterkits.json");
|
|
||||||
public static final Path STARTER_KIT_RECIPIENTS = DIRECTORY.resolve("starterkit-recipients.json");
|
|
||||||
public static final Path AUTH_DIRECTORY = DIRECTORY.resolve("auth");
|
public static final Path AUTH_DIRECTORY = DIRECTORY.resolve("auth");
|
||||||
public static final Path AUTH_CONFIG = AUTH_DIRECTORY.resolve("config.json");
|
public static final Path AUTH_CONFIG = AUTH_DIRECTORY.resolve("config.json");
|
||||||
public static final Path AUTH_ACCOUNTS = AUTH_DIRECTORY.resolve("accounts.json");
|
|
||||||
public static final Path AUTH_RULES = AUTH_DIRECTORY.resolve("rules.json");
|
public static final Path AUTH_RULES = AUTH_DIRECTORY.resolve("rules.json");
|
||||||
|
|
||||||
|
public static final Path DIMENSION_DIRECTORY = DIRECTORY.resolve("dimension");
|
||||||
|
public static final Path DIMENSION_GAMEMODES = DIMENSION_DIRECTORY.resolve("gamemodes.json");
|
||||||
|
public static final Path DIMENSION_GAMERULES = DIMENSION_DIRECTORY.resolve("gamerules.json");
|
||||||
|
|
||||||
|
public static final Path STARTER_KIT_DIRECTORY = DIRECTORY.resolve("starterkit");
|
||||||
|
public static final Path STARTER_KITS = STARTER_KIT_DIRECTORY.resolve("list.json");
|
||||||
|
public static final Path STARTER_KIT_RECIPIENTS = STARTER_KIT_DIRECTORY.resolve("recipients.json");
|
||||||
|
|
||||||
private ConfigPaths() {}
|
private ConfigPaths() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Persistent per-dimension gamerule overrides owned by Lobby Systems.
|
* Persistent per-dimension gamerule overrides owned by Lobby Systems.
|
||||||
*
|
*
|
||||||
* Values are stored in config/lobby/dimension-gamerules.json and are applied
|
* Values are stored in config/lobby/dimension/gamerules.json and are applied
|
||||||
* directly to each ServerLevel's independent GameRules instance.
|
* directly to each ServerLevel's independent GameRules instance.
|
||||||
*/
|
*/
|
||||||
public final class DimensionGameRuleManager {
|
public final class DimensionGameRuleManager {
|
||||||
@@ -31,7 +31,7 @@ public final class DimensionGameRuleManager {
|
|||||||
public boolean load() {
|
public boolean load() {
|
||||||
configured.clear();
|
configured.clear();
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(ConfigPaths.DIRECTORY);
|
Files.createDirectories(ConfigPaths.DIMENSION_GAMERULES.getParent());
|
||||||
if (!Files.exists(ConfigPaths.DIMENSION_GAMERULES)) {
|
if (!Files.exists(ConfigPaths.DIMENSION_GAMERULES)) {
|
||||||
save();
|
save();
|
||||||
return true;
|
return true;
|
||||||
@@ -45,7 +45,7 @@ public final class DimensionGameRuleManager {
|
|||||||
|
|
||||||
JsonElement rootElement = JsonParser.parseString(json);
|
JsonElement rootElement = JsonParser.parseString(json);
|
||||||
if (!rootElement.isJsonObject()) {
|
if (!rootElement.isJsonObject()) {
|
||||||
LobbySystemsMod.LOGGER.error("dimension-gamerules.json root must be a JSON object");
|
LobbySystemsMod.LOGGER.error("dimension/gamerules.json root must be a JSON object");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ public final class DimensionGameRuleManager {
|
|||||||
|
|
||||||
public boolean save() {
|
public boolean save() {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(ConfigPaths.DIRECTORY);
|
Files.createDirectories(ConfigPaths.DIMENSION_GAMERULES.getParent());
|
||||||
JsonObject root = new JsonObject();
|
JsonObject root = new JsonObject();
|
||||||
configured.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(dimensionEntry -> {
|
configured.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(dimensionEntry -> {
|
||||||
JsonObject rules = new JsonObject();
|
JsonObject rules = new JsonObject();
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package id.dartokloning.lobbysystems;
|
|
||||||
|
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Satu-satunya tempat yang mengetahui nama file konfigurasi versi lama.
|
|
||||||
* Runtime saat ini hanya membaca/menulis file di config/lobby/.
|
|
||||||
*/
|
|
||||||
public final class LegacyConfigMigrator {
|
|
||||||
private LegacyConfigMigrator() {}
|
|
||||||
|
|
||||||
public static void migrate() {
|
|
||||||
Path oldConfig = FabricLoader.getInstance().getConfigDir().resolve("lobby-systems.json");
|
|
||||||
Path oldPositions = FabricLoader.getInstance().getConfigDir().resolve("lobby-systems-positions.json");
|
|
||||||
|
|
||||||
migrateOne(oldConfig, ConfigPaths.CONFIG, "configuration");
|
|
||||||
migrateOne(oldPositions, ConfigPaths.PLAYER_POSITIONS, "player positions");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void migrateOne(Path source, Path target, String label) {
|
|
||||||
if (!Files.exists(source) || Files.exists(target)) return;
|
|
||||||
try {
|
|
||||||
Files.createDirectories(target.getParent());
|
|
||||||
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
LobbySystemsMod.LOGGER.info("Migrated legacy {}: {} -> {}", label, source, target);
|
|
||||||
LobbyLog.state("LEGACY CONFIG MIGRATED", "Type: " + label, "From: " + source, "To: " + target);
|
|
||||||
} catch (Exception moveError) {
|
|
||||||
try {
|
|
||||||
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
LobbySystemsMod.LOGGER.info("Copied legacy {}: {} -> {}", label, source, target);
|
|
||||||
LobbyLog.state("LEGACY CONFIG COPIED", "Type: " + label, "From: " + source, "To: " + target);
|
|
||||||
} catch (Exception copyError) {
|
|
||||||
LobbySystemsMod.LOGGER.error("Failed to migrate legacy {} from {} to {}", label, source, target, copyError);
|
|
||||||
LobbyLog.state("LEGACY MIGRATION FAILED", "Type: " + label, "From: " + source, "To: " + target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -82,8 +82,6 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
LobbyLog.startup();
|
LobbyLog.startup();
|
||||||
|
|
||||||
LegacyConfigMigrator.migrate();
|
|
||||||
|
|
||||||
boolean configLoaded = CONFIG.load();
|
boolean configLoaded = CONFIG.load();
|
||||||
LobbyLog.state(
|
LobbyLog.state(
|
||||||
configLoaded ? "CONFIG LOADED" : "CONFIG LOAD FAILED",
|
configLoaded ? "CONFIG LOADED" : "CONFIG LOAD FAILED",
|
||||||
@@ -95,7 +93,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
LobbyLog.state(
|
LobbyLog.state(
|
||||||
authLoaded ? "AUTH SYSTEM READY" : "AUTH SYSTEM LOAD FAILED",
|
authLoaded ? "AUTH SYSTEM READY" : "AUTH SYSTEM LOAD FAILED",
|
||||||
"Config: " + ConfigPaths.AUTH_CONFIG,
|
"Config: " + ConfigPaths.AUTH_CONFIG,
|
||||||
"Accounts: " + ConfigPaths.AUTH_ACCOUNTS,
|
"Player database: " + ConfigPaths.PLAYER,
|
||||||
"Rules: " + ConfigPaths.AUTH_RULES,
|
"Rules: " + ConfigPaths.AUTH_RULES,
|
||||||
"Enabled: " + AUTH.config().enabled,
|
"Enabled: " + AUTH.config().enabled,
|
||||||
"Hybrid online-mode support: enabled"
|
"Hybrid online-mode support: enabled"
|
||||||
@@ -105,7 +103,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
POSITIONS.setExcludedDimension(lobbyKey());
|
POSITIONS.setExcludedDimension(lobbyKey());
|
||||||
LobbyLog.state(
|
LobbyLog.state(
|
||||||
"PLAYER POSITIONS READY",
|
"PLAYER POSITIONS READY",
|
||||||
"File: " + ConfigPaths.PLAYER_POSITIONS,
|
"File: " + ConfigPaths.PLAYER,
|
||||||
"Lobby persistence: excluded"
|
"Lobby persistence: excluded"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -572,7 +570,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
"Portals: " + ConfigPaths.PORTALS,
|
"Portals: " + ConfigPaths.PORTALS,
|
||||||
"Auth config: " + ConfigPaths.AUTH_CONFIG,
|
"Auth config: " + ConfigPaths.AUTH_CONFIG,
|
||||||
"Auth rules: " + ConfigPaths.AUTH_RULES,
|
"Auth rules: " + ConfigPaths.AUTH_RULES,
|
||||||
"Auth accounts: " + ConfigPaths.AUTH_ACCOUNTS
|
"Player database: " + ConfigPaths.PLAYER
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!CONFIG.load()) {
|
if (!CONFIG.load()) {
|
||||||
@@ -596,12 +594,14 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!AUTH.load()) {
|
if (!AUTH.load()) {
|
||||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi auth config/rules/accounts bermasalah. Cek console."));
|
source.sendFailure(Component.literal("§eConfig utama direload, tetapi auth config/rules/player database bermasalah. Cek console."));
|
||||||
LobbyLog.state("RELOAD FAILED", "Reason: auth config/rules/accounts gagal dibaca");
|
LobbyLog.state("RELOAD FAILED", "Reason: auth config/rules/player database gagal dibaca");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
AUTH.applyReload(server);
|
AUTH.applyReload(server);
|
||||||
|
|
||||||
|
// player.json is shared by Auth and last-position; reload both sections.
|
||||||
|
POSITIONS.load();
|
||||||
POSITIONS.setExcludedDimension(key);
|
POSITIONS.setExcludedDimension(key);
|
||||||
boolean portalOk = PORTALS.load();
|
boolean portalOk = PORTALS.load();
|
||||||
if (!portalOk) {
|
if (!portalOk) {
|
||||||
@@ -613,8 +613,8 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
|
|
||||||
boolean gameModesOk = DIMENSION_GAMEMODES.load();
|
boolean gameModesOk = DIMENSION_GAMEMODES.load();
|
||||||
if (!gameModesOk) {
|
if (!gameModesOk) {
|
||||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension-gamemodes.json bermasalah. Cek console."));
|
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension/gamemodes.json bermasalah. Cek console."));
|
||||||
LobbyLog.state("RELOAD FAILED", "Reason: dimension-gamemodes.json gagal dibaca");
|
LobbyLog.state("RELOAD FAILED", "Reason: dimension/gamemodes.json gagal dibaca");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
for (String dimension : DIMENSION_GAMEMODES.all().keySet()) {
|
for (String dimension : DIMENSION_GAMEMODES.all().keySet()) {
|
||||||
@@ -625,16 +625,16 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
|
|
||||||
boolean gameRulesOk = DIMENSION_GAMERULES.load();
|
boolean gameRulesOk = DIMENSION_GAMERULES.load();
|
||||||
if (!gameRulesOk) {
|
if (!gameRulesOk) {
|
||||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension-gamerules.json bermasalah. Cek console."));
|
source.sendFailure(Component.literal("§eConfig utama direload, tetapi dimension/gamerules.json bermasalah. Cek console."));
|
||||||
LobbyLog.state("RELOAD FAILED", "Reason: dimension-gamerules.json gagal dibaca");
|
LobbyLog.state("RELOAD FAILED", "Reason: dimension/gamerules.json gagal dibaca");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
DimensionGameRuleManager.ApplySummary gameRuleSummary = DIMENSION_GAMERULES.applyAll(server);
|
DimensionGameRuleManager.ApplySummary gameRuleSummary = DIMENSION_GAMERULES.applyAll(server);
|
||||||
|
|
||||||
boolean starterKitsOk = STARTER_KITS.load();
|
boolean starterKitsOk = STARTER_KITS.load();
|
||||||
if (!starterKitsOk) {
|
if (!starterKitsOk) {
|
||||||
source.sendFailure(Component.literal("§estarterkits.json atau starterkit-recipients.json bermasalah. Cek console."));
|
source.sendFailure(Component.literal("§estarterkit/list.json atau starterkit/recipients.json bermasalah. Cek console."));
|
||||||
LobbyLog.state("RELOAD FAILED", "Reason: starter kit data gagal dibaca");
|
LobbyLog.state("RELOAD FAILED", "Reason: starterkit/list.json atau starterkit/recipients.json gagal dibaca");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,6 +653,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size() + " (authoritative)",
|
"Dimension gamemodes: " + DIMENSION_GAMEMODES.all().size() + " (authoritative)",
|
||||||
"Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied",
|
"Dimension gamerules: " + DIMENSION_GAMERULES.all().size() + " dimensions / " + gameRuleSummary.applied() + " rules applied",
|
||||||
"Starter kits: " + STARTER_KITS.size() + " / active dimensions: " + STARTER_KITS.activeCount(),
|
"Starter kits: " + STARTER_KITS.size() + " / active dimensions: " + STARTER_KITS.activeCount(),
|
||||||
|
"Player database: " + ConfigPaths.PLAYER,
|
||||||
"Player positions lobby exclusion: active"
|
"Player positions lobby exclusion: active"
|
||||||
);
|
);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -780,7 +781,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
String normalizedRule = rule.getIdentifierWithFallback().toString();
|
String normalizedRule = rule.getIdentifierWithFallback().toString();
|
||||||
String actualValue = gameRuleValueUnchecked(level, rule);
|
String actualValue = gameRuleValueUnchecked(level, rule);
|
||||||
if (!DIMENSION_GAMERULES.set(normalizedDimension, normalizedRule, actualValue)) {
|
if (!DIMENSION_GAMERULES.set(normalizedDimension, normalizedRule, actualValue)) {
|
||||||
source.sendFailure(Component.literal("§cGamerule sudah diterapkan di memory, tetapi gagal menyimpan dimension-gamerules.json."));
|
source.sendFailure(Component.literal("§cGamerule sudah diterapkan di memory, tetapi gagal menyimpan dimension/gamerules.json."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,7 +870,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
String normalizedDimension = level.dimension().identifier().toString();
|
String normalizedDimension = level.dimension().identifier().toString();
|
||||||
String normalizedMode = rawMode.trim().toLowerCase();
|
String normalizedMode = rawMode.trim().toLowerCase();
|
||||||
if (!DIMENSION_GAMEMODES.set(normalizedDimension, normalizedMode)) {
|
if (!DIMENSION_GAMEMODES.set(normalizedDimension, normalizedMode)) {
|
||||||
source.sendFailure(Component.literal("§cGagal menyimpan dimension-gamemodes.json. Cek console."));
|
source.sendFailure(Component.literal("§cGagal menyimpan dimension/gamemodes.json. Cek console."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int applied = DIMENSION_GAMEMODES.applyToDimension(server, normalizedDimension);
|
int applied = DIMENSION_GAMEMODES.applyToDimension(server, normalizedDimension);
|
||||||
@@ -906,7 +907,7 @@ public final class LobbySystemsMod implements ModInitializer {
|
|||||||
"COMPATIBILITY WARNING",
|
"COMPATIBILITY WARNING",
|
||||||
"Dimensional Inventories: detected",
|
"Dimensional Inventories: detected",
|
||||||
"Lobby Systems dimension gamemode management: authoritative / always enabled",
|
"Lobby Systems dimension gamemode management: authoritative / always enabled",
|
||||||
"Lobby Systems will enforce dimension-gamemodes.json on join and dimension changes.",
|
"Lobby Systems will enforce dimension/gamemodes.json on join and dimension changes.",
|
||||||
"Keep Dimensional Inventories pool gamemodes aligned (or disable its gamemode handling) to avoid conflicting transitions."
|
"Keep Dimensional Inventories pool gamemodes aligned (or disable its gamemode handling) to avoid conflicting transitions."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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 java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared persistent player database. Auth and last-position data intentionally
|
||||||
|
* live in one file while remaining separate logical sections, so either
|
||||||
|
* subsystem can update its own data without overwriting the other.
|
||||||
|
*/
|
||||||
|
public final class PlayerDataStore {
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
private static final String AUTH = "auth";
|
||||||
|
private static final String POSITIONS = "positions";
|
||||||
|
|
||||||
|
private PlayerDataStore() {}
|
||||||
|
|
||||||
|
public static <T> T loadAuth(Type type, T fallback) {
|
||||||
|
return loadSection(AUTH, type, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T loadPositions(Type type, T fallback) {
|
||||||
|
return loadSection(POSITIONS, type, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveAuth(Object value, Type type) {
|
||||||
|
saveSection(AUTH, value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void savePositions(Object value, Type type) {
|
||||||
|
saveSection(POSITIONS, value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T loadSection(String name, Type type, T fallback) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
try {
|
||||||
|
JsonObject root = readRoot();
|
||||||
|
JsonElement section = root.get(name);
|
||||||
|
if (section == null || section.isJsonNull()) return fallback;
|
||||||
|
T loaded = GSON.fromJson(section, type);
|
||||||
|
return loaded == null ? fallback : loaded;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LobbySystemsMod.LOGGER.error("Failed to load '{}' from {}", name, ConfigPaths.PLAYER, e);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveSection(String name, Object value, Type type) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
try {
|
||||||
|
JsonObject root = readRoot();
|
||||||
|
root.add(name, GSON.toJsonTree(value, type));
|
||||||
|
Files.createDirectories(ConfigPaths.PLAYER.getParent());
|
||||||
|
try (Writer writer = Files.newBufferedWriter(ConfigPaths.PLAYER)) {
|
||||||
|
GSON.toJson(root, writer);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LobbySystemsMod.LOGGER.error("Failed to save '{}' to {}", name, ConfigPaths.PLAYER, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject readRoot() throws Exception {
|
||||||
|
if (!Files.exists(ConfigPaths.PLAYER)) return new JsonObject();
|
||||||
|
try (Reader reader = Files.newBufferedReader(ConfigPaths.PLAYER)) {
|
||||||
|
JsonElement parsed = JsonParser.parseReader(reader);
|
||||||
|
if (parsed == null || parsed.isJsonNull()) return new JsonObject();
|
||||||
|
if (!parsed.isJsonObject()) throw new IllegalArgumentException("player.json root must be a JSON object");
|
||||||
|
return parsed.getAsJsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
package id.dartokloning.lobbysystems;
|
package id.dartokloning.lobbysystems;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.io.Writer;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public final class PositionManager {
|
public final class PositionManager {
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
private static final Path PATH = ConfigPaths.PLAYER_POSITIONS;
|
|
||||||
private static final Type DATA_TYPE = new TypeToken<Map<String, Map<String, PlayerPosition>>>() {}.getType();
|
private static final Type DATA_TYPE = new TypeToken<Map<String, Map<String, PlayerPosition>>>() {}.getType();
|
||||||
|
|
||||||
private final Map<String, Map<String, PlayerPosition>> positions = new HashMap<>();
|
private final Map<String, Map<String, PlayerPosition>> positions = new HashMap<>();
|
||||||
@@ -28,17 +20,8 @@ public final class PositionManager {
|
|||||||
|
|
||||||
public void load() {
|
public void load() {
|
||||||
positions.clear();
|
positions.clear();
|
||||||
if (!Files.exists(PATH)) {
|
Map<String, Map<String, PlayerPosition>> loaded = PlayerDataStore.loadPositions(DATA_TYPE, new HashMap<>());
|
||||||
save();
|
if (loaded != null) positions.putAll(loaded);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Reader reader = Files.newBufferedReader(PATH)) {
|
|
||||||
Map<String, Map<String, PlayerPosition>> loaded = GSON.fromJson(reader, DATA_TYPE);
|
|
||||||
if (loaded != null) positions.putAll(loaded);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LobbySystemsMod.LOGGER.error("Failed to load player positions from {}", PATH, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,15 +73,8 @@ public final class PositionManager {
|
|||||||
// Guard terakhir: walaupun ada bug di caller, data lobby tidak boleh masuk file.
|
// Guard terakhir: walaupun ada bug di caller, data lobby tidak boleh masuk file.
|
||||||
purgeExcludedDimension();
|
purgeExcludedDimension();
|
||||||
|
|
||||||
try {
|
PlayerDataStore.savePositions(positions, DATA_TYPE);
|
||||||
Files.createDirectories(PATH.getParent());
|
dirty = false;
|
||||||
try (Writer writer = Files.newBufferedWriter(PATH)) {
|
|
||||||
GSON.toJson(positions, DATA_TYPE, writer);
|
|
||||||
}
|
|
||||||
dirty = false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
LobbySystemsMod.LOGGER.error("Failed to save player positions to {}", PATH, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void store(UUID uuid, ResourceKey<Level> dimension, PlayerPosition position) {
|
private void store(UUID uuid, ResourceKey<Level> dimension, PlayerPosition position) {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ public final class StarterKitManager {
|
|||||||
if (wasActive && !previousDimension.equals(dimension)) {
|
if (wasActive && !previousDimension.equals(dimension)) {
|
||||||
config.activeByDimension.remove(previousDimension);
|
config.activeByDimension.remove(previousDimension);
|
||||||
}
|
}
|
||||||
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
|
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkit/list.json.");
|
||||||
|
|
||||||
String status = name.equals(config.activeByDimension.get(dimension)) ? "ACTIVE" : "INACTIVE";
|
String status = name.equals(config.activeByDimension.get(dimension)) ? "ACTIVE" : "INACTIVE";
|
||||||
if (wasActive && !previousDimension.equals(dimension)) {
|
if (wasActive && !previousDimension.equals(dimension)) {
|
||||||
@@ -268,7 +268,7 @@ public final class StarterKitManager {
|
|||||||
if (kit == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
|
if (kit == null) return OperationResult.fail("Starter kit '" + name + "' tidak ditemukan.");
|
||||||
String dimension = normalizeDimension(kit.dimension);
|
String dimension = normalizeDimension(kit.dimension);
|
||||||
String previous = config.activeByDimension.put(dimension, name);
|
String previous = config.activeByDimension.put(dimension, name);
|
||||||
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
|
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkit/list.json.");
|
||||||
if (name.equals(previous)) return OperationResult.ok("Starter kit '" + name + "' sudah ACTIVE untuk " + dimension + ".");
|
if (name.equals(previous)) return OperationResult.ok("Starter kit '" + name + "' sudah ACTIVE untuk " + dimension + ".");
|
||||||
return OperationResult.ok("Starter kit '" + name + "' sekarang ACTIVE untuk " + dimension
|
return OperationResult.ok("Starter kit '" + name + "' sekarang ACTIVE untuk " + dimension
|
||||||
+ (previous == null ? "." : ". Kit sebelumnya: '" + previous + "' -> INACTIVE."));
|
+ (previous == null ? "." : ". Kit sebelumnya: '" + previous + "' -> INACTIVE."));
|
||||||
@@ -283,7 +283,7 @@ public final class StarterKitManager {
|
|||||||
return OperationResult.ok("Starter kit '" + name + "' sudah INACTIVE.");
|
return OperationResult.ok("Starter kit '" + name + "' sudah INACTIVE.");
|
||||||
}
|
}
|
||||||
config.activeByDimension.remove(dimension);
|
config.activeByDimension.remove(dimension);
|
||||||
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
|
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkit/list.json.");
|
||||||
return OperationResult.ok("Starter kit '" + name + "' sekarang INACTIVE. Dimension " + dimension + " tidak memiliki active starter kit.");
|
return OperationResult.ok("Starter kit '" + name + "' sekarang INACTIVE. Dimension " + dimension + " tidak memiliki active starter kit.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ public final class StarterKitManager {
|
|||||||
String dimension = normalizeDimension(removed.dimension);
|
String dimension = normalizeDimension(removed.dimension);
|
||||||
boolean wasActive = name.equals(config.activeByDimension.get(dimension));
|
boolean wasActive = name.equals(config.activeByDimension.get(dimension));
|
||||||
if (wasActive) config.activeByDimension.remove(dimension);
|
if (wasActive) config.activeByDimension.remove(dimension);
|
||||||
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkits.json.");
|
if (!saveKits()) return OperationResult.fail("Gagal menyimpan starterkit/list.json.");
|
||||||
return OperationResult.ok("Starter kit '" + name + "' dihapus" + (wasActive ? " dan active assignment untuk " + dimension + " dilepas." : "."));
|
return OperationResult.ok("Starter kit '" + name + "' dihapus" + (wasActive ? " dan active assignment untuk " + dimension + " dilepas." : "."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ public final class StarterKitManager {
|
|||||||
return OperationResult.ok("Player belum memiliki recipient history untuk " + dimension + ".");
|
return OperationResult.ok("Player belum memiliki recipient history untuk " + dimension + ".");
|
||||||
}
|
}
|
||||||
if (byDimension.isEmpty()) recipients.remove(uuid.toString());
|
if (byDimension.isEmpty()) recipients.remove(uuid.toString());
|
||||||
if (!saveRecipients()) return OperationResult.fail("Gagal menyimpan starterkit-recipients.json.");
|
if (!saveRecipients()) return OperationResult.fail("Gagal menyimpan starterkit/recipients.json.");
|
||||||
return OperationResult.ok("Recipient history untuk " + dimension + " sudah di-reset.");
|
return OperationResult.ok("Recipient history untuk " + dimension + " sudah di-reset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user