diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..7e9a8da --- /dev/null +++ b/server/README.md @@ -0,0 +1,114 @@ +# Hytale F2P - Dedicated Server + +Host your own Hytale server. The scripts handle everything automatically. + +## Prerequisites + +- **Java 25+** — [Windows installer](https://download.oracle.com/java/25/latest/jdk-25_windows-x64_bin.exe) | [Other platforms](https://adoptium.net/) + - If you have the F2P launcher installed, its bundled Java will be used automatically +- **Internet connection** for first launch (downloads ~3.5 GB of game files) + - If you have the F2P launcher installed, game files are copied locally (no download needed) + +## Quick Start + +### Windows + +1. Download `start.bat` to an empty folder +2. Double-click `start.bat` +3. Done — server starts on port **5520** + +### Linux / macOS + +```bash +mkdir hytale-server && cd hytale-server +curl -O https://raw.githubusercontent.com/amiayweb/Hytale-F2P/develop/server/start.sh +chmod +x start.sh +./start.sh +``` + +## What the scripts do + +1. Search for the F2P launcher install (default paths + custom `installPath` from config) +2. Use bundled Java from the launcher, or fall back to system Java (25+ required) +3. Copy game files from the launcher install if available +4. Download missing files: `HytaleServer.jar` (~150 MB), `Assets.zip` (~3.3 GB), `dualauth-agent.jar` (~5 MB) +5. Check for updates on every launch (server, assets, and agent) +6. Generate a persistent server ID +7. Fetch authentication tokens +8. Start the server with dual-auth support + +## Connecting + +- **Same PC**: Connect to `localhost:5520` or `127.0.0.1:5520` +- **LAN**: Connect to your local IP (e.g. `192.168.1.x:5520`) +- **Internet**: Forward port `5520` (TCP + UDP) on your router, friends connect to your public IP + +### No public IP? Use playit.gg (recommended) + +If you're behind CGNAT or can't port forward, [playit.gg](https://playit.gg) gives you a public address for free: + +1. Go to [playit.gg](https://playit.gg) and create an account +2. Download and run the playit agent +3. Create a tunnel — select **Hytale** as the game type, local port `5520` +4. Share the generated address with friends (e.g. `something.joinplayit.gg:12345`) + +### Other options + +- [Radmin VPN](https://www.radmin-vpn.com/) — virtual LAN, all players must install it +- [ZeroTier](https://www.zerotier.com/) — same idea, create a network, friends join and connect via VPN IP + +## Configuration + +Set environment variables before running the script: + +| Variable | Default | Description | +|----------|---------|-------------| +| `SERVER_NAME` | `My Hytale Server` | Server name shown in listings | +| `BIND_ADDRESS` | `0.0.0.0:5520` | IP and port to listen on | +| `JVM_XMX` | *(Java default)* | Max memory (e.g. `4G`, `8G`) | +| `JVM_XMS` | *(Java default)* | Initial memory | +| `AUTH_MODE` | `authenticated` | Auth mode (`authenticated` or `none`) | +| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain | +| `DOWNLOAD_BASE` | `https://download.sanasol.ws/download` | File download URL | + +**Example (Linux):** +```bash +SERVER_NAME="Epic Server" JVM_XMX=4G ./start.sh +``` + +**Example (Windows):** +```cmd +set SERVER_NAME=Epic Server +set JVM_XMX=4G +start.bat +``` + +## Files created + +``` +your-folder/ +├── start.sh / start.bat # Startup script +├── HytaleServer.jar # Game server (auto-downloaded) +├── Assets.zip # Game assets (auto-downloaded) +├── dualauth-agent.jar # Auth agent (auto-downloaded) +├── .server-id # Persistent server UUID +├── .versions/ # Version tracking for auto-updates +├── Server/ # Server data (created by server) +│ ├── config.json +│ └── worlds/ +└── UserData/ # Player saves +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| `java not found` | Install the F2P launcher (includes Java) or install Java 25+ from [adoptium.net](https://adoptium.net/) | +| Download fails | Check internet connection. Files can be downloaded manually from `https://download.sanasol.ws/download/` | +| Port already in use | Change port: `BIND_ADDRESS=0.0.0.0:5521 ./start.sh` | +| Out of memory | Set more RAM: `JVM_XMX=4G ./start.sh` | +| Friends can't connect | Forward port 5520 (TCP+UDP) on your router, or use [playit.gg](https://playit.gg) if you can't port forward | + +## Discord + +Need help? Join the community: https://discord.gg/Fhbb9Yk5WW diff --git a/server/start.bat b/server/start.bat new file mode 100644 index 0000000..d2bb119 --- /dev/null +++ b/server/start.bat @@ -0,0 +1,441 @@ +@echo off +setlocal enabledelayedexpansion + +:: ============================================================ +:: Hytale F2P Dedicated Server - One-Click Starter +:: ============================================================ +:: Just double-click this file to start your server! +:: +:: The script will: +:: 1. Look for game files and Java in your F2P launcher install +:: 2. Auto-download anything missing +:: 3. Auto-update server, assets, and agent on each launch +:: 4. Fetch auth tokens and start the server +:: ============================================================ + +:: Configuration (edit these or set as environment variables) +if not defined HYTALE_AUTH_DOMAIN set "HYTALE_AUTH_DOMAIN=auth.sanasol.ws" +if not defined AUTH_SERVER set "AUTH_SERVER=https://%HYTALE_AUTH_DOMAIN%" +if not defined SERVER_NAME set "SERVER_NAME=My Hytale Server" +if not defined ASSETS_PATH set "ASSETS_PATH=.\Assets.zip" +if not defined BIND_ADDRESS set "BIND_ADDRESS=0.0.0.0:5520" +if not defined AUTH_MODE set "AUTH_MODE=authenticated" +if not defined DOWNLOAD_BASE set "DOWNLOAD_BASE=https://download.sanasol.ws/download" + +:: File names +set "AGENT_JAR=dualauth-agent.jar" +set "SERVER_JAR=HytaleServer.jar" +set "AGENT_URL=https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar" +set "AGENT_VERSION_API=https://api.github.com/repos/sanasol/hytale-auth-server/releases/latest" +set "VERSION_DIR=.versions" + +echo ============================================================ +echo Hytale F2P Dedicated Server +echo ============================================================ +echo. + +:: --- Prerequisite Checks --- + +where curl >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] curl is required but not found + echo [ERROR] curl comes with Windows 10+. Update Windows or install curl. + pause + exit /b 1 +) + +if not exist "%VERSION_DIR%" mkdir "%VERSION_DIR%" + +:: --- Find Local F2P Launcher Install --- + +set "F2P_DIR=" +set "F2P_BASE=%USERPROFILE%\AppData\Local\HytaleF2P" +set "F2P_CONFIG=%F2P_BASE%\config.json" +set "JAVA_CMD=java" + +:: Check config.json for custom installPath +set "F2P_CUSTOM_BASE=" +if exist "%F2P_CONFIG%" ( + for /f "delims=" %%p in ('powershell -Command "try { $c = Get-Content '%F2P_CONFIG%' | ConvertFrom-Json; if ($c.installPath) { $c.installPath.Trim() + '\HytaleF2P' } } catch {}" 2^>nul') do ( + set "F2P_CUSTOM_BASE=%%p" + ) +) + +:: Search for game files: custom path first, then default +if defined F2P_CUSTOM_BASE ( + if exist "!F2P_CUSTOM_BASE!\release\package\game\latest" ( + set "F2P_DIR=!F2P_CUSTOM_BASE!\release\package\game\latest" + ) else if exist "!F2P_CUSTOM_BASE!\pre-release\package\game\latest" ( + set "F2P_DIR=!F2P_CUSTOM_BASE!\pre-release\package\game\latest" + ) +) + +if not defined F2P_DIR ( + if exist "%F2P_BASE%\release\package\game\latest" ( + set "F2P_DIR=%F2P_BASE%\release\package\game\latest" + ) else if exist "%F2P_BASE%\pre-release\package\game\latest" ( + set "F2P_DIR=%F2P_BASE%\pre-release\package\game\latest" + ) +) + +:: --- Find Java from F2P launcher --- + +:: Check config.json for custom javaPath +if exist "%F2P_CONFIG%" ( + for /f "delims=" %%j in ('powershell -Command "try { $c = Get-Content '%F2P_CONFIG%' | ConvertFrom-Json; if ($c.javaPath -and (Test-Path $c.javaPath)) { $c.javaPath.Trim() } } catch {}" 2^>nul') do ( + set "JAVA_CMD=%%j" + echo [INFO] Found Java in F2P config: %%j + ) +) + +:: Check bundled JRE if no custom javaPath found +if "!JAVA_CMD!"=="java" ( + set "F2P_JRE_BASE=" + if defined F2P_CUSTOM_BASE ( + if exist "!F2P_CUSTOM_BASE!\release\package\jre\latest\bin\java.exe" ( + set "F2P_JRE_BASE=!F2P_CUSTOM_BASE!\release\package\jre\latest" + ) else if exist "!F2P_CUSTOM_BASE!\pre-release\package\jre\latest\bin\java.exe" ( + set "F2P_JRE_BASE=!F2P_CUSTOM_BASE!\pre-release\package\jre\latest" + ) + ) + if not defined F2P_JRE_BASE ( + if exist "%F2P_BASE%\release\package\jre\latest\bin\java.exe" ( + set "F2P_JRE_BASE=%F2P_BASE%\release\package\jre\latest" + ) else if exist "%F2P_BASE%\pre-release\package\jre\latest\bin\java.exe" ( + set "F2P_JRE_BASE=%F2P_BASE%\pre-release\package\jre\latest" + ) + ) + if defined F2P_JRE_BASE ( + set "JAVA_CMD=!F2P_JRE_BASE!\bin\java.exe" + echo [INFO] Found Java in F2P launcher: !JAVA_CMD! + ) +) + +:: Verify java exists +"!JAVA_CMD!" -version >nul 2>&1 +if errorlevel 1 ( + where java >nul 2>&1 + if errorlevel 1 ( + echo [ERROR] Java is not installed and no F2P launcher JRE found + echo. + echo Options: + echo 1. Install the F2P launcher first ^(it includes Java^) + echo 2. Download Java 25: https://download.oracle.com/java/25/latest/jdk-25_windows-x64_bin.exe + echo. + pause + exit /b 1 + ) + set "JAVA_CMD=java" +) + +:: Check Java version +for /f "tokens=3 delims= " %%v in ('"!JAVA_CMD!" -version 2^>^&1 ^| findstr /i "version"') do ( + set "JAVA_VER_RAW=%%~v" +) +if defined JAVA_VER_RAW ( + for /f "tokens=1 delims=." %%m in ("!JAVA_VER_RAW!") do set "JAVA_MAJOR=%%m" +) + +echo [INFO] Java: !JAVA_VER_RAW! ^(!JAVA_CMD!^) + +if defined JAVA_MAJOR ( + if !JAVA_MAJOR! LSS 25 ( + echo [ERROR] Java !JAVA_MAJOR! detected. Java 25+ is REQUIRED. + echo The DualAuth agent requires Java 25 ^(class file version 69^). + echo. + echo Download Java 25: https://download.oracle.com/java/25/latest/jdk-25_windows-x64_bin.exe + echo. + pause + exit /b 1 + ) +) + +:: --- Copy game files from F2P install --- + +if defined F2P_DIR ( + echo [INFO] Found F2P launcher game files: !F2P_DIR! + + if not exist "%SERVER_JAR%" ( + if exist "!F2P_DIR!\Server\HytaleServer.jar" ( + echo [INFO] Found HytaleServer.jar in F2P launcher + echo [INFO] Copying from: !F2P_DIR!\Server\HytaleServer.jar + copy "!F2P_DIR!\Server\HytaleServer.jar" "%SERVER_JAR%" >nul + echo [INFO] Copied successfully + ) + ) + + if not exist "%ASSETS_PATH%" ( + if exist "!F2P_DIR!\Assets.zip" ( + echo [INFO] Found Assets.zip in F2P launcher + echo [INFO] Copying from: !F2P_DIR!\Assets.zip + copy "!F2P_DIR!\Assets.zip" "%ASSETS_PATH%" >nul + echo [INFO] Copied successfully + ) + ) + + echo. +) else ( + echo [INFO] No F2P launcher install found, will download files + echo. +) + +:: --- Download / Update HytaleServer.jar --- + +set "JAR_URL=%DOWNLOAD_BASE%/HytaleServer.jar" +set "JAR_VERSION_FILE=%VERSION_DIR%\HytaleServer.jar.version" + +if not exist "%SERVER_JAR%" ( + echo [INFO] HytaleServer.jar not found, downloading... + echo [INFO] Expected size: ~150 MB + curl -fL --progress-bar -o "%SERVER_JAR%.tmp" "%JAR_URL%" --connect-timeout 15 --max-time 3600 + if exist "%SERVER_JAR%.tmp" ( + move /y "%SERVER_JAR%.tmp" "%SERVER_JAR%" >nul + echo [INFO] HytaleServer.jar downloaded + for /f "delims=" %%h in ('powershell -Command "try { (Invoke-WebRequest -Uri '%JAR_URL%' -Method Head -UseBasicParsing).Headers['ETag'] } catch {}" 2^>nul') do ( + echo %%h>"%JAR_VERSION_FILE%" + ) + ) else ( + echo [ERROR] Failed to download HytaleServer.jar + echo [ERROR] Check your internet connection + pause + exit /b 1 + ) +) else ( + echo [INFO] Checking for HytaleServer.jar updates... + set "LOCAL_JAR_VER=" + if exist "%JAR_VERSION_FILE%" set /p LOCAL_JAR_VER=<"%JAR_VERSION_FILE%" + + set "REMOTE_JAR_VER=" + for /f "delims=" %%h in ('powershell -Command "try { (Invoke-WebRequest -Uri '%JAR_URL%' -Method Head -UseBasicParsing -TimeoutSec 10).Headers['ETag'] } catch { '' }" 2^>nul') do ( + set "REMOTE_JAR_VER=%%h" + ) + + if defined REMOTE_JAR_VER ( + if "!LOCAL_JAR_VER!"=="!REMOTE_JAR_VER!" ( + echo [INFO] HytaleServer.jar is up to date + ) else ( + echo [INFO] HytaleServer.jar update available, downloading... + curl -fL --progress-bar -o "%SERVER_JAR%.tmp" "%JAR_URL%" --connect-timeout 15 --max-time 3600 + if exist "%SERVER_JAR%.tmp" ( + move /y "%SERVER_JAR%.tmp" "%SERVER_JAR%" >nul + echo !REMOTE_JAR_VER!>"%JAR_VERSION_FILE%" + echo [INFO] HytaleServer.jar updated + ) else ( + echo [WARN] Update failed, using existing HytaleServer.jar + ) + ) + ) else ( + echo [INFO] Could not check for updates, using existing HytaleServer.jar + ) +) + +:: --- Download / Update Assets.zip --- + +set "ASSETS_URL=%DOWNLOAD_BASE%/Assets.zip" +set "ASSETS_VERSION_FILE=%VERSION_DIR%\Assets.zip.version" + +if not exist "%ASSETS_PATH%" ( + echo [INFO] Assets.zip not found, downloading... + echo [INFO] Expected size: ~3.3 GB - this will take a while + curl -fL --progress-bar -o "%ASSETS_PATH%.tmp" "%ASSETS_URL%" --connect-timeout 15 --max-time 7200 + if exist "%ASSETS_PATH%.tmp" ( + move /y "%ASSETS_PATH%.tmp" "%ASSETS_PATH%" >nul + echo [INFO] Assets.zip downloaded + for /f "delims=" %%h in ('powershell -Command "try { (Invoke-WebRequest -Uri '%ASSETS_URL%' -Method Head -UseBasicParsing).Headers['ETag'] } catch {}" 2^>nul') do ( + echo %%h>"%ASSETS_VERSION_FILE%" + ) + ) else ( + echo [ERROR] Failed to download Assets.zip + echo [ERROR] Check your internet connection + pause + exit /b 1 + ) +) else ( + echo [INFO] Checking for Assets.zip updates... + set "LOCAL_ASSETS_VER=" + if exist "%ASSETS_VERSION_FILE%" set /p LOCAL_ASSETS_VER=<"%ASSETS_VERSION_FILE%" + + set "REMOTE_ASSETS_VER=" + for /f "delims=" %%h in ('powershell -Command "try { (Invoke-WebRequest -Uri '%ASSETS_URL%' -Method Head -UseBasicParsing -TimeoutSec 10).Headers['ETag'] } catch { '' }" 2^>nul') do ( + set "REMOTE_ASSETS_VER=%%h" + ) + + if defined REMOTE_ASSETS_VER ( + if "!LOCAL_ASSETS_VER!"=="!REMOTE_ASSETS_VER!" ( + echo [INFO] Assets.zip is up to date + ) else ( + echo [INFO] Assets.zip update available, downloading... + echo [INFO] This is a large file ^(~3.3 GB^), please be patient + curl -fL --progress-bar -o "%ASSETS_PATH%.tmp" "%ASSETS_URL%" --connect-timeout 15 --max-time 7200 + if exist "%ASSETS_PATH%.tmp" ( + move /y "%ASSETS_PATH%.tmp" "%ASSETS_PATH%" >nul + echo !REMOTE_ASSETS_VER!>"%ASSETS_VERSION_FILE%" + echo [INFO] Assets.zip updated + ) else ( + echo [WARN] Update failed, using existing Assets.zip + ) + ) + ) else ( + echo [INFO] Could not check for updates, using existing Assets.zip + ) +) + +:: --- Download / Update DualAuth Agent --- + +set "AGENT_VERSION_FILE=%VERSION_DIR%\dualauth-agent.jar.version" + +if not exist "%AGENT_JAR%" ( + echo [INFO] Downloading DualAuth Agent... + curl -fL -# -o "%AGENT_JAR%.tmp" "%AGENT_URL%" --connect-timeout 15 --max-time 120 + if exist "%AGENT_JAR%.tmp" ( + move /y "%AGENT_JAR%.tmp" "%AGENT_JAR%" >nul + echo [INFO] DualAuth Agent downloaded + for /f "delims=" %%v in ('powershell -Command "try { $r = Invoke-RestMethod -Uri '%AGENT_VERSION_API%' -TimeoutSec 10; $r.tag_name } catch { '' }" 2^>nul') do ( + echo %%v>"%AGENT_VERSION_FILE%" + ) + ) else ( + echo [ERROR] Failed to download DualAuth Agent + echo [ERROR] Download manually: %AGENT_URL% + pause + exit /b 1 + ) +) else ( + echo [INFO] Checking for DualAuth Agent updates... + set "LOCAL_AGENT_VER=" + if exist "%AGENT_VERSION_FILE%" set /p LOCAL_AGENT_VER=<"%AGENT_VERSION_FILE%" + + set "REMOTE_AGENT_VER=" + for /f "delims=" %%v in ('powershell -Command "try { $r = Invoke-RestMethod -Uri '%AGENT_VERSION_API%' -TimeoutSec 10; $r.tag_name } catch { '' }" 2^>nul') do ( + set "REMOTE_AGENT_VER=%%v" + ) + + if defined REMOTE_AGENT_VER ( + if "!LOCAL_AGENT_VER!"=="!REMOTE_AGENT_VER!" ( + echo [INFO] DualAuth Agent up to date ^(!LOCAL_AGENT_VER!^) + ) else ( + echo [INFO] Agent update: !LOCAL_AGENT_VER! -^> !REMOTE_AGENT_VER! + curl -fL -# -o "%AGENT_JAR%.tmp" "%AGENT_URL%" --connect-timeout 15 --max-time 120 + if exist "%AGENT_JAR%.tmp" ( + move /y "%AGENT_JAR%.tmp" "%AGENT_JAR%" >nul + echo !REMOTE_AGENT_VER!>"%AGENT_VERSION_FILE%" + echo [INFO] DualAuth Agent updated + ) else ( + echo [WARN] Agent update failed, using existing + ) + ) + ) else ( + echo [INFO] Could not check agent updates, using existing + ) +) + +:: --- Final Checks --- + +if not exist "%SERVER_JAR%" ( + echo [ERROR] HytaleServer.jar not found + pause + exit /b 1 +) +if not exist "%ASSETS_PATH%" ( + echo [ERROR] Assets.zip not found + pause + exit /b 1 +) +if not exist "%AGENT_JAR%" ( + echo [ERROR] dualauth-agent.jar not found + pause + exit /b 1 +) + +:: --- Generate or Load Server ID --- + +set "SERVER_ID_FILE=.server-id" +if exist "%SERVER_ID_FILE%" ( + set /p SERVER_ID=<"%SERVER_ID_FILE%" + echo [INFO] Server ID: !SERVER_ID! +) else ( + for /f "delims=" %%i in ('powershell -Command "[guid]::NewGuid().ToString()"') do set "SERVER_ID=%%i" + echo !SERVER_ID!>"%SERVER_ID_FILE%" + echo [INFO] Generated server ID: !SERVER_ID! +) + +:: --- Fetch Server Tokens --- + +echo. +echo [INFO] Fetching server tokens from %AUTH_SERVER%... + +set "TEMP_RESPONSE=%TEMP%\hytale_auth_%RANDOM%.json" + +curl -s -X POST "%AUTH_SERVER%/server/auto-auth" ^ + -H "Content-Type: application/json" ^ + -d "{\"server_id\": \"!SERVER_ID!\", \"server_name\": \"%SERVER_NAME%\"}" ^ + --connect-timeout 10 ^ + --max-time 30 ^ + -o "%TEMP_RESPONSE%" 2>nul + +if errorlevel 1 ( + echo [ERROR] Failed to connect to auth server at %AUTH_SERVER% + del "%TEMP_RESPONSE%" 2>nul + pause + exit /b 1 +) + +findstr /C:"sessionToken" "%TEMP_RESPONSE%" >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Invalid response from auth server: + type "%TEMP_RESPONSE%" + del "%TEMP_RESPONSE%" 2>nul + pause + exit /b 1 +) + +:: Extract tokens using PowerShell +for /f "delims=" %%i in ('powershell -Command "$j = Get-Content '%TEMP_RESPONSE%' | ConvertFrom-Json; $j.sessionToken"') do set "SESSION_TOKEN=%%i" +for /f "delims=" %%i in ('powershell -Command "$j = Get-Content '%TEMP_RESPONSE%' | ConvertFrom-Json; $j.identityToken"') do set "IDENTITY_TOKEN=%%i" + +del "%TEMP_RESPONSE%" 2>nul + +if "!SESSION_TOKEN!"=="" ( + echo [ERROR] Could not extract session token from response + pause + exit /b 1 +) +if "!IDENTITY_TOKEN!"=="" ( + echo [ERROR] Could not extract identity token from response + pause + exit /b 1 +) + +echo [INFO] Tokens received successfully + +:: --- Start Server --- + +set "JAVA_ARGS=" +if defined JVM_XMS set "JAVA_ARGS=!JAVA_ARGS! -Xms%JVM_XMS%" +if defined JVM_XMX set "JAVA_ARGS=!JAVA_ARGS! -Xmx%JVM_XMX%" + +echo. +echo ============================================================ +echo Starting Hytale Server +echo Name: %SERVER_NAME% +echo Bind: %BIND_ADDRESS% +echo Java: !JAVA_CMD! +echo Agent: %AGENT_JAR% +echo ============================================================ +echo. + +"!JAVA_CMD!" %JAVA_ARGS% -javaagent:"%AGENT_JAR%" -jar "%SERVER_JAR%" ^ + --assets "%ASSETS_PATH%" ^ + --bind "%BIND_ADDRESS%" ^ + --auth-mode "%AUTH_MODE%" ^ + --disable-sentry ^ + --session-token "!SESSION_TOKEN!" ^ + --identity-token "!IDENTITY_TOKEN!" ^ + %* + +echo. +echo ============================================================ +echo Server stopped. Exit code: %ERRORLEVEL% +echo ============================================================ +pause + +endlocal diff --git a/server/start.sh b/server/start.sh new file mode 100755 index 0000000..f254cdc --- /dev/null +++ b/server/start.sh @@ -0,0 +1,516 @@ +#!/bin/bash +# ============================================================ +# Hytale F2P Dedicated Server - One-Click Starter +# ============================================================ +# Just run: ./start.sh +# +# The script will: +# 1. Look for game files in your F2P launcher install +# 2. Auto-download anything missing +# 3. Auto-update server, assets, and agent on each launch +# 4. Fetch auth tokens and start the server +# ============================================================ + +set -e + +# Configuration (edit these or set as environment variables) +HYTALE_AUTH_DOMAIN="${HYTALE_AUTH_DOMAIN:-auth.sanasol.ws}" +AUTH_SERVER="${AUTH_SERVER:-https://$HYTALE_AUTH_DOMAIN}" +SERVER_NAME="${SERVER_NAME:-My Hytale Server}" +BIND_ADDRESS="${BIND_ADDRESS:-0.0.0.0:5520}" +AUTH_MODE="${AUTH_MODE:-authenticated}" + +# Download URLs +DOWNLOAD_BASE="${DOWNLOAD_BASE:-https://download.sanasol.ws/download}" +AGENT_URL="https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar" +AGENT_VERSION_API="https://api.github.com/repos/sanasol/hytale-auth-server/releases/latest" + +# File names (in current directory) +AGENT_JAR="dualauth-agent.jar" +SERVER_JAR="HytaleServer.jar" +ASSETS_FILE="Assets.zip" +ASSETS_PATH="${ASSETS_PATH:-./Assets.zip}" +VERSION_DIR=".versions" + +echo "============================================================" +echo " Hytale F2P Dedicated Server" +echo "============================================================" +echo "" + +# --- Prerequisite Checks --- + +if ! command -v curl &>/dev/null; then + echo "[ERROR] curl is required but not found" + echo " Install: sudo apt install curl" + exit 1 +fi + +mkdir -p "$VERSION_DIR" + +# --- Find Local F2P Launcher Install --- + +get_f2p_default_dir() { + case "$(uname -s)" in + Darwin) echo "$HOME/Library/Application Support/HytaleF2P" ;; + Linux) echo "$HOME/.hytalef2p" ;; + *) return 1 ;; + esac +} + +# Read a JSON string field from config.json using available tools +read_config_field() { + local config_file="$1" field="$2" + if [ ! -f "$config_file" ]; then return 1; fi + + if command -v python3 &>/dev/null; then + python3 -c " +import json +try: + c = json.load(open('$config_file')) + v = c.get('$field', '').strip() + if v: print(v) +except: pass +" 2>/dev/null + elif command -v jq &>/dev/null; then + jq -r ".$field // empty" "$config_file" 2>/dev/null + else + grep -o "\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$config_file" 2>/dev/null | cut -d'"' -f4 + fi +} + +find_f2p_install() { + local default_app_dir + default_app_dir=$(get_f2p_default_dir) || return 1 + + local search_dirs=() + + # Check config.json for custom installPath + local config_file="$default_app_dir/config.json" + local custom_path + custom_path=$(read_config_field "$config_file" "installPath") + if [ -n "$custom_path" ]; then + local custom_f2p="$custom_path/HytaleF2P" + if [ -d "$custom_f2p" ]; then + search_dirs+=("$custom_f2p") + fi + fi + + # Always also check default location + search_dirs+=("$default_app_dir") + + for base in "${search_dirs[@]}"; do + for branch in "release" "pre-release"; do + local game_dir="$base/$branch/package/game/latest" + if [ -d "$game_dir" ]; then + echo "$game_dir" + return 0 + fi + done + done + return 1 +} + +# Find bundled JRE from F2P launcher install +find_f2p_java() { + local default_app_dir + default_app_dir=$(get_f2p_default_dir) || return 1 + + local search_dirs=() + + # Check config.json for custom javaPath first + local config_file="$default_app_dir/config.json" + local custom_java + custom_java=$(read_config_field "$config_file" "javaPath") + if [ -n "$custom_java" ] && [ -x "$custom_java" ]; then + echo "$custom_java" + return 0 + fi + + # Check custom installPath + local custom_path + custom_path=$(read_config_field "$config_file" "installPath") + if [ -n "$custom_path" ]; then + local custom_f2p="$custom_path/HytaleF2P" + [ -d "$custom_f2p" ] && search_dirs+=("$custom_f2p") + fi + + search_dirs+=("$default_app_dir") + + for base in "${search_dirs[@]}"; do + for branch in "release" "pre-release"; do + local jre_dir="$base/$branch/package/jre/latest" + # Standard path + if [ -x "$jre_dir/bin/java" ]; then + echo "$jre_dir/bin/java" + return 0 + fi + # macOS bundle path + if [ -x "$jre_dir/Contents/Home/bin/java" ]; then + echo "$jre_dir/Contents/Home/bin/java" + return 0 + fi + done + done + return 1 +} + +copy_from_f2p() { + local file="$1" local_path="$2" f2p_path="$3" + + if [ -f "$local_path" ]; then + return 1 # Already exists locally + fi + + if [ -f "$f2p_path" ]; then + echo "[INFO] Found $file in F2P launcher install" + echo "[INFO] Copying from: $f2p_path" + cp "$f2p_path" "$local_path" + echo "[INFO] Copied successfully" + return 0 + fi + return 1 +} + +# --- Detect Java --- + +JAVA_CMD="java" + +# Try F2P bundled JRE first +F2P_JAVA=$(find_f2p_java 2>/dev/null) || true +if [ -n "$F2P_JAVA" ]; then + echo "[INFO] Found Java in F2P launcher: $F2P_JAVA" + JAVA_CMD="$F2P_JAVA" +elif ! command -v java &>/dev/null; then + echo "[ERROR] Java is not installed and no F2P launcher JRE found" + echo "" + echo " Options:" + echo " 1. Install the F2P launcher first (it includes Java)" + echo " 2. Install Java 25+ manually:" + echo " Windows: https://download.oracle.com/java/25/latest/jdk-25_windows-x64_bin.exe" + echo " macOS: brew install openjdk" + echo " Ubuntu/Debian: sudo apt install openjdk-25-jre" + echo "" + exit 1 +fi + +# Check Java version +JAVA_FULL=$("$JAVA_CMD" -version 2>&1 | head -1) +JAVA_VER=$(echo "$JAVA_FULL" | grep -oP '(?<=")\d+' 2>/dev/null || echo "$JAVA_FULL" | sed 's/.*"\([0-9]*\).*/\1/') +echo "[INFO] Java: $JAVA_FULL" +if [ -n "$JAVA_VER" ] && [ "$JAVA_VER" -lt 25 ] 2>/dev/null; then + echo "[ERROR] Java $JAVA_VER detected. Java 25+ is REQUIRED." + echo " The DualAuth agent requires Java 25 (class file version 69)." + echo "" + if [ -n "$F2P_JAVA" ]; then + echo " Your F2P launcher JRE is outdated. Update the launcher to get a newer Java." + else + echo " Install Java 25+:" + echo " Windows: https://download.oracle.com/java/25/latest/jdk-25_windows-x64_bin.exe" + echo " macOS: brew install openjdk" + echo " Ubuntu/Debian: sudo apt install openjdk-25-jre" + fi + echo "" + exit 1 +fi + +# --- Find F2P Game Files --- + +F2P_DIR="" +if F2P_DIR=$(find_f2p_install 2>/dev/null); then + echo "[INFO] Found F2P launcher game files: $F2P_DIR" + + # Try to copy HytaleServer.jar from F2P install + copy_from_f2p "HytaleServer.jar" "$SERVER_JAR" "$F2P_DIR/Server/HytaleServer.jar" || true + + # Try to copy Assets.zip from F2P install + copy_from_f2p "Assets.zip" "$ASSETS_PATH" "$F2P_DIR/Assets.zip" || true + + echo "" +else + echo "[INFO] No F2P launcher install found, will download files" + echo "" +fi + +# --- Download / Update Functions --- + +get_remote_version() { + local url="$1" + local headers + headers=$(curl -sI -L "$url" --connect-timeout 10 --max-time 15 2>/dev/null | tr -d '\r') + local etag + etag=$(echo "$headers" | grep -i "^etag:" | tail -1 | sed 's/^[^:]*: *//' | tr -d '"') + if [ -n "$etag" ]; then printf '%s' "$etag"; return 0; fi + local lastmod + lastmod=$(echo "$headers" | grep -i "^last-modified:" | tail -1 | sed 's/^[^:]*: *//') + if [ -n "$lastmod" ]; then printf '%s' "$lastmod"; return 0; fi + local length + length=$(echo "$headers" | grep -i "^content-length:" | tail -1 | sed 's/^[^:]*: *//') + if [ -n "$length" ]; then printf 'size:%s' "$length"; return 0; fi + return 1 +} + +needs_update() { + local url="$1" dest="$2" name="$3" + local version_file="${VERSION_DIR}/${name}.version" + + if [ ! -f "$dest" ]; then + echo "[INFO] $name not found, will download" + return 0 + fi + + echo "[INFO] Checking for $name updates..." + local remote_version + remote_version=$(get_remote_version "$url" 2>/dev/null) || true + if [ -z "$remote_version" ]; then + echo "[INFO] Could not check for updates, using existing $name" + return 1 + fi + + local local_version="" + [ -f "$version_file" ] && local_version=$(cat "$version_file" 2>/dev/null) + + if [ "$remote_version" = "$local_version" ]; then + echo "[INFO] $name is up to date" + return 1 + fi + + if [ -n "$local_version" ]; then + echo "[INFO] $name update available" + fi + return 0 +} + +save_version() { + local url="$1" name="$2" + local version_file="${VERSION_DIR}/${name}.version" + local ver + ver=$(get_remote_version "$url" 2>/dev/null) || true + [ -n "$ver" ] && printf '%s\n' "$ver" > "$version_file" +} + +download_file() { + local url="$1" dest="$2" name="$3" expected_mb="${4:-0}" + local tmp="${dest}.tmp" + + echo "[INFO] Downloading $name..." + [ "$expected_mb" -gt 0 ] 2>/dev/null && echo "[INFO] Expected size: ~${expected_mb} MB" + + for attempt in 1 2 3; do + rm -f "$tmp" 2>/dev/null || true + + if [ "$expected_mb" -gt 50 ] 2>/dev/null; then + curl -fL --progress-bar -o "$tmp" "$url" --connect-timeout 15 --max-time 3600 2>&1 + else + curl -fL -# -o "$tmp" "$url" --connect-timeout 15 --max-time 300 2>&1 + fi + + if [ $? -eq 0 ] && [ -f "$tmp" ]; then + local size + size=$(stat -c%s "$tmp" 2>/dev/null || stat -f%z "$tmp" 2>/dev/null || echo 0) + if [ "$size" -gt 1000 ]; then + mv -f "$tmp" "$dest" + local mb=$((size / 1024 / 1024)) + echo "[INFO] $name downloaded (${mb} MB)" + return 0 + fi + echo "[WARN] $name download too small (${size} bytes), retrying..." + fi + + echo "[WARN] Download attempt $attempt failed, retrying..." + rm -f "$tmp" 2>/dev/null || true + sleep 2 + done + + echo "[ERROR] Failed to download $name after 3 attempts" + rm -f "$tmp" 2>/dev/null || true + return 1 +} + +# --- Download / Update Server Files --- + +# HytaleServer.jar +JAR_URL="${DOWNLOAD_BASE}/HytaleServer.jar" +if needs_update "$JAR_URL" "$SERVER_JAR" "HytaleServer.jar"; then + if download_file "$JAR_URL" "$SERVER_JAR" "HytaleServer.jar" "150"; then + save_version "$JAR_URL" "HytaleServer.jar" + else + if [ ! -f "$SERVER_JAR" ]; then + echo "[ERROR] HytaleServer.jar is required. Check your internet connection." + exit 1 + fi + echo "[WARN] Update failed, using existing HytaleServer.jar" + fi +fi + +# Assets.zip +ASSETS_URL="${DOWNLOAD_BASE}/Assets.zip" +if needs_update "$ASSETS_URL" "$ASSETS_PATH" "Assets.zip"; then + echo "[INFO] Assets.zip is large (~3.3 GB), this may take a while..." + if download_file "$ASSETS_URL" "$ASSETS_PATH" "Assets.zip" "3300"; then + save_version "$ASSETS_URL" "Assets.zip" + else + if [ ! -f "$ASSETS_PATH" ]; then + echo "[ERROR] Assets.zip is required. Check your internet connection." + exit 1 + fi + echo "[WARN] Update failed, using existing Assets.zip" + fi +fi + +# DualAuth Agent (uses GitHub releases API for version tracking) +check_agent_update() { + if [ -f "$AGENT_JAR" ]; then + local agent_size + agent_size=$(stat -c%s "$AGENT_JAR" 2>/dev/null || stat -f%z "$AGENT_JAR" 2>/dev/null || echo 0) + if [ "$agent_size" -lt 10000 ]; then + echo "[WARN] Agent JAR seems corrupt (${agent_size} bytes), re-downloading..." + rm -f "$AGENT_JAR" + return 0 + fi + + local version_file="${VERSION_DIR}/${AGENT_JAR}.version" + local local_version="" + [ -f "$version_file" ] && local_version=$(cat "$version_file" 2>/dev/null) + + echo "[INFO] Checking for DualAuth Agent updates..." + local remote_version + remote_version=$(curl -sf "$AGENT_VERSION_API" --connect-timeout 5 --max-time 10 2>/dev/null | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4) + + if [ -n "$remote_version" ]; then + if [ "$local_version" = "$remote_version" ]; then + echo "[INFO] DualAuth Agent up to date ($local_version)" + return 1 + else + echo "[INFO] Agent update: ${local_version:-unknown} -> $remote_version" + return 0 + fi + else + echo "[INFO] Could not check agent updates, using existing" + return 1 + fi + else + return 0 + fi +} + +AGENT_REMOTE_VERSION="" +if check_agent_update; then + echo "[INFO] Downloading DualAuth Agent..." + if curl -fL -# -o "${AGENT_JAR}.tmp" "$AGENT_URL" --connect-timeout 15 --max-time 120 2>&1 && [ -f "${AGENT_JAR}.tmp" ]; then + dl_size=$(stat -c%s "${AGENT_JAR}.tmp" 2>/dev/null || stat -f%z "${AGENT_JAR}.tmp" 2>/dev/null || echo 0) + if [ "$dl_size" -gt 10000 ]; then + mv -f "${AGENT_JAR}.tmp" "$AGENT_JAR" + AGENT_REMOTE_VERSION=$(curl -sf "$AGENT_VERSION_API" --connect-timeout 5 --max-time 10 2>/dev/null | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4) + [ -n "$AGENT_REMOTE_VERSION" ] && printf '%s\n' "$AGENT_REMOTE_VERSION" > "${VERSION_DIR}/${AGENT_JAR}.version" + echo "[INFO] DualAuth Agent ready (${AGENT_REMOTE_VERSION:-latest})" + else + echo "[WARN] Downloaded agent too small, discarding" + rm -f "${AGENT_JAR}.tmp" + fi + else + rm -f "${AGENT_JAR}.tmp" 2>/dev/null + if [ -f "$AGENT_JAR" ]; then + echo "[WARN] Agent update failed, using existing" + else + echo "[ERROR] Failed to download DualAuth Agent" + echo "[ERROR] Download manually: $AGENT_URL" + exit 1 + fi + fi +fi + +# --- Final Checks --- + +for required in "$SERVER_JAR" "$ASSETS_PATH" "$AGENT_JAR"; do + if [ ! -f "$required" ]; then + echo "[ERROR] Required file missing: $required" + exit 1 + fi +done + +# --- Generate Server ID --- + +SERVER_ID_FILE=".server-id" +if [ -f "$SERVER_ID_FILE" ]; then + SERVER_ID=$(cat "$SERVER_ID_FILE") + echo "[INFO] Server ID: $SERVER_ID" +else + SERVER_ID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null) + if [ -z "$SERVER_ID" ]; then + echo "[ERROR] Could not generate UUID. Install uuidgen or python3." + exit 1 + fi + printf '%s' "$SERVER_ID" > "$SERVER_ID_FILE" + echo "[INFO] Generated server ID: $SERVER_ID" +fi + +# --- Fetch Tokens --- + +echo "" +echo "[INFO] Fetching server tokens from $AUTH_SERVER..." + +TEMP_RESPONSE=$(mktemp) +curl -s -X POST "$AUTH_SERVER/server/auto-auth" \ + -H "Content-Type: application/json" \ + -d "{\"server_id\": \"$SERVER_ID\", \"server_name\": \"$SERVER_NAME\"}" \ + --connect-timeout 10 \ + --max-time 30 \ + -o "$TEMP_RESPONSE" + +if [ $? -ne 0 ]; then + echo "[ERROR] Failed to connect to auth server at $AUTH_SERVER" + rm -f "$TEMP_RESPONSE" + exit 1 +fi + +if ! grep -q "sessionToken" "$TEMP_RESPONSE" 2>/dev/null; then + echo "[ERROR] Invalid response from auth server:" + cat "$TEMP_RESPONSE" + rm -f "$TEMP_RESPONSE" + exit 1 +fi + +# Extract tokens (python3 > jq > grep fallback) +if command -v python3 &>/dev/null; then + SESSION_TOKEN=$(python3 -c "import json,sys; print(json.load(open('$TEMP_RESPONSE'))['sessionToken'])") + IDENTITY_TOKEN=$(python3 -c "import json,sys; print(json.load(open('$TEMP_RESPONSE'))['identityToken'])") +elif command -v jq &>/dev/null; then + SESSION_TOKEN=$(jq -r '.sessionToken' "$TEMP_RESPONSE") + IDENTITY_TOKEN=$(jq -r '.identityToken' "$TEMP_RESPONSE") +else + SESSION_TOKEN=$(grep -o '"sessionToken":"[^"]*"' "$TEMP_RESPONSE" | cut -d'"' -f4) + IDENTITY_TOKEN=$(grep -o '"identityToken":"[^"]*"' "$TEMP_RESPONSE" | cut -d'"' -f4) +fi + +rm -f "$TEMP_RESPONSE" + +if [ -z "$SESSION_TOKEN" ] || [ -z "$IDENTITY_TOKEN" ]; then + echo "[ERROR] Could not extract tokens from auth server response" + exit 1 +fi + +echo "[INFO] Tokens received successfully" + +# --- Start Server --- + +JAVA_ARGS="" +[ -n "${JVM_XMS:-}" ] && JAVA_ARGS="$JAVA_ARGS -Xms$JVM_XMS" +[ -n "${JVM_XMX:-}" ] && JAVA_ARGS="$JAVA_ARGS -Xmx$JVM_XMX" + +echo "" +echo "============================================================" +echo " Starting Hytale Server" +echo " Name: $SERVER_NAME" +echo " Bind: $BIND_ADDRESS" +echo " Agent: $AGENT_JAR" +echo "============================================================" +echo "" + +exec "$JAVA_CMD" $JAVA_ARGS -javaagent:"$AGENT_JAR" -jar "$SERVER_JAR" \ + --assets "$ASSETS_PATH" \ + --bind "$BIND_ADDRESS" \ + --auth-mode "$AUTH_MODE" \ + --disable-sentry \ + --session-token "$SESSION_TOKEN" \ + --identity-token "$IDENTITY_TOKEN" \ + "$@"