#!/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" \ "$@"