commit 8ae4e6982d78efad4763c59457082f516f889608 Author: Darto KLoning Date: Sun Sep 6 13:43:16 2026 +0700 Initial Lobby Systems v1.2.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81a2e88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.gradle/ +.gradle-local/ +build/ +run/ +out/ +ChatGPT/ + +.idea/ +.vscode/ +*.iml + +*.log +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c61639b --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 DartoKloning + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..beb9b75 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Lobby Systems - Fabric 26.2 + +Server-side lobby router untuk Minecraft Java 26.2 / Fabric. + +## Identity +- Mod ID: `dartokloning` +- Java package: `id.dartokloning.lobbysystems` +- Maven group: `id.dartokloning` +- Mod name: `Lobby Systems` + +## Requirements +- JDK / Java 25 atau lebih baru +- Minecraft 26.2 +- Fabric Loader 0.19.3+ +- Fabric API 0.156.0+26.2 +- Internet pada build pertama untuk mengunduh Gradle dan dependency Fabric/Minecraft +- Optional: World Manager 1.4.4+26.2 hanya untuk membuat/mengelola world; Lobby Systems tidak memiliki hard dependency ke World Manager + +## Compile di Windows +Cara termudah: double-click `compile.bat`. + +Script tersebut memanggil `compile.ps1`, mengecek Java 25+, mengunduh Gradle 9.5.1 secara lokal ke `.gradle-local` bila diperlukan, lalu menjalankan: + +```text +clean build +``` + +Alternatif PowerShell: + +```powershell +powershell -ExecutionPolicy Bypass -File .\compile.ps1 +``` + +JAR hasil build berada di: + +```text +build\libs\ +``` + +Folder `.gradle-local` hanya cache Gradle lokal project dan boleh dihapus kapan saja. + +## Compile manual +Jika Gradle sudah terpasang: + +```bash +gradle clean build +``` + +## Configuration +Jalankan server sekali. Edit: + +`config/lobby-systems.json` + +Contoh: + +```json +{ + "lobbyDimension": "minecraft:overworld", + "lobbyX": 0.5, + "lobbyY": 100.0, + "lobbyZ": 0.5, + "lobbyYaw": 0.0, + "lobbyPitch": 0.0, + "minimumPlaySeconds": 300, + "inLobbyMessage": "§eKamu sudah berada di lobby.", + "returnedMessage": "§aKamu telah kembali ke lobby.", + "waitMessage": "§cKamu belum bisa kembali ke lobby. Tunggu §e{time}§c lagi." +} +``` + +Set `lobbyDimension` ke identifier dimensi/world yang dibuat oleh World Manager. + +## Behavior +1. Setiap login diarahkan ke dimensi dan koordinat lobby yang dikonfigurasi. +2. Saat player keluar lobby dan masuk dimensi lain, timer sesi game baru dimulai. +3. `/lobby` saat sudah berada di lobby hanya memberi informasi bahwa player sedang di lobby. +4. `/lobby` di luar lobby diblokir sampai `minimumPlaySeconds` berlalu dan menampilkan countdown. +5. Setelah timer selesai, `/lobby` mengembalikan player ke lobby. +6. Pindah dari satu dimensi non-lobby ke dimensi game lain memulai timer baru untuk dimensi tersebut. +7. Reconnect selalu mengembalikan player ke lobby dan membersihkan timer sesi sebelumnya. + +## World Manager integration +Lobby Systems tidak menggunakan internal API World Manager. World Manager cukup membuat/load custom world, kemudian ID dimensinya dimasukkan ke `lobby-systems.json`. Dengan desain ini, portal mod/command lain juga tetap bisa digunakan. + +## Admin command (v1.1.3) + +Requires Minecraft admin/OP command permission. + +- `/ls reload` — reloads `config/lobby-systems.json` live without restarting the server. +- `/ls tp ` — teleports the executing player to a loaded dimension. The `` argument autocompletes from the server's currently loaded dimension keys. + +### Last-position behavior + +Lobby Systems stores each player's last coordinates and rotation per dimension in: + +`config/lobby-systems-positions.json` + +When `/ls tp ` is used, the player returns to their last recorded position in that dimension. If the player has never visited that dimension before, the command falls back to the dimension's configured respawn/spawn position. + +Positions are captured for non-lobby dimensions when players change dimensions, use `/ls tp`, use `/lobby`, disconnect, and are periodically flushed to disk. The configured lobby dimension is deliberately excluded: every teleport/login to the lobby always uses `lobbyX/Y/Z/Yaw/Pitch` from `lobby-systems.json`. Existing saved lobby-position entries are removed automatically on startup/reload. + +## v1.1.3 stability changes +- Cross-dimension teleports now use Minecraft's `TeleportTransition` path instead of calling `teleportTo(...)` directly. +- Destination chunk is loaded before the transition is sent. +- Login-to-lobby is delayed 20 server ticks so the client has time to finish entering play state. +- A 40-tick per-player teleport lock prevents overlapping dimension transitions. +- Lobby position persistence is enforced inside `PositionManager`, not only at call sites. The configured lobby dimension is purged on startup/reload and filtered again before every save, so it cannot be written to `lobby-systems-positions.json`. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..dbbab67 --- /dev/null +++ b/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'net.fabricmc.fabric-loom' version "${loom_version}" + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { archivesName = project.archives_base_name } + +repositories { mavenCentral() } + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + implementation "net.fabricmc:fabric-loader:${project.loader_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" +} + +processResources { + inputs.property "version", project.version + filesMatching("fabric.mod.json") { expand "version": project.version } +} + +tasks.withType(JavaCompile).configureEach { + options.release = 25 + options.encoding = 'UTF-8' +} + +java { + withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} diff --git a/compile.bat b/compile.bat new file mode 100644 index 0000000..c47fa5c --- /dev/null +++ b/compile.bat @@ -0,0 +1,375 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +REM ============================================================ +REM Lobby Systems - Fabric Build Script +REM Windows +REM +REM Features: +REM - Read version from gradle.properties +REM - Check Java 25 +REM - Download Gradle 9.5.1 automatically if needed +REM - Clean build +REM - Ask for Git commit + push ONLY after BUILD SUCCESS +REM ============================================================ + +cd /d "%~dp0" + +set "GRADLE_PROPERTIES=%CD%\gradle.properties" +set "LOCAL_GRADLE_DIR=%CD%\.gradle-local" +set "GRADLE_VERSION=9.5.1" +set "GRADLE_HOME=%LOCAL_GRADLE_DIR%\gradle-%GRADLE_VERSION%" +set "GRADLE_BIN=%GRADLE_HOME%\bin\gradle.bat" + +set "MOD_VERSION=" +set "MINECRAFT_VERSION=" +set "MAVEN_GROUP=" + +REM ============================================================ +REM Read gradle.properties +REM ============================================================ + +if not exist "%GRADLE_PROPERTIES%" ( + echo [ERROR] gradle.properties tidak ditemukan. + goto :failed +) + +for /f "usebackq tokens=1,* delims==" %%A in ("%GRADLE_PROPERTIES%") do ( + if "%%A"=="mod_version" set "MOD_VERSION=%%B" + if "%%A"=="minecraft_version" set "MINECRAFT_VERSION=%%B" + if "%%A"=="maven_group" set "MAVEN_GROUP=%%B" +) + +if not defined MOD_VERSION set "MOD_VERSION=unknown" +if not defined MINECRAFT_VERSION set "MINECRAFT_VERSION=26.2" +if not defined MAVEN_GROUP set "MAVEN_GROUP=id.dartokloning" + +echo ============================================ +echo Lobby Systems - Fabric %MINECRAFT_VERSION% Build +echo Version: %MOD_VERSION% +echo Namespace: id.dartokloning.lobbysystems +echo Mod ID: dartokloning +echo ============================================ + +REM ============================================================ +REM Check Java +REM ============================================================ + +where java.exe >nul 2>&1 + +if errorlevel 1 ( + echo [ERROR] Java tidak ditemukan di PATH. + echo Install JDK 25, buka terminal baru, lalu jalankan script lagi. + goto :failed +) + +for /f "delims=" %%J in ('where java.exe 2^>nul') do ( + set "JAVA_PATH=%%J" + goto :java_path_found +) + +:java_path_found + +echo [INFO] Java executable: %JAVA_PATH% + +set "JAVA_VERSION=" + +for /f "tokens=2" %%V in ('java --version 2^>^&1 ^| findstr /R /C:"^openjdk " /C:"^java "') do ( + if not defined JAVA_VERSION set "JAVA_VERSION=%%V" +) + +if not defined JAVA_VERSION ( + echo [ERROR] Tidak dapat membaca versi Java. + goto :failed +) + +for /f "tokens=1 delims=." %%M in ("%JAVA_VERSION%") do ( + set "JAVA_MAJOR=%%M" +) + +if not "%JAVA_MAJOR%"=="25" ( + echo [ERROR] Java 25 diperlukan. + echo [INFO] Java yang terdeteksi: %JAVA_VERSION% + goto :failed +) + +echo [OK] Java 25 terdeteksi. + +REM ============================================================ +REM Prepare Gradle +REM ============================================================ + +if exist "%GRADLE_BIN%" goto :gradle_ready + +echo [INFO] Gradle %GRADLE_VERSION% belum tersedia. +echo [INFO] Mengunduh distribusi resmi... + +if not exist "%LOCAL_GRADLE_DIR%" ( + mkdir "%LOCAL_GRADLE_DIR%" +) + +set "GRADLE_ZIP=%LOCAL_GRADLE_DIR%\gradle-%GRADLE_VERSION%-bin.zip" +set "GRADLE_URL=https://services.gradle.org/distributions/gradle-%GRADLE_VERSION%-bin.zip" + +where curl.exe >nul 2>&1 + +if errorlevel 1 ( + echo [ERROR] curl.exe tidak ditemukan. + echo Windows 10/11 biasanya sudah menyediakan curl. + goto :failed +) + +curl.exe -L --fail --progress-bar "%GRADLE_URL%" -o "%GRADLE_ZIP%" + +if errorlevel 1 ( + echo [ERROR] Gagal mengunduh Gradle %GRADLE_VERSION%. + goto :failed +) + +echo [INFO] Mengekstrak Gradle %GRADLE_VERSION%... + +powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ^ + "Expand-Archive -LiteralPath '%GRADLE_ZIP%' -DestinationPath '%LOCAL_GRADLE_DIR%' -Force" + +if errorlevel 1 ( + echo [ERROR] Gagal mengekstrak Gradle. + goto :failed +) + +del /q "%GRADLE_ZIP%" >nul 2>&1 + +if not exist "%GRADLE_BIN%" ( + echo [ERROR] Gradle berhasil diekstrak tetapi executable tidak ditemukan. + goto :failed +) + +:gradle_ready + +echo [OK] Gradle %GRADLE_VERSION% tersedia. + +REM ============================================================ +REM Build +REM ============================================================ + +echo [INFO] Menjalankan clean build Lobby Systems v%MOD_VERSION%... +echo. + +call "%GRADLE_BIN%" clean build --no-daemon + +if errorlevel 1 goto :build_failed + +REM ============================================================ +REM BUILD SUCCESS +REM ============================================================ + +echo. +echo ============================================ +echo BUILD SUCCESS +echo ============================================ + +set "JAR_PATH=%CD%\build\libs\lobby-systems-%MOD_VERSION%.jar" + +if exist "%JAR_PATH%" ( + echo JAR: + echo %JAR_PATH% +) else ( + echo [WARN] Build berhasil, tetapi JAR utama tidak ditemukan pada: + echo %JAR_PATH% +) + +echo. + +REM ============================================================ +REM Ask whether to continue to Git +REM ============================================================ + +choice /C YN /N /M "Commit dan push perubahan ke Git/Gitea? [Y/N]: " + +if errorlevel 2 goto :skip_git +if errorlevel 1 goto :git_integration + +:git_integration + +echo. +echo ============================================ +echo Git Integration +echo ============================================ + +where git.exe >nul 2>&1 + +if errorlevel 1 ( + echo [ERROR] Git tidak ditemukan di PATH. + echo Build tetap berhasil, tetapi Git tidak dapat dijalankan. + goto :success +) + +git rev-parse --is-inside-work-tree >nul 2>&1 + +if errorlevel 1 ( + echo [ERROR] Folder ini belum menjadi Git repository. + echo. + echo Jalankan terlebih dahulu: + echo git init + echo git branch -M main + echo git remote add origin ^ + echo. + goto :success +) + +echo [INFO] Memeriksa perubahan... + +set "HAS_CHANGES=" + +for /f "delims=" %%G in ('git status --porcelain') do ( + set "HAS_CHANGES=1" +) + +if defined HAS_CHANGES ( + echo. + echo Perubahan ditemukan: + git status --short + echo. + + set "COMMIT_MESSAGE=" + + set /p "COMMIT_MESSAGE=Commit message [Lobby Systems v%MOD_VERSION% build]: " + + if not defined COMMIT_MESSAGE ( + for /f "tokens=1-3 delims=/ " %%A in ("%date%") do ( + set "DATESTAMP=%date%" + ) + + set "COMMIT_MESSAGE=Lobby Systems v%MOD_VERSION% build" + ) + + echo. + echo [INFO] Menambahkan perubahan ke staging... + + git add -A + + if errorlevel 1 ( + echo [ERROR] git add gagal. + goto :git_failed + ) + + echo [INFO] Membuat commit... + + git commit -m "!COMMIT_MESSAGE!" + + if errorlevel 1 ( + echo [ERROR] git commit gagal. + goto :git_failed + ) + +) else ( + echo [INFO] Tidak ada perubahan source yang perlu di-commit. + echo [INFO] Akan mencoba push commit yang sudah ada. +) + +REM ============================================================ +REM Determine current branch +REM ============================================================ + +set "CURRENT_BRANCH=" + +for /f "delims=" %%B in ('git branch --show-current') do ( + set "CURRENT_BRANCH=%%B" +) + +if not defined CURRENT_BRANCH ( + echo [ERROR] Tidak dapat menentukan branch Git aktif. + goto :git_failed +) + +REM ============================================================ +REM Check origin +REM ============================================================ + +git remote get-url origin >nul 2>&1 + +if errorlevel 1 ( + echo [ERROR] Remote Git 'origin' belum dikonfigurasi. + echo. + echo Tambahkan Gitea dengan: + echo git remote add origin ^ + echo. + echo Commit lokal tetap tersimpan. + goto :git_failed +) + +echo [INFO] Push ke origin/!CURRENT_BRANCH!... + +git push origin "!CURRENT_BRANCH!" + +if errorlevel 1 ( + echo [ERROR] Git push gagal. + echo [INFO] Commit lokal tetap tersimpan. + goto :git_failed +) + +echo. +echo [SUCCESS] Push ke Git/Gitea berhasil. +goto :success + +REM ============================================================ +REM Skip Git +REM ============================================================ + +:skip_git + +echo. +echo [INFO] Git commit/push dilewati. +goto :success + +REM ============================================================ +REM Success +REM ============================================================ + +:success + +echo. +echo ============================================ +echo Lobby Systems v%MOD_VERSION% selesai. +echo ============================================ +echo. +pause +exit /b 0 + +REM ============================================================ +REM Build failed +REM Git section is NEVER reached +REM ============================================================ + +:build_failed + +echo. +echo ============================================ +echo BUILD FAILED +echo ============================================ +echo. +echo [ERROR] Build gagal. +echo [INFO] Git Integration tidak dijalankan. +echo. +pause +exit /b 1 + +:git_failed + +echo. +echo ============================================ +echo GIT FAILED +echo ============================================ +echo. +echo [INFO] Build Lobby Systems tetap berhasil. +echo [INFO] Hanya proses Git yang mengalami kegagalan. +echo. +pause +exit /b 2 + +:failed + +echo. +echo Build gagal. +echo. +pause +exit /b 1 \ No newline at end of file diff --git a/compile.sh b/compile.sh new file mode 100644 index 0000000..5e22b27 --- /dev/null +++ b/compile.sh @@ -0,0 +1,445 @@ +#!/usr/bin/env bash + +# ============================================================ +# Lobby Systems - Fabric Build Script +# Linux / macOS +# +# Features: +# - Read version from gradle.properties +# - Check Java 25 +# - Download Gradle 9.5.1 automatically if needed +# - Clean build +# - Ask for Git commit + push ONLY after BUILD SUCCESS +# ============================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" || exit 1 + +GRADLE_PROPERTIES="$SCRIPT_DIR/gradle.properties" + +LOCAL_GRADLE_DIR="$SCRIPT_DIR/.gradle-local" + +GRADLE_VERSION="9.5.1" +GRADLE_HOME="$LOCAL_GRADLE_DIR/gradle-$GRADLE_VERSION" +GRADLE_BIN="$GRADLE_HOME/bin/gradle" + +# ============================================================ +# Helper functions +# ============================================================ + +info() { + echo "[INFO] $1" +} + +ok() { + echo "[OK] $1" +} + +warn() { + echo "[WARN] $1" +} + +error() { + echo "[ERROR] $1" +} + +separator() { + echo "============================================" +} + +read_property() { + local key="$1" + + grep -E "^${key}=" "$GRADLE_PROPERTIES" \ + | head -n 1 \ + | cut -d'=' -f2- \ + | tr -d '\r' +} + +# ============================================================ +# Read gradle.properties +# ============================================================ + +if [[ ! -f "$GRADLE_PROPERTIES" ]]; then + error "gradle.properties tidak ditemukan." + exit 1 +fi + +MOD_VERSION="$(read_property "mod_version")" +MINECRAFT_VERSION="$(read_property "minecraft_version")" +MAVEN_GROUP="$(read_property "maven_group")" + +if [[ -z "$MOD_VERSION" ]]; then + MOD_VERSION="unknown" +fi + +if [[ -z "$MINECRAFT_VERSION" ]]; then + MINECRAFT_VERSION="26.2" +fi + +if [[ -z "$MAVEN_GROUP" ]]; then + MAVEN_GROUP="id.dartokloning" +fi + +separator +echo " Lobby Systems - Fabric $MINECRAFT_VERSION Build" +echo " Version: $MOD_VERSION" +echo " Namespace: id.dartokloning.lobbysystems" +echo " Mod ID: dartokloning" +separator + +# ============================================================ +# Check Java +# ============================================================ + +if ! command -v java >/dev/null 2>&1; then + error "Java tidak ditemukan di PATH." + echo "Install JDK 25 lalu jalankan script lagi." + exit 1 +fi + +JAVA_PATH="$(command -v java)" + +info "Java executable: $JAVA_PATH" + +JAVA_VERSION_OUTPUT="$(java --version 2>&1 | head -n 1)" + +JAVA_VERSION="$(echo "$JAVA_VERSION_OUTPUT" | awk '{print $2}')" + +JAVA_MAJOR="$(echo "$JAVA_VERSION" | cut -d'.' -f1)" + +if [[ "$JAVA_MAJOR" != "25" ]]; then + error "Java 25 diperlukan." + echo "[INFO] Java yang terdeteksi:" + echo " $JAVA_VERSION_OUTPUT" + exit 1 +fi + +ok "Java 25 terdeteksi." + +# ============================================================ +# Prepare Gradle +# ============================================================ + +if [[ ! -x "$GRADLE_BIN" ]]; then + + info "Gradle $GRADLE_VERSION belum tersedia." + info "Mengunduh distribusi resmi..." + + mkdir -p "$LOCAL_GRADLE_DIR" + + GRADLE_ZIP="$LOCAL_GRADLE_DIR/gradle-$GRADLE_VERSION-bin.zip" + + GRADLE_URL="https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip" + + if command -v curl >/dev/null 2>&1; then + + curl \ + -L \ + --fail \ + --progress-bar \ + "$GRADLE_URL" \ + -o "$GRADLE_ZIP" + + DOWNLOAD_RESULT=$? + + elif command -v wget >/dev/null 2>&1; then + + wget \ + "$GRADLE_URL" \ + -O "$GRADLE_ZIP" + + DOWNLOAD_RESULT=$? + + else + + error "curl atau wget tidak ditemukan." + exit 1 + + fi + + if [[ $DOWNLOAD_RESULT -ne 0 ]]; then + error "Gagal mengunduh Gradle $GRADLE_VERSION." + exit 1 + fi + + info "Mengekstrak Gradle $GRADLE_VERSION..." + + if ! command -v unzip >/dev/null 2>&1; then + error "Command 'unzip' tidak ditemukan." + exit 1 + fi + + unzip \ + -q \ + -o \ + "$GRADLE_ZIP" \ + -d "$LOCAL_GRADLE_DIR" + + EXTRACT_RESULT=$? + + if [[ $EXTRACT_RESULT -ne 0 ]]; then + error "Gagal mengekstrak Gradle." + exit 1 + fi + + rm -f "$GRADLE_ZIP" +fi + +if [[ ! -x "$GRADLE_BIN" ]]; then + error "Gradle executable tidak ditemukan:" + echo "$GRADLE_BIN" + exit 1 +fi + +ok "Gradle $GRADLE_VERSION tersedia." + +# ============================================================ +# Build +# ============================================================ + +info "Menjalankan clean build Lobby Systems v$MOD_VERSION..." + +echo + +"$GRADLE_BIN" clean build --no-daemon + +BUILD_RESULT=$? + +# ============================================================ +# BUILD FAILED +# ============================================================ + +if [[ $BUILD_RESULT -ne 0 ]]; then + + echo + separator + echo " BUILD FAILED" + separator + echo + + error "Build gagal dengan exit code $BUILD_RESULT." + info "Git Integration tidak dijalankan." + + exit "$BUILD_RESULT" +fi + +# ============================================================ +# BUILD SUCCESS +# ============================================================ + +echo +separator +echo " BUILD SUCCESS" +separator + +JAR_PATH="$SCRIPT_DIR/build/libs/lobby-systems-$MOD_VERSION.jar" + +if [[ -f "$JAR_PATH" ]]; then + + echo "JAR:" + echo "$JAR_PATH" + +else + + warn "Build berhasil, tetapi JAR utama tidak ditemukan pada:" + echo "$JAR_PATH" + +fi + +# ============================================================ +# Ask whether to use Git +# ============================================================ + +echo + +read -r -p "Commit dan push perubahan ke Git/Gitea? [Y/N]: " GIT_CONFIRM + +case "$GIT_CONFIRM" in + + y|Y|yes|YES|Yes) + ;; + + *) + echo + info "Git commit/push dilewati." + + echo + separator + echo " Lobby Systems v$MOD_VERSION selesai." + separator + + exit 0 + ;; +esac + +# ============================================================ +# Git Integration +# ============================================================ + +echo +separator +echo " Git Integration" +separator + +# ------------------------------------------------------------ +# Check Git +# ------------------------------------------------------------ + +if ! command -v git >/dev/null 2>&1; then + + error "Git tidak ditemukan di PATH." + info "Build tetap berhasil." + + exit 0 +fi + +# ------------------------------------------------------------ +# Check repository +# ------------------------------------------------------------ + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + + error "Folder ini belum menjadi Git repository." + + echo + echo "Jalankan terlebih dahulu:" + echo + echo " git init" + echo " git branch -M main" + echo " git remote add origin " + echo + + exit 0 +fi + +# ------------------------------------------------------------ +# Check source changes +# ------------------------------------------------------------ + +info "Memeriksa perubahan Git..." + +CHANGES="$(git status --porcelain)" + +if [[ -n "$CHANGES" ]]; then + + echo + echo "Perubahan ditemukan:" + echo + + git status --short + + echo + + read -r -p \ + "Commit message [Lobby Systems v$MOD_VERSION build]: " \ + COMMIT_MESSAGE + + if [[ -z "$COMMIT_MESSAGE" ]]; then + + TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" + + COMMIT_MESSAGE="Lobby Systems v$MOD_VERSION build - $TIMESTAMP" + + fi + + # -------------------------------------------------------- + # Git Add + # -------------------------------------------------------- + + info "Menambahkan perubahan ke staging..." + + if ! git add -A; then + + error "git add gagal." + exit 2 + + fi + + # -------------------------------------------------------- + # Git Commit + # -------------------------------------------------------- + + info "Membuat commit..." + + if ! git commit -m "$COMMIT_MESSAGE"; then + + error "git commit gagal." + exit 2 + + fi + +else + + info "Tidak ada perubahan source yang perlu di-commit." + info "Akan mencoba push commit yang sudah ada." + +fi + +# ============================================================ +# Determine current branch +# ============================================================ + +CURRENT_BRANCH="$(git branch --show-current)" + +if [[ -z "$CURRENT_BRANCH" ]]; then + + error "Tidak dapat menentukan branch Git aktif." + exit 2 + +fi + +info "Branch aktif: $CURRENT_BRANCH" + +# ============================================================ +# Check origin +# ============================================================ + +if ! git remote get-url origin >/dev/null 2>&1; then + + error "Remote Git 'origin' belum dikonfigurasi." + + echo + echo "Tambahkan remote Gitea dengan:" + echo + echo " git remote add origin " + echo + + info "Commit lokal tetap tersimpan." + + exit 2 +fi + +REMOTE_URL="$(git remote get-url origin)" + +info "Remote origin: $REMOTE_URL" + +# ============================================================ +# Git Push +# ============================================================ + +info "Push ke origin/$CURRENT_BRANCH..." + +if ! git push origin "$CURRENT_BRANCH"; then + + echo + error "Git push gagal." + info "Commit lokal tetap tersimpan." + + exit 2 + +fi + +# ============================================================ +# Complete +# ============================================================ + +echo +ok "Push ke Git/Gitea berhasil." + +echo +separator +echo " Lobby Systems v$MOD_VERSION selesai." +separator + +exit 0 \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..0dd64b4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true +org.gradle.configuration-cache=false +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.1.3 +maven_group=id.dartokloning +archives_base_name=lobby-systems diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5ce4160 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + maven { name = 'Fabric'; url = 'https://maven.fabricmc.net/' } + mavenCentral() + gradlePluginPortal() + } +} +rootProject.name = 'lobby-systems' diff --git a/src/main/java/id/dartokloning/lobbysystems/ConfigManager.java b/src/main/java/id/dartokloning/lobbysystems/ConfigManager.java new file mode 100644 index 0000000..fa7d038 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/ConfigManager.java @@ -0,0 +1,63 @@ +package id.dartokloning.lobbysystems; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class ConfigManager { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("lobby-systems.json"); + private LobbyConfig config = new LobbyConfig(); + + public LobbyConfig get() { + return config; + } + + public boolean load() { + if (!Files.exists(PATH)) { + save(); + return true; + } + + try (Reader reader = Files.newBufferedReader(PATH)) { + LobbyConfig loaded = GSON.fromJson(reader, LobbyConfig.class); + if (loaded == null) throw new IllegalArgumentException("Config JSON kosong atau tidak valid."); + validate(loaded); + config = loaded; + return true; + } catch (Exception e) { + LobbySystemsMod.LOGGER.error("Failed to load {}. Keeping previous config.", PATH, e); + return false; + } + } + + public void save() { + try { + Files.createDirectories(PATH.getParent()); + try (Writer writer = Files.newBufferedWriter(PATH)) { + GSON.toJson(config, writer); + } + } catch (IOException e) { + LobbySystemsMod.LOGGER.error("Failed to save {}", PATH, e); + } + } + + public Path path() { + return PATH; + } + + private static void validate(LobbyConfig c) { + if (c.lobbyDimension == null || c.lobbyDimension.isBlank()) { + throw new IllegalArgumentException("lobbyDimension tidak boleh kosong."); + } + if (c.minimumPlaySeconds < 0) { + throw new IllegalArgumentException("minimumPlaySeconds tidak boleh negatif."); + } + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java b/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java new file mode 100644 index 0000000..872a9e1 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/LobbyConfig.java @@ -0,0 +1,15 @@ +package id.dartokloning.lobbysystems; + +public final class LobbyConfig { + public String lobbyDimension = "minecraft:overworld"; + public double lobbyX = 0.5; + public double lobbyY = 100.0; + public double lobbyZ = 0.5; + public float lobbyYaw = 0.0f; + public float lobbyPitch = 0.0f; + public int minimumPlaySeconds = 300; + + public String inLobbyMessage = "§eKamu sudah berada di lobby."; + public String returnedMessage = "§aKamu telah kembali ke lobby."; + public String waitMessage = "§cKamu belum bisa kembali ke lobby. Tunggu §e{time}§c lagi."; +} diff --git a/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java new file mode 100644 index 0000000..9f4fc3f --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/LobbySystemsMod.java @@ -0,0 +1,265 @@ +package id.dartokloning.lobbysystems; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.logging.LogUtils; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import org.slf4j.Logger; + +public final class LobbySystemsMod implements ModInitializer { + public static final String MOD_ID = "dartokloning"; + public static final Logger LOGGER = LogUtils.getLogger(); + + private static final ConfigManager CONFIG = new ConfigManager(); + private static final SessionTracker SESSIONS = new SessionTracker(); + private static final PositionManager POSITIONS = new PositionManager(); + private static final TeleportCoordinator TELEPORTS = new TeleportCoordinator(); + private static int saveTicker; + + @Override + public void onInitialize() { + CONFIG.load(); + POSITIONS.load(); + POSITIONS.setExcludedDimension(lobbyKey()); + + ServerPlayConnectionEvents.JOIN.register((listener, sender, server) -> { + ServerPlayer player = listener.getPlayer(); + server.execute(() -> { + // Simpan posisi login terakhir jika player sebelumnya logout di game dimension. + // Kalau player login di lobby, PositionManager akan mengabaikannya secara mutlak. + POSITIONS.captureCurrent(player); + + // Jangan cross-dimension teleport pada callback JOIN itu sendiri. + // Tunggu 20 tick supaya client sudah stabil di play state. + TELEPORTS.scheduleJoinToLobby(player); + LOGGER.info("Scheduled lobby login teleport for {} ({})", player.getName().getString(), player.getUUID()); + }); + }); + + ServerPlayConnectionEvents.DISCONNECT.register((listener, server) -> { + ServerPlayer player = listener.getPlayer(); + POSITIONS.onDisconnect(player); + SESSIONS.onDisconnect(player.getUUID()); + TELEPORTS.onDisconnect(player.getUUID()); + }); + + ServerTickEvents.END_SERVER_TICK.register(server -> { + TELEPORTS.tick(server, (player, minecraftServer) -> { + if (teleportToLobby(player, minecraftServer, false)) { + SESSIONS.onJoin(player.getUUID(), lobbyKey()); + } + }); + + ResourceKey lobby = lobbyKey(); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + POSITIONS.update(player); + SESSIONS.update(player.getUUID(), player.level().dimension(), lobby); + } + + if (++saveTicker >= 1200) { + saveTicker = 0; + POSITIONS.saveIfDirty(); + } + }); + + CommandRegistrationCallback.EVENT.register((dispatcher, buildContext, selection) -> { + dispatcher.register(Commands.literal("lobby").executes(ctx -> { + ServerPlayer player = ctx.getSource().getPlayerOrException(); + ResourceKey lobby = lobbyKey(); + + if (player.level().dimension().equals(lobby)) { + player.sendSystemMessage(Component.literal(CONFIG.get().inLobbyMessage)); + return 1; + } + + if (TELEPORTS.isLocked(player)) { + player.sendSystemMessage(Component.literal("§eTeleport sebelumnya masih diproses. Tunggu sebentar lalu coba lagi.")); + return 0; + } + + long remaining = SESSIONS.remainingSeconds(player.getUUID(), CONFIG.get().minimumPlaySeconds); + if (remaining > 0) { + player.sendSystemMessage(Component.literal( + CONFIG.get().waitMessage.replace("{time}", formatDuration(remaining)))); + return 0; + } + + POSITIONS.captureCurrent(player); + return teleportToLobby(player, ctx.getSource().getServer(), true) ? 1 : 0; + })); + + dispatcher.register(Commands.literal("ls") + .requires(Commands.hasPermission(Commands.LEVEL_ADMINS)) + .then(Commands.literal("reload") + .executes(ctx -> reloadConfig(ctx.getSource().getServer(), ctx.getSource()))) + .then(Commands.literal("tp") + .then(Commands.argument("dimension", StringArgumentType.greedyString()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggestResource( + ctx.getSource().getServer().levelKeys().stream().map(ResourceKey::identifier), builder)) + .executes(ctx -> teleportToDimension( + ctx.getSource().getPlayerOrException(), + ctx.getSource().getServer(), + StringArgumentType.getString(ctx, "dimension") + )))) + ); + }); + + LOGGER.info("Lobby Systems v1.1.3 initialized for Minecraft 26.2."); + } + + private static int reloadConfig(MinecraftServer server, net.minecraft.commands.CommandSourceStack source) { + if (!CONFIG.load()) { + source.sendFailure(Component.literal("§cLobby Systems: reload gagal. Cek console untuk detail error JSON/config.")); + return 0; + } + + final ResourceKey key; + try { + key = lobbyKey(); + if (server.getLevel(key) == null) { + source.sendFailure(Component.literal("§eConfig berhasil dibaca, tetapi lobbyDimension '" + CONFIG.get().lobbyDimension + "' tidak sedang ter-load.")); + return 0; + } + } catch (Exception e) { + source.sendFailure(Component.literal("§cConfig berhasil dibaca, tetapi lobbyDimension tidak valid: " + CONFIG.get().lobbyDimension)); + return 0; + } + + // Ini bukan sekadar cleanup sekali. PositionManager menjadikan lobby sebagai + // excluded dimension permanen dan akan menyaringnya lagi sebelum setiap save. + POSITIONS.setExcludedDimension(key); + + source.sendSuccess(() -> Component.literal( + "§aLobby Systems config direload. §7Lobby: §f" + CONFIG.get().lobbyDimension + + " §7| Minimum play: §f" + CONFIG.get().minimumPlaySeconds + "s"), false); + return 1; + } + + private static int teleportToDimension(ServerPlayer player, MinecraftServer server, String dimensionId) { + final Identifier id; + try { + id = Identifier.parse(dimensionId.trim()); + } catch (Exception e) { + player.sendSystemMessage(Component.literal("§cDimension ID tidak valid: " + dimensionId)); + return 0; + } + + ResourceKey key = ResourceKey.create(Registries.DIMENSION, id); + ServerLevel target = server.getLevel(key); + if (target == null) { + player.sendSystemMessage(Component.literal("§cDimension '" + dimensionId + "' tidak ditemukan atau tidak ter-load.")); + return 0; + } + + ResourceKey lobby = lobbyKey(); + if (key.equals(lobby)) { + POSITIONS.captureCurrent(player); + boolean ok = teleportToLobby(player, server, false); + if (ok) { + player.sendSystemMessage(Component.literal("§aTeleport ke lobby pada posisi spawn yang dikonfigurasi.")); + return 1; + } + return 0; + } + + if (player.level().dimension().equals(key)) { + player.sendSystemMessage(Component.literal("§eKamu sudah berada di dimension " + dimensionId + ".")); + return 1; + } + + if (TELEPORTS.isLocked(player)) { + player.sendSystemMessage(Component.literal("§eTeleport sebelumnya masih diproses. Tunggu sebentar lalu coba lagi.")); + return 0; + } + + POSITIONS.captureCurrent(player); + PlayerPosition saved = POSITIONS.get(player.getUUID(), key); + + double x; + double y; + double z; + float yaw; + float pitch; + boolean returning; + + if (saved != null) { + x = saved.x; + y = saved.y; + z = saved.z; + yaw = saved.yaw; + pitch = saved.pitch; + returning = true; + } else { + BlockPos spawn = target.getRespawnData().pos(); + x = spawn.getX() + 0.5; + y = spawn.getY(); + z = spawn.getZ() + 0.5; + yaw = player.getYRot(); + pitch = player.getXRot(); + returning = false; + } + + boolean ok = TELEPORTS.teleport(player, target, x, y, z, yaw, pitch); + if (!ok) { + player.sendSystemMessage(Component.literal("§cTeleport ke " + dimensionId + " gagal.")); + return 0; + } + + player.sendSystemMessage(Component.literal(returning + ? "§aTeleport ke §f" + dimensionId + "§a pada posisi terakhirmu." + : "§aTeleport ke §f" + dimensionId + "§a. Belum ada posisi terakhir, jadi menggunakan spawn dimension.")); + return 1; + } + + private static ResourceKey lobbyKey() { + Identifier id = Identifier.parse(CONFIG.get().lobbyDimension); + return ResourceKey.create(Registries.DIMENSION, id); + } + + private static boolean teleportToLobby(ServerPlayer player, MinecraftServer server, boolean sendMessage) { + final ResourceKey key; + try { + key = lobbyKey(); + } catch (Exception e) { + player.sendSystemMessage(Component.literal("§cLobby Systems: lobbyDimension tidak valid.")); + LOGGER.error("Invalid lobbyDimension: {}", CONFIG.get().lobbyDimension, e); + return false; + } + + ServerLevel target = server.getLevel(key); + if (target == null) { + player.sendSystemMessage(Component.literal("§cLobby Systems: dimensi lobby '" + CONFIG.get().lobbyDimension + "' tidak ditemukan.")); + LOGGER.error("Lobby dimension {} is not loaded/found.", CONFIG.get().lobbyDimension); + return false; + } + + LobbyConfig c = CONFIG.get(); + + // Jika sudah di lobby (mis. join terakhir memang lobby), tidak perlu cross-dimension + // transition. Tetap pakai TeleportTransition untuk menempatkan player ke canonical spawn. + boolean ok = TELEPORTS.teleport(player, target, c.lobbyX, c.lobbyY, c.lobbyZ, c.lobbyYaw, c.lobbyPitch); + if (ok && sendMessage) { + player.sendSystemMessage(Component.literal(c.returnedMessage)); + } + return ok; + } + + private static String formatDuration(long seconds) { + long minutes = seconds / 60; + long secs = seconds % 60; + return minutes > 0 ? minutes + "m " + secs + "s" : secs + "s"; + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/PlayerPosition.java b/src/main/java/id/dartokloning/lobbysystems/PlayerPosition.java new file mode 100644 index 0000000..c3111fc --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/PlayerPosition.java @@ -0,0 +1,20 @@ +package id.dartokloning.lobbysystems; + +public final class PlayerPosition { + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + + public PlayerPosition() { + } + + public PlayerPosition(double x, double y, double z, float yaw, float pitch) { + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/PositionManager.java b/src/main/java/id/dartokloning/lobbysystems/PositionManager.java new file mode 100644 index 0000000..b6bd065 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/PositionManager.java @@ -0,0 +1,139 @@ +package id.dartokloning.lobbysystems; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; + +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public final class PositionManager { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("lobby-systems-positions.json"); + private static final Type DATA_TYPE = new TypeToken>>() {}.getType(); + + private final Map> positions = new HashMap<>(); + private final Map snapshots = new HashMap<>(); + private ResourceKey excludedDimension; + private boolean dirty; + + public void load() { + positions.clear(); + if (!Files.exists(PATH)) return; + + try (Reader reader = Files.newBufferedReader(PATH)) { + Map> 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); + } + } + + /** + * Dimension ini sama sekali tidak boleh disimpan sebagai last-position. + * Dipakai untuk lobby. Pemanggilan method ini juga membersihkan data lama + * yang mungkin masih tersisa dari versi mod sebelumnya. + */ + public void setExcludedDimension(ResourceKey dimension) { + this.excludedDimension = dimension; + purgeExcludedDimension(); + saveIfDirty(); + } + + public void update(ServerPlayer player) { + UUID uuid = player.getUUID(); + Snapshot current = Snapshot.from(player); + Snapshot previous = snapshots.put(uuid, current); + + if (previous != null && !previous.dimension.equals(current.dimension)) { + store(uuid, previous.dimension, previous.position); + } + } + + public void captureCurrent(ServerPlayer player) { + Snapshot snapshot = Snapshot.from(player); + snapshots.put(player.getUUID(), snapshot); + store(player.getUUID(), snapshot.dimension, snapshot.position); + } + + public void onDisconnect(ServerPlayer player) { + captureCurrent(player); + snapshots.remove(player.getUUID()); + saveIfDirty(); + } + + public PlayerPosition get(UUID uuid, ResourceKey dimension) { + if (isExcluded(dimension)) return null; + Map byDimension = positions.get(uuid.toString()); + if (byDimension == null) return null; + return byDimension.get(dimension.identifier().toString()); + } + + public void saveIfDirty() { + if (!dirty) return; + save(); + } + + public void save() { + // Guard terakhir: walaupun ada bug di caller, data lobby tidak boleh masuk file. + purgeExcludedDimension(); + + try { + Files.createDirectories(PATH.getParent()); + 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 dimension, PlayerPosition position) { + if (isExcluded(dimension)) return; + + positions.computeIfAbsent(uuid.toString(), ignored -> new HashMap<>()) + .put(dimension.identifier().toString(), position); + dirty = true; + } + + private boolean isExcluded(ResourceKey dimension) { + return excludedDimension != null && excludedDimension.equals(dimension); + } + + private void purgeExcludedDimension() { + if (excludedDimension == null) return; + + String dimensionId = excludedDimension.identifier().toString(); + boolean changed = false; + + for (Map byDimension : positions.values()) { + if (byDimension.remove(dimensionId) != null) changed = true; + } + if (positions.entrySet().removeIf(entry -> entry.getValue().isEmpty())) changed = true; + + if (changed) { + dirty = true; + LobbySystemsMod.LOGGER.info("Removed persisted lobby positions for dimension {}.", dimensionId); + } + } + + private record Snapshot(ResourceKey dimension, PlayerPosition position) { + static Snapshot from(ServerPlayer player) { + return new Snapshot( + player.level().dimension(), + new PlayerPosition(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()) + ); + } + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/SessionTracker.java b/src/main/java/id/dartokloning/lobbysystems/SessionTracker.java new file mode 100644 index 0000000..873228d --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/SessionTracker.java @@ -0,0 +1,40 @@ +package id.dartokloning.lobbysystems; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public final class SessionTracker { + private final Map> lastDimension = new HashMap<>(); + private final Map gameEnteredAtMillis = new HashMap<>(); + + public void onJoin(UUID id, ResourceKey dimension) { + lastDimension.put(id, dimension); + gameEnteredAtMillis.remove(id); + } + + public void onDisconnect(UUID id) { + lastDimension.remove(id); + gameEnteredAtMillis.remove(id); + } + + public void update(UUID id, ResourceKey current, ResourceKey lobby) { + ResourceKey previous = lastDimension.put(id, current); + if (previous == null || previous.equals(current)) return; + if (current.equals(lobby)) { + gameEnteredAtMillis.remove(id); + } else { + gameEnteredAtMillis.put(id, System.currentTimeMillis()); + } + } + + public long remainingSeconds(UUID id, int minimumSeconds) { + Long enteredAt = gameEnteredAtMillis.get(id); + if (enteredAt == null) return minimumSeconds; + long elapsed = Math.max(0L, (System.currentTimeMillis() - enteredAt) / 1000L); + return Math.max(0L, minimumSeconds - elapsed); + } +} diff --git a/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java b/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java new file mode 100644 index 0000000..557fb04 --- /dev/null +++ b/src/main/java/id/dartokloning/lobbysystems/TeleportCoordinator.java @@ -0,0 +1,109 @@ +package id.dartokloning.lobbysystems; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Relative; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.portal.TeleportTransition; +import net.minecraft.world.phys.Vec3; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Menangani teleport lintas dimensi melalui TeleportTransition vanilla. + * Juga memberi lock singkat agar request teleport tidak bertumpuk ketika + * client masih menyelesaikan pergantian dimension/chunk. + */ +public final class TeleportCoordinator { + private static final int TELEPORT_LOCK_TICKS = 40; // 2 detik + private static final int JOIN_DELAY_TICKS = 20; // 1 detik setelah JOIN + + private final Map lockedUntilTick = new HashMap<>(); + private final Map pendingJoinTeleports = new HashMap<>(); + private long currentTick; + + public void scheduleJoinToLobby(ServerPlayer player) { + pendingJoinTeleports.put(player.getUUID(), new PendingLobbyTeleport(currentTick + JOIN_DELAY_TICKS)); + } + + public void onDisconnect(UUID uuid) { + lockedUntilTick.remove(uuid); + pendingJoinTeleports.remove(uuid); + } + + public void tick(MinecraftServer server, LobbyTeleportHandler lobbyHandler) { + currentTick++; + lockedUntilTick.entrySet().removeIf(entry -> entry.getValue() <= currentTick); + + pendingJoinTeleports.entrySet().removeIf(entry -> { + if (entry.getValue().executeAtTick > currentTick) return false; + + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + if (player != null) { + lobbyHandler.teleport(player, server); + } + return true; + }); + } + + public boolean isLocked(ServerPlayer player) { + return lockedUntilTick.getOrDefault(player.getUUID(), 0L) > currentTick; + } + + public boolean teleport(ServerPlayer player, ServerLevel target, + double x, double y, double z, float yaw, float pitch) { + if (isLocked(player)) { + player.sendSystemMessage(Component.literal("§eTeleport sebelumnya masih diproses. Tunggu sebentar lalu coba lagi.")); + return false; + } + + ResourceKey from = player.level().dimension(); + ResourceKey to = target.dimension(); + + // Paksa destination chunk tersedia sebelum transition dikirim ke client. + int chunkX = ((int) Math.floor(x)) >> 4; + int chunkZ = ((int) Math.floor(z)) >> 4; + target.getChunk(chunkX, chunkZ); + + lockedUntilTick.put(player.getUUID(), currentTick + TELEPORT_LOCK_TICKS); + + TeleportTransition transition = new TeleportTransition( + target, + new Vec3(x, y, z), + Vec3.ZERO, + yaw, + pitch, + Set.of(), + TeleportTransition.DO_NOTHING + ); + + LobbySystemsMod.LOGGER.info( + "Teleporting {} ({}): {} -> {} @ [{}, {}, {}]", + player.getName().getString(), player.getUUID(), + from.identifier(), to.identifier(), + String.format("%.2f", x), String.format("%.2f", y), String.format("%.2f", z) + ); + + ServerPlayer result = player.teleport(transition); + if (result == null) { + lockedUntilTick.remove(player.getUUID()); + LobbySystemsMod.LOGGER.warn("Teleport failed for {}: {} -> {}", player.getUUID(), from.identifier(), to.identifier()); + return false; + } + + return true; + } + + @FunctionalInterface + public interface LobbyTeleportHandler { + void teleport(ServerPlayer player, MinecraftServer server); + } + + private record PendingLobbyTeleport(long executeAtTick) {} +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..9b4ce66 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "id": "dartokloning", + "version": "${version}", + "name": "Lobby Systems", + "description": "Server-side Fabric lobby routing and minimum game-session timer.", + "authors": ["DartoKloning"], + "license": "MIT", + "environment": "server", + "entrypoints": { + "main": ["id.dartokloning.lobbysystems.LobbySystemsMod"] + }, + "depends": { + "fabricloader": ">=0.19.3", + "minecraft": "~26.2", + "java": ">=25", + "fabric-api": "*" + } +}