#!/bin/bash set -euo pipefail JAVA11="/usr/lib/jvm/temurin-11-jre-amd64/bin/java" JAVA17="/usr/lib/jvm/java-17-openjdk-amd64/bin/java" BUNGEE_DIR="/opt/server/bungee" BACKEND_DIR="/opt/server/backend" WORLD_DIR="$BACKEND_DIR/world" PAPER_PIPE="$BACKEND_DIR/paper_input" TOTAL_MEM_MB=$(free -m | awk '/^Mem:/{print $2}') BUNGEE_MAX_MB=192 PAPER_MAX_MB=$(( TOTAL_MEM_MB - BUNGEE_MAX_MB - 768 )) [ "$PAPER_MAX_MB" -gt 4096 ] && PAPER_MAX_MB=4096 [ "$PAPER_MAX_MB" -lt 1024 ] && PAPER_MAX_MB=1024 PAPER_MIN_MB=1024 CPU_CORES=$(nproc 2>/dev/null || echo 2) GC_THREADS=$CPU_CORES CONC_GC=$(( CPU_CORES / 2 )) [ "$CONC_GC" -lt 1 ] && CONC_GC=1 NETTY_THREADS=$CPU_CORES echo "============================================" echo " Eaglercraft 1.8.8 — Vanilla" echo " Mem: ${PAPER_MIN_MB}-${PAPER_MAX_MB}MB | CPUs: ${CPU_CORES}" echo " Setup UI: port 7860 (only if no world)" echo "============================================" PAPER_JVM_FLAGS=( -Xmx${PAPER_MAX_MB}M -Xms${PAPER_MIN_MB}M -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=15 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:G1NewSizePercent=40 -XX:G1MaxNewSizePercent=50 -XX:G1HeapRegionSize=16M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=50 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -XX:+OptimizeStringConcat -XX:+UseCompressedOops -XX:+AlwaysPreTouch -XX:ParallelGCThreads=${GC_THREADS} -XX:ConcGCThreads=${CONC_GC} -XX:MaxMetaspaceSize=256M -XX:CompressedClassSpaceSize=128M -XX:ReservedCodeCacheSize=128M -XX:-UseCodeCacheFlushing -Xss1m -Dio.netty.allocator.maxCachedBufferCapacity=524288 -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.eventLoopThreads=${NETTY_THREADS} -Dio.netty.allocator.numDirectArenas=${NETTY_THREADS} -Dio.netty.allocator.numHeapArenas=${NETTY_THREADS} -Dcom.mojang.eula.agree=true -DIReallyKnowWhatIAmDoingISwear -Dspigot.disable-firewall-check=true -Djava.net.preferIPv4Stack=true ) BUNGEE_JVM_FLAGS=( -Xmx${BUNGEE_MAX_MB}M -Xms128M -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=30 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem -XX:+OptimizeStringConcat -XX:+UseCompressedOops -XX:MaxMetaspaceSize=128M -XX:ReservedCodeCacheSize=64M -Xss1m -XX:ParallelGCThreads=${GC_THREADS} -XX:ConcGCThreads=${CONC_GC} -Dio.netty.allocator.maxCachedBufferCapacity=524288 -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.eventLoopThreads=${NETTY_THREADS} -Dio.netty.allocator.numDirectArenas=${NETTY_THREADS} -Dio.netty.allocator.numHeapArenas=${NETTY_THREADS} -Djava.net.preferIPv4Stack=true ) PAPER_PID="" BUNGEE_PID="" WEB_PID="" NGINX_PID="" echo "127.0.0.1 checkip.amazonaws.com" >> /etc/hosts echo "127.0.0.1 api.ipify.org" >> /etc/hosts echo "127.0.0.1 icanhazip.com" >> /etc/hosts echo "127.0.0.1 portcheck.twizmwazin.com" >> /etc/hosts cleanup() { echo "" echo "[SHUTDOWN] Saving world to HuggingFace..." python3 /opt/server/sync_world.py upload 2>/dev/null || true echo "[SHUTDOWN] Stopping WindSpigot..." if [ -p "$PAPER_PIPE" ]; then echo "stop" >&3 2>/dev/null || true fi sleep 5 [ -n "$PAPER_PID" ] && kill "$PAPER_PID" 2>/dev/null || true [ -n "$BUNGEE_PID" ] && kill "$BUNGEE_PID" 2>/dev/null || true [ -n "$WEB_PID" ] && kill "$WEB_PID" 2>/dev/null || true [ -n "$NGINX_PID" ] && kill "$NGINX_PID" 2>/dev/null || true exec 3>&- 2>/dev/null || true wait 2>/dev/null || true } trap cleanup SIGTERM SIGINT EXIT echo "[0/7] Starting nginx (port 7860) and web panel (port 5000)..." nginx -g "daemon off;" >> /tmp/nginx_error.log 2>&1 & NGINX_PID=$! python3 /opt/server/web.py >> /tmp/web.log 2>&1 & WEB_PID=$! echo " nginx PID=$NGINX_PID web PID=$WEB_PID" sleep 1 echo "[1/7] Downloading world from HuggingFace..." python3 /opt/server/sync_world.py download 2>&1 WORLD_OK=$? if [ "$WORLD_OK" -ne 0 ] || [ ! -d "$WORLD_DIR" ]; then echo "" echo "╔══════════════════════════════════════════╗" echo "║ NO WORLD FOUND — Setup UI is active ║" echo "║ Open port 7860 and upload world.zip ║" echo "╚══════════════════════════════════════════╝" touch /tmp/setup_mode wait "$WEB_PID" exit 0 fi rm -f /tmp/setup_mode echo "[2/7] Generating server icon..." if command -v convert &>/dev/null; then if [ -f /etc/ImageMagick-6/policy.xml ]; then sed -i 's/rights="none" pattern="PNG"/rights="read|write" pattern="PNG"/' \ /etc/ImageMagick-6/policy.xml fi convert -size 64x64 xc:"#1a1a2e" \ \( -size 64x64 gradient:"#1a1a2e"-"#16213e" \) -composite \ -fill "#fffde7" -draw "circle 52,10 58,10" \ -fill "#16213e" -draw "circle 55,8 61,8" \ -fill "#ffffff" \ -draw "point 10,5" -draw "point 25,3" \ -draw "point 40,8" -draw "point 15,15" \ -draw "point 35,12" -draw "point 5,20" \ -fill "#4caf50" -draw "rectangle 0,48 63,63" \ -fill none -stroke "#000000" -strokewidth 1 \ -draw "rectangle 0,0 63,63" \ PNG:"$BACKEND_DIR/server-icon.png" \ && cp "$BACKEND_DIR/server-icon.png" "$BUNGEE_DIR/server-icon.png" \ && echo " Icon generated." \ || echo " Warning: icon generation failed." else echo " Warning: ImageMagick not found." fi echo "[3/7] Writing backend config..." cd "$BACKEND_DIR" echo "eula=true" > eula.txt cat > server.properties << 'EOF' server-port=25565 server-ip=127.0.0.1 online-mode=false spawn-protection=0 max-players=50 view-distance=8 motd=\u00A7aEaglercraft 1.8.8 pvp=true allow-flight=false level-name=world gamemode=2 default-player-permission-level=visitor difficulty=1 use-native-transport=true network-compression-threshold=-1 max-tick-time=-1 generate-structures=true allow-nether=true spawn-animals=true spawn-monsters=true prevent-proxy-connections=false enable-query=false enable-rcon=false EOF cat > spigot.yml << 'EOF' config-version: 8 settings: bungeecord: true timeout-time: 60 netty-threads: 2 async-catcher-enabled: false save-user-cache-on-stop-only: true check-firewall: false commands: tab-complete: 0 log: false world-settings: default: verbose: false view-distance: 8 mob-spawn-range: 4 ticks-per: hopper-transfer: 8 hopper-check: 1 arrow-despawn-rate: 300 item-despawn-rate: 6000 EOF cat > bukkit.yml << 'EOF' settings: connection-throttle: -1 allow-end: true spawn-npcs: true spawn-animals: true spawn-monsters: true chunk-gc: period-in-ticks: 600 ticks-per: animal-spawns: 400 monster-spawns: 1 autosave: 6000 spawn: location: world: world x: 0.0 y: 64.0 z: 0.0 yaw: 0.0 pitch: 0.0 EOF python3 -c " import uuid, json players = [ {'name': 'Xx_N01i_xX', 'level': 4}, {'name': 'EXCUSE_ME_SIR', 'level': 4} ] ops = [] for p in players: # Offline-mode UUID = UUID v3 of 'OfflinePlayer:' offline_uuid = uuid.uuid3(uuid.NAMESPACE_URL, 'OfflinePlayer:' + p['name']) # Minecraft uses UUID v3 but with a specific namespace # Actually the correct method: import hashlib h = hashlib.md5(('OfflinePlayer:' + p['name']).encode('utf-8')).digest() b = bytearray(h) b[6] = (b[6] & 0x0f) | 0x30 # version 3 b[8] = (b[8] & 0x3f) | 0x80 # variant 1 u = uuid.UUID(bytes=bytes(b)) ops.append({ 'uuid': str(u), 'name': p['name'], 'level': p['level'], 'bypassesPlayerLimit': False }) print(f\" {p['name']} -> {u}\") with open('ops.json', 'w') as f: json.dump(ops, f, indent=2) print(' ops.json written!') " 2>&1 echo "[4/7] Writing BungeeCord config..." cd "$BUNGEE_DIR" cat > config.yml << 'EOF' server_connect_timeout: 5000 player_limit: 50 permissions: default: - bungeecord.command.server - bungeecord.command.list timeout: 30000 online_mode: false throttle: 4000 throttle_limit: 3 servers: main: motd: '&aEaglercraft 1.8.8' address: 127.0.0.1:25565 restricted: false listeners: - query_port: 25567 motd: '&aEaglercraft 1.8.8' tab_list: GLOBAL_PING query_enabled: false proxy_protocol: false priorities: - main bind_local_address: true host: 127.0.0.1:25567 max_players: 50 force_default_server: true ip_forward: true connection_throttle: -1 stats: none log_commands: false log_pings: false network_compression_threshold: -1 EOF echo "[5/7] Generating EaglerXServer config..." rm -rf \ "$BUNGEE_DIR/plugins/EaglercraftXServer" \ "$BUNGEE_DIR/plugins/EaglercraftXBungee" \ "$BUNGEE_DIR/plugins/EaglerXServer" > /tmp/bungee-gen.log timeout 20s $JAVA17 "${BUNGEE_JVM_FLAGS[@]}" \ -cp "sqlite-jdbc.jar:BungeeCord.jar" net.md_5.bungee.Bootstrap \ >> /tmp/bungee-gen.log 2>&1 || true pkill -f 'net.md_5.bungee.Bootstrap' 2>/dev/null || true sleep 3 EAGLER_CFG="$(find "$BUNGEE_DIR/plugins" -maxdepth 2 -name listeners.yml | head -n1 || true)" if [ -z "$EAGLER_CFG" ]; then echo " ERROR: EaglerXServer did not generate listeners.yml" echo " Last 120 lines of bungee-gen.log:" tail -120 /tmp/bungee-gen.log echo " Files under plugins/:" find "$BUNGEE_DIR/plugins" -maxdepth 2 -type f | sort exit 1 fi sed -i 's|inject_address:.*|inject_address: "127.0.0.1:25567"|' "$EAGLER_CFG" || true echo " Using Eagler config: $EAGLER_CFG" echo "[6/7] Starting WindSpigot (backend)..." cd "$BACKEND_DIR" rm -f "$PAPER_PIPE" mkfifo "$PAPER_PIPE" > /tmp/paper.log $JAVA11 "${PAPER_JVM_FLAGS[@]}" \ -jar server.jar nogui \ < "$PAPER_PIPE" >> /tmp/paper.log 2>&1 & PAPER_PID=$! sleep 1 exec 3>"$PAPER_PIPE" echo " WindSpigot PID: $PAPER_PID" echo " Waiting for WindSpigot to be ready (up to 120s)..." READY=0 for i in $(seq 1 120); do if grep -q "Done" /tmp/paper.log 2>/dev/null; then READY=1 echo " WindSpigot READY! (${i}s)" break fi if ! kill -0 "$PAPER_PID" 2>/dev/null; then echo " ERROR: WindSpigot died early" tail -60 /tmp/paper.log exit 1 fi [ $(( i % 20 )) -eq 0 ] && echo " Still waiting... (${i}s)" && tail -5 /tmp/paper.log sleep 1 done if [ "$READY" -eq 0 ]; then echo " WARNING: WindSpigot did not print Done within 120s" tail -20 /tmp/paper.log fi sleep 2 echo "[7/7] Starting BungeeCord + EaglerXServer..." cd "$BUNGEE_DIR" > /tmp/bungee.log $JAVA17 "${BUNGEE_JVM_FLAGS[@]}" \ -cp "sqlite-jdbc.jar:BungeeCord.jar" net.md_5.bungee.Bootstrap \ >> /tmp/bungee.log 2>&1 & BUNGEE_PID=$! echo " BungeeCord PID: $BUNGEE_PID" echo " Waiting for Bungee/Eagler listener on 25567 (up to 30s)..." WS_READY=0 for i in $(seq 1 30); do if nc -z 127.0.0.1 25567 2>/dev/null; then WS_READY=1 echo " Eagler listener READY! (${i}s)" break fi if ! kill -0 "$BUNGEE_PID" 2>/dev/null; then echo " ERROR: BungeeCord died early" tail -80 /tmp/bungee.log exit 1 fi sleep 1 done if [ "$WS_READY" -eq 0 ]; then echo " ERROR: 25567 never opened" echo " Last 120 lines of bungee.log:" tail -120 /tmp/bungee.log exit 1 fi echo "" echo "============================================" echo " SERVER STATUS" echo "============================================" echo " WindSpigot :25565 $(nc -z 127.0.0.1 25565 2>/dev/null && echo '✓ ONLINE' || echo '✗ OFFLINE')" echo " Eagler/Bungee:25567 $(nc -z 127.0.0.1 25567 2>/dev/null && echo '✓ ONLINE' || echo '✗ OFFLINE')" echo " Nginx :7860 $(nc -z 127.0.0.1 7860 2>/dev/null && echo '✓ ONLINE' || echo '✗ OFFLINE')" echo " Flask :5000 $(nc -z 127.0.0.1 5000 2>/dev/null && echo '✓ ONLINE' || echo '✗ OFFLINE')" echo "============================================" echo " JOIN URL: wss://YOUR-ACTUAL-SPACE.hf.space/ws/" echo "============================================" echo "" LAST_LOG_LINE=0 LAST_SAVE_TIME=$(date +%s) SAVE_INTERVAL=300 LOOP_COUNT=0 while true; do LOOP_COUNT=$(( LOOP_COUNT + 1 )) sleep 10 if [ $(( LOOP_COUNT % 6 )) -eq 0 ]; then CUR=$(wc -l < /tmp/paper.log 2>/dev/null || echo 0) if [ "$CUR" -gt "$LAST_LOG_LINE" ]; then ERR=$(tail -n +"$(( LAST_LOG_LINE + 1 ))" /tmp/paper.log | grep -c "ERROR\|SEVERE" 2>/dev/null || echo 0) [ "$ERR" -gt 0 ] && echo "[$(date '+%H:%M:%S')] $ERR new error(s) in paper.log" LAST_LOG_LINE=$CUR fi fi NOW=$(date +%s) if [ $(( NOW - LAST_SAVE_TIME )) -ge "$SAVE_INTERVAL" ]; then echo "[$(date '+%H:%M:%S')] Autosaving world to HuggingFace..." echo "save-all" >&3 2>/dev/null || true sleep 3 python3 /opt/server/sync_world.py upload 2>&1 & LAST_SAVE_TIME=$NOW fi done