auth gui
This commit is contained in:
@@ -1,244 +1,159 @@
|
||||
# Lobby Systems v1.7.0 — Fabric 26.2
|
||||
# Lobby Systems v1.8.1 — Fabric 26.2
|
||||
|
||||
Server-side lobby routing dan dimension policy untuk Minecraft 26.2/Fabric. v1.6.0 menambahkan Starter Kit per-dimension dengan active assignment dan recipient history, sambil mempertahankan initial Lobby placement pre-login, gamemode authoritative, gamerule per-dimension, portal, dan position persistence.
|
||||
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.
|
||||
|
||||
**v1.5.6 login placement fix (dipertahankan):** player data tetap dibaca untuk menyimpan posisi gameplay terakhir, tetapi sebelum `PlayerList` mengirim initial world state, Lobby Systems mengganti initial level, posisi, dan yaw/pitch ke konfigurasi Lobby. Gamemode disinkronkan segera setelah JOIN dan dikoreksi singkat pada +1, +5, dan +20 tick.
|
||||
## Target
|
||||
|
||||
**v1.5.7 behavior update:** Lobby Systems sekarang selalu menjadi source of truth untuk gamemode per-dimension. Opsi `manageDimensionGamemode` dihapus; config lama akan dimigrasikan otomatis dengan membuang field tersebut. Command `list`, `set`, dan `remove` selalu tersedia. Compatibility warning Dimensional Inventories tetap aktif.
|
||||
|
||||
**v1.6.0 Starter Kit:** operator dapat menyimpan snapshot inventory/equipment sebagai kit, mengaktifkan satu kit per dimension, memuat kit untuk diedit, memberi kit secara manual, dan mengelola recipient history. Automatic starter kit diberikan satu kali per player per dimension setelah transisi inventory destination stabil. Mengganti active kit tidak memberi ulang kepada player yang sudah pernah menerima starter kit di dimension tersebut.
|
||||
|
||||
### Reload feedback
|
||||
|
||||
`/ls reload` menulis banner `RELOAD START` dan `RELOAD COMPLETE` hanya ke console server. Ringkasan reload satu baris dikirim langsung hanya ke player admin/operator yang menjalankan command; ringkasan tersebut tidak dibroadcast ke player lain dan tidak dikirim melalui command feedback ke console.
|
||||
|
||||
|
||||
## Target build
|
||||
|
||||
- Minecraft: 26.2
|
||||
- Fabric Loader: 0.19.3
|
||||
- Fabric API: 0.156.0+26.2
|
||||
- Fabric Loom: 1.17
|
||||
- Java: 25+
|
||||
- Minecraft: **26.2**
|
||||
- Java: **25**
|
||||
- Fabric Loader: **0.19.3**
|
||||
- Fabric API: **0.156.0+26.2**
|
||||
- Fabric Loom: **1.17**
|
||||
- Mod ID: `dartokloning`
|
||||
- Java namespace: `id.dartokloning.lobbysystems`
|
||||
- WorldEdit: opsional untuk runtime portal, diperlukan saat membuat portal memakai `//wand`
|
||||
- Portal Visual System v1.5.0: memakai block state vanilla `minecraft:nether_portal`, sehingga tidak membutuhkan mod/resource pack di client
|
||||
- Namespace: `id.dartokloning.lobbysystems`
|
||||
- License: All Rights Reserved
|
||||
|
||||
## Compile di Windows
|
||||
Lobby Systems adalah mod server-side. WorldEdit Fabric diperlukan untuk workflow pembuatan portal dari selection.
|
||||
|
||||
Double-click `compile.bat` atau jalankan:
|
||||
## Build
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
.\compile.bat
|
||||
```
|
||||
|
||||
Linux/macOS:
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
chmod +x compile.sh release.sh
|
||||
./compile.sh
|
||||
```
|
||||
|
||||
`compile.bat` dan `compile.sh` membaca `mod_version` dan `minecraft_version` langsung dari `gradle.properties`. Setelah `BUILD SUCCESS`, script akan menanyakan apakah source ingin di-commit dan di-push ke Git/Gitea. Header build akan menampilkan versi yang sedang di-compile, misalnya:
|
||||
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.
|
||||
|
||||
## Struktur konfigurasi
|
||||
|
||||
```text
|
||||
============================================
|
||||
Lobby Systems - Fabric 26.2 Build
|
||||
Version: 1.5.5
|
||||
Namespace: id.dartokloning.lobbysystems
|
||||
Mod ID: dartokloning
|
||||
============================================
|
||||
```
|
||||
|
||||
Output utama:
|
||||
|
||||
```text
|
||||
build/libs/lobby-systems-1.5.5.jar
|
||||
```
|
||||
|
||||
`*-sources.jar` hanya source archive dan tidak perlu dipasang ke server.
|
||||
|
||||
|
||||
## Membuat Gitea Release
|
||||
|
||||
Gunakan `release.bat` di Windows atau `./release.sh` di Linux/macOS. Release script akan:
|
||||
|
||||
1. menjalankan build melalui mode `--build-only`;
|
||||
2. memastikan source sudah di-commit/push;
|
||||
3. membuat/push tag `v<mod_version>`;
|
||||
4. membuat Gitea Release melalui API;
|
||||
5. mengupload `build/libs/lobby-systems-<version>.jar` sebagai release asset.
|
||||
|
||||
Default repository release:
|
||||
|
||||
```text
|
||||
https://git.darto.id/dartokloning/lobby-systems
|
||||
```
|
||||
|
||||
API release membutuhkan Gitea Personal Access Token. Jangan simpan token di repository. Anda dapat memasukkan token ketika script meminta, atau menyediakan environment variable `GITEA_TOKEN`.
|
||||
|
||||
## Console state logging
|
||||
|
||||
State penting Lobby Systems memakai separator agar mudah terlihat di server console, antara lain startup, config load, player-position store ready, portal load, reload, player join/disconnect, portal create/delete, dan teleport. Contoh:
|
||||
|
||||
```text
|
||||
============================================
|
||||
Lobby Systems - READY
|
||||
Version: 1.5.5
|
||||
Minecraft: 26.2
|
||||
Config directory: config/lobby
|
||||
WorldEdit: available
|
||||
============================================
|
||||
```
|
||||
|
||||
## Struktur konfigurasi v1.5.7
|
||||
|
||||
```text
|
||||
config/
|
||||
└── lobby/
|
||||
config/lobby/
|
||||
├── config.json
|
||||
├── player-positions.json
|
||||
├── portals.json
|
||||
├── dimension-gamemodes.json
|
||||
├── dimension-gamerules.json
|
||||
├── starterkits.json
|
||||
├── starterkit-recipients.json
|
||||
└── auth/
|
||||
├── config.json
|
||||
├── player-positions.json
|
||||
├── portals.json
|
||||
├── dimension-gamemodes.json
|
||||
├── dimension-gamerules.json
|
||||
├── starterkits.json
|
||||
└── starterkit-recipients.json
|
||||
└── accounts.json
|
||||
```
|
||||
|
||||
### `config/lobby/config.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 `rulesVersion`, `rulesEnabled`, `requireLoginEveryJoin`, dan `enabled` tidak memerlukan restart server.
|
||||
|
||||
Contoh:
|
||||
## Lobby dan login placement
|
||||
|
||||
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.
|
||||
|
||||
`/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.
|
||||
|
||||
## Authentication System
|
||||
|
||||
Server tetap menggunakan:
|
||||
|
||||
```properties
|
||||
online-mode=true
|
||||
```
|
||||
|
||||
Lobby Systems kemudian menyediakan hybrid authentication:
|
||||
|
||||
- **Premium**: tetap melalui official Mojang/vanilla authentication.
|
||||
- **Local/cracked**: menggunakan deterministic offline UUID dan password Lobby Systems.
|
||||
- Username premium tidak boleh diregistrasikan sebagai local account.
|
||||
- Password disimpan sebagai PBKDF2-HMAC-SHA256 dengan random salt; plaintext password tidak disimpan.
|
||||
|
||||
### Dialog flow
|
||||
|
||||
Authentication v1.8 menggunakan vanilla Minecraft Dialog UI sehingga normal player tidak perlu mengetik `/register` atau `/login`.
|
||||
|
||||
First join dapat mengikuti:
|
||||
|
||||
```text
|
||||
Connect
|
||||
-> Server Rules (jika aktif/belum accepted)
|
||||
-> Create Account (jika local password diperlukan)
|
||||
-> Authenticated
|
||||
```
|
||||
|
||||
Returning local player:
|
||||
|
||||
```text
|
||||
Connect -> Rules jika versi berubah -> Sign In -> Authenticated
|
||||
```
|
||||
|
||||
`/register` dan `/login` tetap tersedia sebagai fallback compatibility.
|
||||
|
||||
### `requireLoginEveryJoin`
|
||||
|
||||
Default:
|
||||
|
||||
```json
|
||||
{
|
||||
"lobbyDimension": "minecraft:lobby",
|
||||
"lobbyX": -12.5,
|
||||
"lobbyY": 65.0,
|
||||
"lobbyZ": -3.5,
|
||||
"lobbyYaw": 0.0,
|
||||
"lobbyPitch": 0.0,
|
||||
"minimumPlaySeconds": 300,
|
||||
"portalCheckIntervalTicks": 5,
|
||||
"portalCooldownTicks": 60,
|
||||
"inLobbyMessage": "§eKamu sudah berada di lobby.",
|
||||
"returnedMessage": "§aKamu telah kembali ke lobby.",
|
||||
"waitMessage": "§cKamu belum bisa kembali ke lobby. Tunggu §e{time}§c lagi."
|
||||
}
|
||||
"requireLoginEveryJoin": false
|
||||
```
|
||||
|
||||
Lobby selalu memakai koordinat statis dari file ini. Lobby **tidak** disimpan sebagai last-position player.
|
||||
Semantik:
|
||||
|
||||
### `config/lobby/player-positions.json`
|
||||
- `false`: premium player cukup dengan Mojang authentication dan auto-login; local/cracked player tetap wajib password Lobby Systems pada setiap connection.
|
||||
- `true`: premium player tetap diverifikasi Mojang, tetapi juga wajib mempunyai dan memasukkan password Lobby Systems setiap connection. Local/cracked player tetap wajib password seperti biasa.
|
||||
|
||||
Berisi posisi terakhir masing-masing player pada dimension non-lobby. Data ini dipakai ketika player kembali melalui portal atau `/ls tp`.
|
||||
Jadi `true` merupakan lapisan password tambahan untuk premium account, bukan pengganti Mojang authentication.
|
||||
|
||||
Dimension yang sedang dikonfigurasi sebagai lobby disaring pada saat capture, read, dan save sehingga tidak disimpan di file ini.
|
||||
### Server Rules
|
||||
|
||||
### `config/lobby/portals.json`
|
||||
Auth config menyediakan `rulesEnabled` dan `rulesVersion`. Acceptance disimpan per account. Naikkan `rulesVersion` jika semua account perlu menerima versi rules yang baru. `/ls reload` langsung menerapkan perubahan versi rules kepada player yang sedang online.
|
||||
|
||||
Portal dibuat lewat command dan tersimpan otomatis. Contoh:
|
||||
### Auth account data
|
||||
|
||||
```json
|
||||
{
|
||||
"portals": {
|
||||
"survival": {
|
||||
"sourceDimension": "minecraft:lobby",
|
||||
"min": {
|
||||
"x": 10,
|
||||
"y": 64,
|
||||
"z": -5
|
||||
},
|
||||
"max": {
|
||||
"x": 12,
|
||||
"y": 67,
|
||||
"z": -5
|
||||
},
|
||||
"targetDimension": "minecraft:overworld",
|
||||
"visualMode": "nether_portal",
|
||||
"visualAxis": "x"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
`config/lobby/auth/accounts.json` menyimpan metadata account, rules acceptance, salt, password hash, dan PBKDF2 iteration count. File ini bukan tempat plaintext password.
|
||||
|
||||
Trigger portal tetap berupa region Lobby Systems. Mulai v1.5.0, portal baru yang dibuat lewat command menyimpan `visualMode=nether_portal` dan `visualAxis=x|z`. Frame/dekorasinya bebas, tetapi selection portal visual harus berupa bidang vertikal 1 block tebal dan hanya mencakup area dalam portal.
|
||||
## 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.
|
||||
|
||||
### `config/lobby/dimension-gamemodes.json`
|
||||
Destination dapat mengarah ke dimensi lain dan teleport menggunakan `TeleportTransition`, chunk preload, serta teleport lock. Portal visual dapat diperbaiki kembali saat reload/startup.
|
||||
|
||||
Minecraft menyimpan gamemode pada player, bukan pada dimension. Lobby Systems menyimpan mapping dimension → gamemode di file ini dan selalu menerapkannya pada join serta perpindahan dimension. Sejak v1.5.7, Lobby Systems adalah source of truth gamemode per-dimension dan tidak memiliki mode viewer-only.
|
||||
Gunakan command `/ls portal ...` yang tersedia melalui command completion untuk create/delete/info dan maintenance portal.
|
||||
|
||||
Contoh:
|
||||
## Dimension Gamemode
|
||||
|
||||
```json
|
||||
{
|
||||
"minecraft:lobby": "adventure",
|
||||
"minecraft:worldcreative": "creative",
|
||||
"minecraft:overworld": "survival"
|
||||
}
|
||||
```
|
||||
|
||||
Gamemode yang didukung: `survival`, `creative`, `adventure`, dan `spectator`.
|
||||
|
||||
Jika mod **Dimensional Inventories** (`dimensional-inventories`) terpasang, Lobby Systems menampilkan warning di console karena kedua mod dapat memiliki aturan gamemode pada transisi dimension/pool. Lobby Systems tetap authoritative dan akan menerapkan `dimension-gamemodes.json`; untuk hasil paling konsisten, samakan konfigurasi gamemode pool Dimensional Inventories atau nonaktifkan pengelolaan gamemode pada mod tersebut bila tersedia.
|
||||
|
||||
## Migrasi dari v1.1.x
|
||||
|
||||
Pada startup pertama v1.4.0:
|
||||
Gamemode per dimension bersifat authoritative dan selalu aktif.
|
||||
|
||||
```text
|
||||
config/lobby-systems.json
|
||||
```
|
||||
|
||||
akan dimigrasikan ke:
|
||||
|
||||
```text
|
||||
config/lobby/config.json
|
||||
```
|
||||
|
||||
Dan:
|
||||
|
||||
```text
|
||||
config/lobby-systems-positions.json
|
||||
```
|
||||
|
||||
akan dimigrasikan ke:
|
||||
|
||||
```text
|
||||
config/lobby/player-positions.json
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Command player
|
||||
|
||||
```text
|
||||
/lobby
|
||||
```
|
||||
|
||||
- Jika sudah di lobby: memberi informasi bahwa player sudah berada di lobby.
|
||||
- Jika berada di gameplay dimension: hanya dapat digunakan setelah `minimumPlaySeconds` tercapai.
|
||||
- Jika terlalu cepat: menampilkan countdown.
|
||||
- Jika berhasil: selalu kembali ke fixed lobby spawn dari `config.json`.
|
||||
|
||||
## Command admin / OP
|
||||
|
||||
```text
|
||||
/ls reload
|
||||
/ls tp <dimension>
|
||||
|
||||
/ls gamerule <dimension>
|
||||
/ls gamerule <dimension> --page <page>
|
||||
/ls gamerule set <dimension> <rule> <value>
|
||||
/ls gamerule reset <dimension> <rule>
|
||||
|
||||
/ls gamemode list
|
||||
/ls gamemode set <dimension> <survival|creative|adventure|spectator>
|
||||
/ls gamemode set <dimension> <gamemode>
|
||||
/ls gamemode remove <dimension>
|
||||
```
|
||||
|
||||
Mapping disimpan di `dimension-gamemodes.json` dan diterapkan pada join serta perpindahan dimension.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Gunakan `/ls gamerule ...` beserta command completion untuk melihat dan mengubah mapping.
|
||||
|
||||
## 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**.
|
||||
|
||||
```text
|
||||
/ls starterkit set <name> <dimension>
|
||||
/ls starterkit load <name>
|
||||
/ls starterkit activate <name>
|
||||
@@ -249,242 +164,49 @@ Jika file tujuan baru sudah ada, migrator tidak menimpanya.
|
||||
/ls starterkit status <dimension> <player>
|
||||
/ls starterkit reset <dimension> <player>
|
||||
/ls starterkit delete <name>
|
||||
|
||||
/ls portal create <name> <target_dimension>
|
||||
/ls portal delete <name>
|
||||
/ls portal list
|
||||
/ls portal info <name>
|
||||
/ls portal tp <name>
|
||||
```
|
||||
|
||||
`/ls tp` tetap command maintenance/admin. Player biasa tidak memerlukannya karena flow normal menggunakan portal.
|
||||
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.
|
||||
|
||||
|
||||
### Gamerule per dimension
|
||||
|
||||
`/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:
|
||||
## Command utama
|
||||
|
||||
```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:
|
||||
|
||||
```text
|
||||
/ls gamerule minecraft:lobby
|
||||
/ls gamerule minecraft:lobby --page 2
|
||||
```
|
||||
|
||||
Header chat menampilkan `halaman/total halaman`, nama dimension, dan jumlah gamerule. Jika masih ada halaman berikutnya, command untuk membuka halaman selanjutnya ikut ditampilkan.
|
||||
|
||||
### Gamemode per dimension
|
||||
|
||||
Gunakan mapping gamemode agar player otomatis mendapat gamemode yang sesuai saat memasuki dimension. Contoh:
|
||||
|
||||
```text
|
||||
/ls gamemode set minecraft:lobby adventure
|
||||
/ls gamemode set minecraft:worldcreative creative
|
||||
/ls gamemode set minecraft:overworld survival
|
||||
```
|
||||
|
||||
Command gamemode selalu tersedia: `/ls gamemode list`, `/ls gamemode set <dimension> <mode>`, dan `/ls gamemode remove <dimension>`. Mapping disimpan di `config/lobby/dimension-gamemodes.json`, diterapkan langsung ke player pada dimension terkait, dan ikut direload oleh `/ls reload`.
|
||||
|
||||
### Starter Kit per-dimension
|
||||
|
||||
Starter kit dibuat dari snapshot inventory operator dengan `/ls starterkit set <name> <dimension>`. Kit baru selalu INACTIVE sampai `/ls starterkit activate <name>` dijalankan. Satu dimension hanya dapat memiliki satu active starter kit; mengaktifkan kit lain otomatis mengganti pointer active tanpa menghapus definisi kit lama.
|
||||
|
||||
Automatic delivery hanya terjadi satu kali per **player + dimension**. Recipient history disimpan di `config/lobby/starterkit-recipients.json`, sedangkan definisi kit dan `activeByDimension` disimpan di `config/lobby/starterkits.json`. Mengganti active kit tidak membuat player lama menerima kit baru. `/ls starterkit give` adalah forced administrative delivery dan tidak mengubah recipient history. `/ls starterkit reset <dimension> <player>` membuat player eligible lagi pada entry berikutnya.
|
||||
|
||||
Snapshot menyimpan slot inventory player termasuk hotbar, main inventory, armor/equipment, dan offhand menggunakan codec ItemStack vanilla agar components/data item modded ikut tersimpan. Automatic delivery ditunda singkat setelah dimension entry untuk memberi kesempatan mod inventory per-dimension seperti Dimensional Inventories menyelesaikan pergantian inventory destination. Jika slot snapshot sudah terisi, Lobby Systems mencoba inventory kosong lain dan menjatuhkan sisa stack dengan aman bila inventory penuh; item existing player tidak dihapus.
|
||||
|
||||
### `/ls reload`
|
||||
|
||||
Reload live:
|
||||
|
||||
- `config/lobby/config.json`
|
||||
- `config/lobby/portals.json`
|
||||
- `config/lobby/dimension-gamemodes.json`
|
||||
- `config/lobby/starterkits.json`
|
||||
- `config/lobby/starterkit-recipients.json`
|
||||
- `config/lobby/dimension-gamerules.json`
|
||||
- lobby exclusion pada player-position store
|
||||
|
||||
Tidak perlu restart untuk perubahan JSON tersebut.
|
||||
|
||||
### Membuat portal dengan WorldEdit + visual portal
|
||||
|
||||
Mulai v1.5.0, `/ls portal create` dapat membuat **visual swirl vanilla Nether Portal secara otomatis** tanpa Flint and Steel dan tanpa mewajibkan mod Lobby Systems pada client. Lobby Systems memakai resource/block vanilla `minecraft:nether_portal`; tidak ada asset Custom Portals yang disalin.
|
||||
|
||||
Frame portal boleh dibuat dari block apa saja. Yang penting, WorldEdit selection harus memilih **area dalam/permukaan aktif portal saja**, bukan frame. Selection harus berupa bidang vertikal setebal tepat 1 block pada salah satu axis horizontal. Contoh valid:
|
||||
|
||||
```text
|
||||
1 x 5 x 3 (portal membentang pada axis Z)
|
||||
3 x 5 x 1 (portal membentang pada axis X)
|
||||
```
|
||||
|
||||
Contoh tidak valid untuk visual vanilla:
|
||||
|
||||
```text
|
||||
3 x 5 x 3 (volume terlalu tebal)
|
||||
5 x 1 x 5 (portal horizontal tidak didukung oleh nether_portal vanilla)
|
||||
```
|
||||
|
||||
Workflow:
|
||||
|
||||
1. Bangun frame/dekorasi portal dari block apa saja.
|
||||
2. Masuk ke dimension tempat portal berada, biasanya Lobby.
|
||||
3. Jalankan `//wand`.
|
||||
4. Select dua sudut **bagian dalam portal**. Jangan sertakan frame.
|
||||
5. Buat portal:
|
||||
|
||||
```text
|
||||
/ls portal create survival minecraft:overworld
|
||||
```
|
||||
|
||||
Lobby Systems otomatis mendeteksi orientasi selection:
|
||||
|
||||
- selection dengan X tetap dan Z melebar -> `nether_portal[axis=z]`
|
||||
- selection dengan Z tetap dan X melebar -> `nether_portal[axis=x]`
|
||||
|
||||
Ini memperbaiki kasus ketika `/fill ... minecraft:nether_portal` memakai default axis yang salah sehingga tekstur portal terlihat menyamping. Portal visual juga dipulihkan secara berkala jika block portal vanilla menghilang akibat shape update pada frame non-obsidian. Teleport tetap ditentukan oleh `portals.json`/Lobby Systems, bukan oleh destination Nether Portal vanilla. Saat player berada di managed visual region, Lobby Systems menjaga vanilla portal cooldown tetap aktif agar mekanisme Nether Portal vanilla tidak mengambil alih destination, termasuk untuk player Creative.
|
||||
|
||||
Untuk keamanan, saat membuat portal baru Lobby Systems hanya akan mengganti air, fire, soul fire, atau nether portal di dalam selection. Jika selection mengenai block frame/dekorasi, pembuatan dibatalkan agar build tidak tertimpa.
|
||||
|
||||
Portal lama dari sebelum v1.5.0 tetap berfungsi sebagai trigger tetapi **tidak otomatis diberi visual**, karena selection lama mungkin mencakup frame. Hapus dan buat ulang portal tersebut dengan selection interior jika ingin visual baru.
|
||||
|
||||
Contoh lain:
|
||||
|
||||
```text
|
||||
/ls portal create nether minecraft:the_nether
|
||||
/ls portal create dungeon mypack:dungeon
|
||||
```
|
||||
|
||||
Nama portal hanya menerima huruf kecil `a-z`, angka `0-9`, `_`, dan `-`.
|
||||
|
||||
WorldEdit hanya dibutuhkan saat membaca selection untuk **membuat** portal. Setelah `portals.json` tersimpan, portal runtime dan visual recovery dikelola Lobby Systems sendiri. Jika WorldEdit dilepas kemudian, portal yang sudah ada tetap dapat bekerja.
|
||||
|
||||
### Menghapus dan mengecek portal
|
||||
|
||||
```text
|
||||
/ls portal list
|
||||
/ls portal info survival
|
||||
/ls portal delete survival
|
||||
```
|
||||
|
||||
Nama portal memiliki autocomplete.
|
||||
|
||||
### Teleport admin ke region portal
|
||||
|
||||
```text
|
||||
/ls portal tp survival
|
||||
```
|
||||
|
||||
Command ini digunakan untuk maintenance. Lobby Systems memberi suppression sementara agar admin tidak langsung mengaktifkan portal tepat setelah teleport ke region tersebut.
|
||||
|
||||
## Perilaku destination
|
||||
|
||||
Jika target adalah gameplay dimension:
|
||||
|
||||
- player pernah mengunjungi dimension tersebut → teleport ke last position;
|
||||
- belum pernah → fallback ke spawn dimension.
|
||||
|
||||
Jika target adalah Lobby:
|
||||
|
||||
- selalu teleport ke `lobbyX/Y/Z/Yaw/Pitch`;
|
||||
- tidak pernah menggunakan last-position;
|
||||
- jika portal berasal dari gameplay dimension, minimum playtime tetap diberlakukan.
|
||||
|
||||
## Portal detection dan performa
|
||||
|
||||
`portals.json` tidak dibaca setiap tick. Portal dimuat ke RAM saat startup atau `/ls reload` dan di-index berdasarkan source dimension.
|
||||
|
||||
Default pemeriksaan region:
|
||||
|
||||
```json
|
||||
"portalCheckIntervalTicks": 5
|
||||
```
|
||||
|
||||
Artinya sekitar empat kali per detik. Region check hanya berupa perbandingan koordinat player dengan portal pada dimension yang sama.
|
||||
|
||||
Setelah portal aktif, `portalCooldownTicks` mencegah portal terpicu berulang saat client menyelesaikan dimension transition. Teleport lintas dimension tetap memakai `TeleportTransition` dan teleport lock dari v1.1.3.
|
||||
|
||||
## Flow server
|
||||
|
||||
```text
|
||||
PLAYER DATA LOADED
|
||||
↓
|
||||
Lobby Systems pre-login placement
|
||||
↓
|
||||
Initial world sent to client = Lobby
|
||||
↓
|
||||
Lobby fixed spawn + configured gamemode
|
||||
↓
|
||||
Player berjalan ke portal
|
||||
↓
|
||||
Portal region terdeteksi
|
||||
↓
|
||||
Gameplay dimension
|
||||
↓
|
||||
Last position jika tersedia / spawn jika pertama kali
|
||||
↓
|
||||
Minimum gameplay time
|
||||
↓
|
||||
/lobby
|
||||
↓
|
||||
Lobby fixed spawn
|
||||
/ls reload
|
||||
/ls tp <dimension>
|
||||
/ls gamemode ...
|
||||
/ls gamerule ...
|
||||
/ls portal ...
|
||||
/ls starterkit ...
|
||||
```
|
||||
|
||||
## Catatan pengujian
|
||||
Command `/ls` administratif memerlukan operator permission sesuai implementasi command.
|
||||
|
||||
Karena project ini menargetkan Java 25, compile final sebaiknya dilakukan menggunakan `compile.bat` atau `compile.sh` pada mesin dengan JDK 25. Saat menguji portal, perhatikan console untuk log teleport dan error WorldEdit selection jika selection belum lengkap.
|
||||
## `/ls reload`
|
||||
|
||||
Reload adalah satu pintu untuk seluruh runtime configuration. Urutannya mencakup:
|
||||
|
||||
1. `config.json`
|
||||
2. Auth `config.json` + `accounts.json`
|
||||
3. `portals.json` + portal visual repair
|
||||
4. `dimension-gamemodes.json` + apply ke dimension/player
|
||||
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.1
|
||||
|
||||
- Memperbaiki compile error Fabric 26.2 pada Auth dialog command dispatch: `ServerPlayer#getServer()` diganti dengan server dari `player.level().getServer()`.
|
||||
- `/ls reload` sekarang tidak hanya membaca ulang Auth config tetapi juga menerapkan perubahan Auth ke session player yang sedang online.
|
||||
- README diatur ulang berdasarkan subsistem, bukan riwayat patch versi lama.
|
||||
- Dialog Rules/Register/Login tetap menjadi primary authentication UI; command login/register dipertahankan sebagai fallback.
|
||||
|
||||
## 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.
|
||||
|
||||
## License
|
||||
|
||||
Lobby Systems is **proprietary software — All Rights Reserved**. Public access to this repository is for viewing, auditing, and reference only. Using, running, compiling, modifying, redistributing, or re-uploading the mod requires prior written permission from DartoKloning. See [`LICENSE`](LICENSE) for the complete terms.
|
||||
|
||||
|
||||
## v1.6.1
|
||||
|
||||
- Fixed a newly-created Lobby Systems visual portal being able to use vanilla Nether routing on its first entry.
|
||||
- Managed `minecraft:nether_portal` surfaces are now explicitly intercepted so Lobby Systems exclusively owns their destination.
|
||||
- Reduced the creator-side post-create trigger suppression from the old 100-tick window to the configured portal check interval.
|
||||
- Added the Lobby Systems mod icon to Fabric metadata.
|
||||
|
||||
## v1.7.0 - Lobby Systems Auth
|
||||
|
||||
- Hybrid authentication while keeping `online-mode=true`.
|
||||
- Premium usernames stay on vanilla/Mojang authentication and auto-login after successful verification.
|
||||
- Non-premium usernames may join through a deterministic vanilla offline profile and must `/register` once, then `/login` on every connection.
|
||||
- Premium usernames are protected: a cracked client using a premium name remains on vanilla authentication and is rejected when it cannot prove the official session.
|
||||
- Registration/login guidance is shown both as title/subtitle and chat.
|
||||
- Unauthenticated players are frozen in Lobby, cannot walk into Lobby Systems portals, and are disconnected after the configured timeout.
|
||||
- Passwords are stored as salted PBKDF2-HMAC-SHA256 hashes; plaintext passwords are never persisted.
|
||||
- New files: `config/lobby/auth/config.json` and `config/lobby/auth/accounts.json`.
|
||||
- EasyAuth is intentionally marked incompatible while native Lobby Systems Auth is enabled.
|
||||
|
||||
For the intended hybrid mode, keep `online-mode=true` in `server.properties` and remove EasyAuth before testing.
|
||||
|
||||
## v1.7.1
|
||||
- Fixed hybrid `online-mode=true` login: confirmed non-premium players now receive a deterministic offline GameProfile and advance through the login state machine instead of falling into vanilla `Invalid session`.
|
||||
- Premium username lookup now prefers the modern `api.minecraftservices.com` public profile endpoint and falls back to the legacy Mojang endpoint.
|
||||
- Premium/unknown identities still use vanilla authentication; lookup failure never silently grants a local identity.
|
||||
- Local/cracked players remain password-gated after join. `requireLoginEveryJoin` defaults to `true`; session bypass is intentionally not enabled yet, so changing it currently does not weaken authentication.
|
||||
- `enforce-secure-profile=true` is intentionally left supported; cracked chat will be validated in server testing before any secure-chat compatibility override is added.
|
||||
|
||||
## v1.7.2
|
||||
- Fixed the authentication access widener so the private Minecraft login state type is widened during compilation, not only declared in mod metadata.
|
||||
- Added Loom `accessWidenerPath` configuration for compile-time access widening.
|
||||
All Rights Reserved. Source dan asset Lobby Systems tidak boleh didistribusikan atau digunakan ulang di luar izin pemilik project.
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
loom_version=1.17-SNAPSHOT
|
||||
fabric_api_version=0.156.0+26.2
|
||||
mod_version=1.7.2
|
||||
mod_version=1.8.1
|
||||
maven_group=id.dartokloning
|
||||
archives_base_name=lobby-systems
|
||||
|
||||
@@ -11,5 +11,16 @@ public final class AuthConfig {
|
||||
public int pbkdf2Iterations = 210000;
|
||||
public String mojangProfileUrl = "https://api.minecraftservices.com/minecraft/profile/lookup/name/{username}";
|
||||
public String legacyMojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}";
|
||||
public boolean requireLoginEveryJoin = true;
|
||||
|
||||
/**
|
||||
* false: premium players use Mojang authentication as their login, while local/cracked
|
||||
* players must use their Lobby Systems password on every connection.
|
||||
* true: every player, including premium players, must also register/use a Lobby Systems
|
||||
* password after Mojang authentication.
|
||||
*/
|
||||
public boolean requireLoginEveryJoin = false;
|
||||
|
||||
/** Rules are accepted once per version. Increment to require acceptance again. */
|
||||
public boolean rulesEnabled = true;
|
||||
public int rulesVersion = 1;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
@@ -22,6 +20,7 @@ import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@@ -33,6 +32,10 @@ public final class AuthManager {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(4)).build();
|
||||
private static final String DIALOG_RULES = "dartokloning:auth_rules";
|
||||
private static final String DIALOG_REGISTER = "dartokloning:auth_register";
|
||||
private static final String DIALOG_LOGIN = "dartokloning:auth_login";
|
||||
|
||||
private final Map<String, Account> accounts = new HashMap<>();
|
||||
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
|
||||
private final Map<String, PremiumCache> premiumCache = new ConcurrentHashMap<>();
|
||||
@@ -46,13 +49,21 @@ public final class AuthManager {
|
||||
AuthConfig c = GSON.fromJson(r, AuthConfig.class);
|
||||
if (c != null) config = c;
|
||||
}
|
||||
} else saveConfig();
|
||||
}
|
||||
saveConfig(); // writes newly-added defaults/fields during upgrades
|
||||
|
||||
if (Files.exists(ConfigPaths.AUTH_ACCOUNTS)) {
|
||||
try (Reader r = Files.newBufferedReader(ConfigPaths.AUTH_ACCOUNTS)) {
|
||||
Map<String, Account> loaded = GSON.fromJson(r, new TypeToken<Map<String, Account>>(){}.getType());
|
||||
if (loaded != null) { accounts.clear(); accounts.putAll(loaded); }
|
||||
if (loaded != null) {
|
||||
accounts.clear();
|
||||
accounts.putAll(loaded);
|
||||
}
|
||||
}
|
||||
} else saveAccounts();
|
||||
} else {
|
||||
saveAccounts();
|
||||
}
|
||||
migrateAccounts();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to load Lobby Systems Auth", e);
|
||||
@@ -62,7 +73,56 @@ public final class AuthManager {
|
||||
|
||||
public AuthConfig config() { return config; }
|
||||
|
||||
/** Called from the login mixin. */
|
||||
/** Re-applies the freshly loaded auth configuration to players already online. */
|
||||
public void applyReload(MinecraftServer server) {
|
||||
premiumCache.clear();
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
if (!config.enabled) {
|
||||
Session existing = sessions.get(player.getUUID());
|
||||
if (existing != null) {
|
||||
existing.authenticated = true;
|
||||
existing.stage = Stage.AUTHENTICATED;
|
||||
clearDialog(player);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) continue;
|
||||
Account account = account(player.getName().getString());
|
||||
|
||||
// A rules-version bump is intentionally live: online players must accept
|
||||
// the new version before continuing.
|
||||
if (needsRules(account)) {
|
||||
s.authenticated = false;
|
||||
s.stage = Stage.RULES;
|
||||
s.joinedAt = System.currentTimeMillis();
|
||||
s.attempts = 0;
|
||||
showDialog(player, DIALOG_RULES);
|
||||
continue;
|
||||
}
|
||||
|
||||
// false means premium accounts are satisfied by Mojang authentication.
|
||||
if (s.premium && !config.requireLoginEveryJoin) {
|
||||
authenticate(player);
|
||||
continue;
|
||||
}
|
||||
|
||||
// true makes the Lobby Systems password an additional layer for premium
|
||||
// accounts, and local accounts always require it on each connection.
|
||||
if (s.premium && config.requireLoginEveryJoin && s.authenticated) {
|
||||
s.authenticated = false;
|
||||
s.joinedAt = System.currentTimeMillis();
|
||||
s.attempts = 0;
|
||||
}
|
||||
if (!s.authenticated) {
|
||||
s.stage = isRegistered(player.getName().getString()) ? Stage.LOGIN : Stage.REGISTER;
|
||||
showCurrentDialog(player, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called from the login mixin before the player enters play state. */
|
||||
public LoginRoute resolveLoginRoute(String username) {
|
||||
if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM;
|
||||
PremiumResult result = premiumStatus(username);
|
||||
@@ -74,7 +134,7 @@ public final class AuthManager {
|
||||
}
|
||||
|
||||
public PremiumResult premiumStatus(String username) {
|
||||
String key = username.toLowerCase(Locale.ROOT);
|
||||
String key = key(username);
|
||||
PremiumCache cached = premiumCache.get(key);
|
||||
long now = System.currentTimeMillis();
|
||||
if (cached != null && now - cached.checkedAt < 86_400_000L) return cached.result;
|
||||
@@ -111,20 +171,41 @@ public final class AuthManager {
|
||||
if (!config.enabled) return;
|
||||
String name = player.getName().getString();
|
||||
boolean local = LobbySystemsMod.consumeLocalLogin(name);
|
||||
if (!local) {
|
||||
sessions.put(player.getUUID(), new Session(true, true, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
||||
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||
boolean premium = !local;
|
||||
Session s = new Session(false, premium, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
||||
sessions.put(player.getUUID(), s);
|
||||
|
||||
Account account = account(name);
|
||||
if (premium) {
|
||||
ensurePremiumMetadata(player, account);
|
||||
account = account(name);
|
||||
}
|
||||
|
||||
if (needsRules(account)) {
|
||||
s.stage = Stage.RULES;
|
||||
showDialog(player, DIALOG_RULES);
|
||||
return;
|
||||
}
|
||||
Session s = new Session(false, false, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
||||
sessions.put(player.getUUID(), s);
|
||||
sendPrompt(player, true);
|
||||
|
||||
continueAfterRules(player);
|
||||
}
|
||||
|
||||
public void onDisconnect(UUID uuid) { sessions.remove(uuid); }
|
||||
public boolean isAuthenticated(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return !config.enabled || (s != null && s.authenticated); }
|
||||
public boolean isPremium(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return s != null && s.premium; }
|
||||
public boolean isRegistered(String username) { synchronized (accounts) { return accounts.containsKey(username.toLowerCase(Locale.ROOT)); } }
|
||||
|
||||
public boolean isAuthenticated(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
return !config.enabled || (s != null && s.authenticated);
|
||||
}
|
||||
|
||||
public boolean isPremium(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
return s != null && s.premium;
|
||||
}
|
||||
|
||||
public boolean isRegistered(String username) {
|
||||
Account a = account(username);
|
||||
return a != null && a.hasPassword();
|
||||
}
|
||||
|
||||
public void tick(MinecraftServer server) {
|
||||
if (!config.enabled) return;
|
||||
@@ -136,84 +217,198 @@ public final class AuthManager {
|
||||
player.connection.disconnect(Component.literal("Authentication timed out. Please reconnect and try again."));
|
||||
continue;
|
||||
}
|
||||
// Server-authoritative freeze. This also prevents walking into Lobby portals before authentication.
|
||||
// Keep the player at the safe Lobby authentication position. World safety is
|
||||
// intentionally handled by the configured Lobby dimension itself.
|
||||
if (player.distanceToSqr(s.x, s.y, s.z) > 0.01) {
|
||||
player.connection.teleport(s.x, s.y, s.z, s.yaw, s.pitch);
|
||||
}
|
||||
if (now - s.lastReminder >= config.reminderIntervalSeconds * 1000L) {
|
||||
s.lastReminder = now;
|
||||
sendPrompt(player, false);
|
||||
showCurrentDialog(player, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int register(ServerPlayer player, String password, String confirm) {
|
||||
if (!config.enabled) return fail(player, "§cAuthentication is disabled.");
|
||||
/** Called by the rules dialog's command-template action. */
|
||||
public int acceptRules(ServerPlayer player, boolean accepted) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s != null && s.premium) return fail(player, "§ePremium accounts do not need local registration.");
|
||||
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
|
||||
String name = player.getName().getString();
|
||||
PremiumResult premium = premiumStatus(name);
|
||||
if (premium != PremiumResult.NOT_PREMIUM) return fail(player, premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.");
|
||||
if (!password.equals(confirm)) return fail(player, "§cPasswords do not match.");
|
||||
if (password.length() < config.minimumPasswordLength) return fail(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.");
|
||||
if (s == null || s.authenticated || s.stage != Stage.RULES) return 0;
|
||||
if (!accepted) {
|
||||
player.sendSystemMessage(Component.literal("§cYou must accept the server rules to continue."));
|
||||
showDialog(player, DIALOG_RULES);
|
||||
return 0;
|
||||
}
|
||||
|
||||
synchronized (accounts) {
|
||||
String key = name.toLowerCase(Locale.ROOT);
|
||||
if (accounts.containsKey(key)) return fail(player, "§cThis local account is already registered. Use /login <password>.");
|
||||
accounts.put(key, hash(password));
|
||||
String name = player.getName().getString();
|
||||
Account a = accounts.computeIfAbsent(key(name), ignored -> Account.metadataOnly(s.premium));
|
||||
a.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
||||
if (s.premium) a.officialUuid = player.getUUID().toString();
|
||||
a.rulesVersionAccepted = config.rulesVersion;
|
||||
a.rulesAcceptedAt = Instant.now().toString();
|
||||
saveAccounts();
|
||||
}
|
||||
authenticate(player, false);
|
||||
continueAfterRules(player);
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int register(ServerPlayer player, String password, String confirm) {
|
||||
if (!config.enabled) return failAndReopen(player, "§cAuthentication is disabled.", DIALOG_REGISTER);
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null || s.authenticated) return failAndReopen(player, "§eYou are already authenticated.", DIALOG_REGISTER);
|
||||
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
|
||||
if (s.premium && !config.requireLoginEveryJoin) return fail(player, "§ePremium accounts use Mojang authentication and do not need a local password.");
|
||||
|
||||
String name = player.getName().getString();
|
||||
if (!s.premium) {
|
||||
PremiumResult premium = premiumStatus(name);
|
||||
if (premium != PremiumResult.NOT_PREMIUM) {
|
||||
return failAndReopen(player,
|
||||
premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.",
|
||||
DIALOG_REGISTER);
|
||||
}
|
||||
}
|
||||
if (!password.equals(confirm)) return failAndReopen(player, "§cPasswords do not match.", DIALOG_REGISTER);
|
||||
if (password.length() < config.minimumPasswordLength) return failAndReopen(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.", DIALOG_REGISTER);
|
||||
|
||||
synchronized (accounts) {
|
||||
String k = key(name);
|
||||
Account existing = accounts.get(k);
|
||||
if (existing != null && existing.hasPassword()) return failAndReopen(player, "§cThis account is already registered. Please sign in.", DIALOG_LOGIN);
|
||||
Account hashed = hash(password);
|
||||
if (existing != null) {
|
||||
hashed.accountType = existing.accountType;
|
||||
hashed.officialUuid = existing.officialUuid;
|
||||
hashed.rulesVersionAccepted = existing.rulesVersionAccepted;
|
||||
hashed.rulesAcceptedAt = existing.rulesAcceptedAt;
|
||||
} else {
|
||||
hashed.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
||||
hashed.officialUuid = s.premium ? player.getUUID().toString() : null;
|
||||
}
|
||||
accounts.put(k, hashed);
|
||||
saveAccounts();
|
||||
}
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aRegistration successful. You are now logged in."));
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int login(ServerPlayer player, String password) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s != null && s.premium) return fail(player, "§ePremium account is already authenticated.");
|
||||
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
|
||||
Account account;
|
||||
synchronized (accounts) { account = accounts.get(player.getName().getString().toLowerCase(Locale.ROOT)); }
|
||||
if (account == null) return fail(player, "§cAccount not registered. Use /register <password> <password>.");
|
||||
if (s == null || s.authenticated) return fail(player, "§eYou are already authenticated.");
|
||||
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
|
||||
|
||||
Account account = account(player.getName().getString());
|
||||
if (account == null || !account.hasPassword()) return failAndReopen(player, "§cAccount not registered. Please create a password first.", DIALOG_REGISTER);
|
||||
if (!verify(password, account)) {
|
||||
if (s != null && ++s.attempts >= config.maxLoginAttempts) {
|
||||
if (++s.attempts >= config.maxLoginAttempts) {
|
||||
player.connection.disconnect(Component.literal("Too many failed login attempts."));
|
||||
return 0;
|
||||
}
|
||||
return fail(player, "§cIncorrect password.");
|
||||
int remaining = Math.max(0, config.maxLoginAttempts - s.attempts);
|
||||
return failAndReopen(player, "§cIncorrect password. §7" + remaining + " attempt(s) remaining.", DIALOG_LOGIN);
|
||||
}
|
||||
authenticate(player, false);
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aLogin successful. Welcome back!"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void authenticate(ServerPlayer player, boolean premium) {
|
||||
Session old = sessions.get(player.getUUID());
|
||||
sessions.put(player.getUUID(), new Session(true, premium, 0, old == null ? System.currentTimeMillis() : old.joinedAt,
|
||||
player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
||||
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal("§aAUTHENTICATED")));
|
||||
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§fWelcome, " + player.getName().getString() + "!")));
|
||||
private void continueAfterRules(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) return;
|
||||
|
||||
if (s.premium && !config.requireLoginEveryJoin) {
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRegistered(player.getName().getString())) {
|
||||
s.stage = Stage.LOGIN;
|
||||
showDialog(player, DIALOG_LOGIN);
|
||||
} else {
|
||||
s.stage = Stage.REGISTER;
|
||||
showDialog(player, DIALOG_REGISTER);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPrompt(ServerPlayer player, boolean chat) {
|
||||
boolean registered = isRegistered(player.getName().getString());
|
||||
String title = registered ? "§eLOGIN REQUIRED" : "§bREGISTER REQUIRED";
|
||||
String command = registered ? "/login <password>" : "/register <password> <password>";
|
||||
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal(title)));
|
||||
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§f" + command)));
|
||||
if (chat) player.sendSystemMessage(Component.literal("§6[Lobby Systems Auth] §f" + (registered ? "Please login: §e" : "Please register: §b") + command));
|
||||
private void authenticate(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) return;
|
||||
s.authenticated = true;
|
||||
s.stage = Stage.AUTHENTICATED;
|
||||
s.attempts = 0;
|
||||
clearDialog(player);
|
||||
}
|
||||
|
||||
private static int fail(ServerPlayer p, String msg) { p.sendSystemMessage(Component.literal(msg)); return 0; }
|
||||
private boolean needsRules(Account account) {
|
||||
return config.rulesEnabled && (account == null || account.rulesVersionAccepted < config.rulesVersion);
|
||||
}
|
||||
|
||||
private void ensurePremiumMetadata(ServerPlayer player, Account existing) {
|
||||
synchronized (accounts) {
|
||||
String k = key(player.getName().getString());
|
||||
Account a = existing;
|
||||
if (a == null) {
|
||||
a = Account.metadataOnly(true);
|
||||
accounts.put(k, a);
|
||||
}
|
||||
a.accountType = "PREMIUM";
|
||||
a.officialUuid = player.getUUID().toString();
|
||||
saveAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
private void showCurrentDialog(ServerPlayer player, Session s) {
|
||||
switch (s.stage) {
|
||||
case RULES -> showDialog(player, DIALOG_RULES);
|
||||
case REGISTER -> showDialog(player, DIALOG_REGISTER);
|
||||
case LOGIN -> showDialog(player, DIALOG_LOGIN);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private void showDialog(ServerPlayer player, String id) {
|
||||
try {
|
||||
String command = "dialog show " + player.getGameProfile().name() + " " + id;
|
||||
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to show auth dialog {} to {}", id, player.getName().getString(), e);
|
||||
player.sendSystemMessage(Component.literal("§cAuthentication UI could not be opened. Please reconnect or contact staff."));
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDialog(ServerPlayer player) {
|
||||
try {
|
||||
String command = "dialog clear " + player.getGameProfile().name();
|
||||
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.debug("Failed to clear auth dialog for {}: {}", player.getName().getString(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private int failAndReopen(ServerPlayer player, String message, String dialog) {
|
||||
player.sendSystemMessage(Component.literal(message));
|
||||
showDialog(player, dialog);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int fail(ServerPlayer p, String msg) {
|
||||
p.sendSystemMessage(Component.literal(msg));
|
||||
return 0;
|
||||
}
|
||||
|
||||
private Account hash(String password) {
|
||||
try {
|
||||
byte[] salt = new byte[16]; RANDOM.nextBytes(salt);
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, config.pbkdf2Iterations, 256);
|
||||
byte[] digest = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
||||
spec.clearPassword();
|
||||
return new Account(Base64.getEncoder().encodeToString(salt), Base64.getEncoder().encodeToString(digest), config.pbkdf2Iterations);
|
||||
} catch (Exception e) { throw new IllegalStateException("Unable to hash password", e); }
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to hash password", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verify(String password, Account a) {
|
||||
@@ -223,15 +418,98 @@ public final class AuthManager {
|
||||
byte[] actual = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
||||
spec.clearPassword();
|
||||
return MessageDigest.isEqual(actual, Base64.getDecoder().decode(a.hash));
|
||||
} catch (Exception e) { return false; }
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveConfig() throws Exception { try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); } }
|
||||
private void saveAccounts() { try { Files.createDirectories(ConfigPaths.AUTH_DIRECTORY); try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); } } catch (Exception e) { LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e); } }
|
||||
private Account account(String username) {
|
||||
synchronized (accounts) {
|
||||
return accounts.get(key(username));
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateAccounts() {
|
||||
boolean changed = false;
|
||||
synchronized (accounts) {
|
||||
for (Account a : accounts.values()) {
|
||||
if (a.accountType == null || a.accountType.isBlank()) {
|
||||
a.accountType = "LOCAL";
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) saveAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
private static String key(String username) { return username.toLowerCase(Locale.ROOT); }
|
||||
|
||||
private void saveConfig() throws Exception {
|
||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); }
|
||||
}
|
||||
|
||||
private void saveAccounts() {
|
||||
try {
|
||||
Files.createDirectories(ConfigPaths.AUTH_DIRECTORY);
|
||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); }
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e);
|
||||
}
|
||||
}
|
||||
|
||||
public enum PremiumResult { PREMIUM, NOT_PREMIUM, UNKNOWN }
|
||||
public enum LoginRoute { PREMIUM, LOCAL, UNKNOWN }
|
||||
private enum Stage { RULES, REGISTER, LOGIN, AUTHENTICATED }
|
||||
private record PremiumCache(PremiumResult result, long checkedAt) {}
|
||||
private static final class Account { String salt; String hash; int iterations; Account(String salt,String hash,int iterations){this.salt=salt;this.hash=hash;this.iterations=iterations;} }
|
||||
private static final class Session { boolean authenticated; boolean premium; int attempts; long joinedAt; long lastReminder; double x,y,z; float yaw,pitch; Session(boolean a,boolean p,int at,long j,double x,double y,double z,float yaw,float pitch){authenticated=a;premium=p;attempts=at;joinedAt=j;lastReminder=0;this.x=x;this.y=y;this.z=z;this.yaw=yaw;this.pitch=pitch;} }
|
||||
|
||||
private static final class Account {
|
||||
String salt;
|
||||
String hash;
|
||||
int iterations;
|
||||
String accountType;
|
||||
String officialUuid;
|
||||
int rulesVersionAccepted;
|
||||
String rulesAcceptedAt;
|
||||
|
||||
Account(String salt, String hash, int iterations) {
|
||||
this.salt = salt;
|
||||
this.hash = hash;
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
static Account metadataOnly(boolean premium) {
|
||||
Account a = new Account(null, null, 0);
|
||||
a.accountType = premium ? "PREMIUM" : "LOCAL";
|
||||
return a;
|
||||
}
|
||||
|
||||
boolean hasPassword() {
|
||||
return salt != null && !salt.isBlank() && hash != null && !hash.isBlank() && iterations > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Session {
|
||||
boolean authenticated;
|
||||
final boolean premium;
|
||||
int attempts;
|
||||
long joinedAt;
|
||||
long lastReminder;
|
||||
final double x, y, z;
|
||||
final float yaw, pitch;
|
||||
Stage stage = Stage.RULES;
|
||||
|
||||
Session(boolean authenticated, boolean premium, int attempts, long joinedAt,
|
||||
double x, double y, double z, float yaw, float pitch) {
|
||||
this.authenticated = authenticated;
|
||||
this.premium = premium;
|
||||
this.attempts = attempts;
|
||||
this.joinedAt = joinedAt;
|
||||
this.lastReminder = 0;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,19 +247,29 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
ctx.getSource().getPlayerOrException(), ctx.getSource().getServer())));
|
||||
|
||||
dispatcher.register(Commands.literal("register")
|
||||
.then(Commands.argument("password", StringArgumentType.word())
|
||||
.then(Commands.argument("confirm", StringArgumentType.word())
|
||||
.then(Commands.argument("password", StringArgumentType.string())
|
||||
.then(Commands.argument("confirm", StringArgumentType.string())
|
||||
.executes(ctx -> AUTH.register(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
StringArgumentType.getString(ctx, "password"),
|
||||
StringArgumentType.getString(ctx, "confirm"))))));
|
||||
|
||||
dispatcher.register(Commands.literal("login")
|
||||
.then(Commands.argument("password", StringArgumentType.word())
|
||||
.then(Commands.argument("password", StringArgumentType.string())
|
||||
.executes(ctx -> AUTH.login(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
StringArgumentType.getString(ctx, "password")))));
|
||||
|
||||
// Internal command used by the vanilla dialog command-template action.
|
||||
// It intentionally has no operator requirement, but AuthManager only accepts it
|
||||
// while the calling player is in the RULES authentication stage.
|
||||
dispatcher.register(Commands.literal("lobbyauth")
|
||||
.then(Commands.literal("rules")
|
||||
.then(Commands.argument("accepted", com.mojang.brigadier.arguments.BoolArgumentType.bool())
|
||||
.executes(ctx -> AUTH.acceptRules(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
com.mojang.brigadier.arguments.BoolArgumentType.getBool(ctx, "accepted"))))));
|
||||
|
||||
dispatcher.register(Commands.literal("ls")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
||||
.then(Commands.literal("reload")
|
||||
@@ -581,6 +591,13 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!AUTH.load()) {
|
||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi auth config/accounts bermasalah. Cek console."));
|
||||
LobbyLog.state("RELOAD FAILED", "Reason: auth config/accounts gagal dibaca");
|
||||
return 0;
|
||||
}
|
||||
AUTH.applyReload(server);
|
||||
|
||||
POSITIONS.setExcludedDimension(key);
|
||||
boolean portalOk = PORTALS.load();
|
||||
if (!portalOk) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "minecraft:notice",
|
||||
"title": {"text": "Welcome Back!", "color": "yellow", "bold": true},
|
||||
"can_close_with_escape": false,
|
||||
"pause": false,
|
||||
"after_action": "close",
|
||||
"body": [
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Please enter your password to continue.", "color": "white"}},
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "If you forgot your password, contact staff.", "color": "gray"}}
|
||||
],
|
||||
"inputs": [
|
||||
{"type": "minecraft:text", "key": "password", "label": {"text": "Password"}, "width": 300, "max_length": 128, "initial": ""}
|
||||
],
|
||||
"action": {
|
||||
"label": {"text": "Sign In", "color": "green"},
|
||||
"width": 300,
|
||||
"action": {"type": "dynamic/run_command", "template": "login $(password)"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "minecraft:notice",
|
||||
"title": {"text": "Create Account", "color": "aqua", "bold": true},
|
||||
"can_close_with_escape": false,
|
||||
"pause": false,
|
||||
"after_action": "close",
|
||||
"body": [
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Welcome! Create a secure password below.", "color": "white"}},
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Your password protects your progress.", "color": "gray"}}
|
||||
],
|
||||
"inputs": [
|
||||
{"type": "minecraft:text", "key": "password", "label": {"text": "Password"}, "width": 300, "max_length": 128, "initial": ""},
|
||||
{"type": "minecraft:text", "key": "confirm", "label": {"text": "Confirm Password"}, "width": 300, "max_length": 128, "initial": ""}
|
||||
],
|
||||
"action": {
|
||||
"label": {"text": "Register", "color": "green"},
|
||||
"width": 300,
|
||||
"action": {"type": "dynamic/run_command", "template": "register $(password) $(confirm)"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "minecraft:notice",
|
||||
"title": {"text": "Server Rules", "color": "yellow", "bold": true},
|
||||
"can_close_with_escape": false,
|
||||
"pause": false,
|
||||
"after_action": "close",
|
||||
"body": [
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Please read and accept our rules:", "color": "yellow"}},
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "1. No cheating, hacking, or exploits\n\n2. Be respectful to all players\n\n3. No advertising or spam\n\n4. Follow staff instructions\n\n5. Have fun and play fair!", "color": "white"}},
|
||||
{"type": "minecraft:plain_message", "contents": {"text": "Click confirm to accept and continue.", "color": "gray"}}
|
||||
],
|
||||
"inputs": [
|
||||
{"type": "minecraft:boolean", "key": "accepted", "label": {"text": "I have read and agree to the server rules"}, "initial": false, "on_true": "true", "on_false": "false"}
|
||||
],
|
||||
"action": {
|
||||
"label": {"text": "I Accept", "color": "green"},
|
||||
"action": {"type": "dynamic/run_command", "template": "lobbyauth rules $(accepted)"}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"id": "dartokloning",
|
||||
"version": "${version}",
|
||||
"name": "Lobby Systems",
|
||||
"description": "Server-side Fabric lobby routing, hybrid premium/cracked authentication, persistent world positions, portals, per-dimension gamemodes/gamerules, and starter kits.",
|
||||
"description": "Server-side Fabric lobby routing with hybrid premium/cracked authentication, vanilla dialog rules/register/login UI, persistent world positions, portals, per-dimension gamemodes/gamerules, and starter kits.",
|
||||
"authors": [
|
||||
"DartoKloning"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user