#!/bin/bash JAVA_HOME_DIR=$(find /usr/lib/jvm -maxdepth 1 -name "java-17-openjdk-*" -type d 2>/dev/null | head -1) if [ -z "$JAVA_HOME_DIR" ]; then echo "ERROR: Java 17 not found!" exit 1 fi JAVA="$JAVA_HOME_DIR/bin/java" BUNGEE_DIR="/opt/server/bungee" BACKEND_DIR="/opt/server/backend" PLUGIN_DIR="$BACKEND_DIR/plugins" mkdir -p "$PLUGIN_DIR" HF_BUCKET_HANDLE="hf://buckets/smodusermc/mc" SAVE_DIRS="world world_nether world_the_end players banned-ips.json banned-players.json ops.json whitelist.json plugins" SYNC_INTERVAL="${SYNC_INTERVAL:-300}" IDLE_MODE=false # ============================================= # OP ACCOUNT # ============================================= OP_USERNAME="CreppyBitch" CPU_CORES=$(nproc 2>/dev/null || echo 2) NETTY_THREADS=4 TOTAL_MEM_MB=$(free -m | awk '/^Mem:/{print $2}') BUNGEE_MAX_MB=512 PAPER_MAX_MB=$(( TOTAL_MEM_MB - BUNGEE_MAX_MB - 512 )) [ "$PAPER_MAX_MB" -gt 6144 ] && PAPER_MAX_MB=6144 [ "$PAPER_MAX_MB" -lt 1024 ] && PAPER_MAX_MB=1024 # FIX: Min should be less than or equal to max PAPER_MIN_MB=$(( PAPER_MAX_MB / 2 )) [ "$PAPER_MIN_MB" -lt 512 ] && PAPER_MIN_MB=512 echo "========================================" echo " Eaglercraft 1.8.8 Survival Server" echo " WindSpigot + HuggingFace Buckets" echo "========================================" echo "" echo " CPUs: $CPU_CORES | RAM: ${TOTAL_MEM_MB}MB" echo " Server: ${PAPER_MIN_MB}-${PAPER_MAX_MB}MB | Bungee: ${BUNGEE_MAX_MB}MB" echo " Java: $($JAVA -version 2>&1 | head -1)" echo " Bucket: $HF_BUCKET_HANDLE" [ -n "$OP_USERNAME" ] && echo " OP Account: $OP_USERNAME" echo "" # ============================================= # JVM FLAGS - Optimized for limited resources # ============================================= PAPER_JVM_FLAGS=( -Xmx${PAPER_MAX_MB}M -Xms${PAPER_MIN_MB}M -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=50 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -XX:+OptimizeStringConcat -XX:+UseCompressedOops -XX:MaxMetaspaceSize=192M -XX:CompressedClassSpaceSize=96M -XX:ReservedCodeCacheSize=96M -Xss256k -Djline.terminal=jline.UnsupportedTerminal -Dio.netty.allocator.maxCachedBufferCapacity=262144 -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.eventLoopThreads=${NETTY_THREADS} -Dcom.mojang.eula.agree=true -DIReallyKnowWhatIAmDoingISwear ) BUNGEE_JVM_FLAGS=( -Xmx${BUNGEE_MAX_MB}M -Xms128M -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=50 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem -XX:+OptimizeStringConcat -XX:+UseCompressedOops -XX:MaxMetaspaceSize=96M -Xss256k -Dio.netty.allocator.maxCachedBufferCapacity=262144 -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.eventLoopThreads=${NETTY_THREADS} -Deaglerxbungee.stfu=true ) # ============================================================= # RCON # ============================================================= RCON_PASS="chunkystart" get_player_count() { local RESULT RESULT=$(mcrcon -H 127.0.0.1 -P 25575 -p "$RCON_PASS" "list" 2>/dev/null) echo "$RESULT" | grep -oE 'are [0-9]+' | grep -oE '[0-9]+' || echo "0" } mc_command() { for cmd in "$@"; do mcrcon -H 127.0.0.1 -P 25575 -p "$RCON_PASS" "$cmd" 2>/dev/null done } # ============================================================= # WindSpigot starter # ============================================================= start_windspigot() { cd "$BACKEND_DIR" $JAVA "${PAPER_JVM_FLAGS[@]}" -jar server.jar nogui --noconsole >> /tmp/paper.log 2>&1 & BACKEND_PID=$! } # ============================================================= # OP Account Setup # ============================================================= setup_op_account() { if [ -z "$OP_USERNAME" ]; then return fi echo " Setting up OP for: $OP_USERNAME" local OFFLINE_UUID OFFLINE_UUID=$(echo -n "OfflinePlayer:${OP_USERNAME}" | md5sum | sed 's/\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)/\1\2\3\4-\5\6-\7\8-\9\10-\11\12\13\14\15\16/') local V3_UUID V3_UUID=$(echo "$OFFLINE_UUID" | sed 's/.\{1\}\(.\{3\}-\)/3\1/' | sed 's/\(.\{14\}-\).\(.\{3\}-\)/\1'"$(echo "$OFFLINE_UUID" | cut -c15 | tr '0-9a-f' '89ab89ab89ab89ab')"'\2/') cat > "$BACKEND_DIR/ops.json" << OPEOF [ { "uuid": "${V3_UUID}", "name": "${OP_USERNAME}", "level": 4, "bypassesPlayerLimit": true } ] OPEOF echo " ops.json written (level 4, UUID: ${V3_UUID})" } # ============================================================= # IDLE MODE - Reduced tick rate but keeps mob spawning logic # ============================================================= enter_idle_mode() { [ "$IDLE_MODE" = true ] && return IDLE_MODE=true # Only reduce tick speed, don't disable spawning entirely mc_command "gamerule randomTickSpeed 1" # Clear excess hostile mobs to reduce lag when idle mc_command "kill @e[type=Zombie,r=200]" mc_command "kill @e[type=Skeleton,r=200]" mc_command "kill @e[type=Spider,r=200]" mc_command "kill @e[type=Creeper,r=200]" echo "[IDLE] Low-power mode — reduced ticks, cleared distant mobs" } exit_idle_mode() { [ "$IDLE_MODE" = false ] && return IDLE_MODE=false mc_command "gamerule randomTickSpeed 2" echo "[IDLE] Normal gameplay restored" } # ============================================================= # Port fix # ============================================================= find_listeners_yml() { find "$BUNGEE_DIR/plugins" -name "listeners.yml" -type f 2>/dev/null | head -1 } patch_eagler_port() { local FILE=$(find_listeners_yml) [ -z "$FILE" ] && return 1 grep -q ":7860" "$FILE" && return 0 sed -i 's/\(address:[[:space:]]*"[^:]*:\)[0-9]*/\17860/' "$FILE" sed -i "s/\(address:[[:space:]]*[^\"][^:]*:\)[0-9]*/\17860/" "$FILE" echo " Port -> 7860" } start_bungee() { cd "$BUNGEE_DIR" $JAVA "${BUNGEE_JVM_FLAGS[@]}" \ -cp "sqlite-jdbc.jar:BungeeCord.jar" \ net.md_5.bungee.Bootstrap >> /tmp/bungee.log 2>&1 & BUNGEE_PID=$! } # ============================================================= # HuggingFace Bucket # ============================================================= hf_authenticate() { if [ -n "$HF_TOKEN" ]; then hf auth login --token "$HF_TOKEN" --add-to-git-credential 2>/dev/null || true echo " Authenticated" else echo " No HF_TOKEN" fi } hf_ensure_bucket() { local BUCKET_ID=$(echo "$HF_BUCKET_HANDLE" | sed 's|hf://buckets/||') hf buckets create "$BUCKET_ID" --exist-ok 2>/dev/null } hf_restore_saves() { echo " Restoring game data..." hf buckets sync "${HF_BUCKET_HANDLE}/game-data" "$BACKEND_DIR" 2>&1 | tail -5 for dir in $SAVE_DIRS; do [ -e "$BACKEND_DIR/$dir" ] && echo " Found: $dir" done } hf_push_saves() { local STAGING="/tmp/hf-staging" rm -rf "$STAGING" && mkdir -p "$STAGING" for item in $SAVE_DIRS; do if [ -e "$BACKEND_DIR/$item" ]; then mkdir -p "$STAGING/$(dirname "$item")" cp -a "$BACKEND_DIR/$item" "$STAGING/$item" fi done hf buckets sync "$STAGING" "${HF_BUCKET_HANDLE}/game-data" --delete 2>&1 | tail -3 [ $? -eq 0 ] && echo "[SYNC] OK $(date '+%H:%M:%S')" \ || echo "[SYNC] FAIL $(date '+%H:%M:%S')" rm -rf "$STAGING" } hf_sync_loop() { while true; do sleep "$SYNC_INTERVAL" hf_push_saves done } # ============================================================= # STEP 0: Bucket # ============================================================= echo "[0/7] Bucket setup..." hf_authenticate hf_ensure_bucket hf_restore_saves echo "" # ============================================================= # STEP 1: World size # ============================================================= echo "[1/7] World analysis..." for WORLD_DIR in world world_nether world_the_end; do if [ -d "$BACKEND_DIR/$WORLD_DIR" ]; then SIZE=$(du -sh "$BACKEND_DIR/$WORLD_DIR" 2>/dev/null | awk '{print $1}') REGIONS=$(find "$BACKEND_DIR/$WORLD_DIR" -name "*.mca" 2>/dev/null | wc -l) echo " $WORLD_DIR: $SIZE ($REGIONS region files)" fi done echo "" # ============================================================= # STEP 2: Core server configs + Start WindSpigot # ============================================================= cd "$BACKEND_DIR" echo "eula=true" > eula.txt echo "[2/7] Writing core server configs + starting WindSpigot..." # FIX: Enable mobs, use DEFAULT world type for survival cat > server.properties << 'EOF' server-port=25565 server-ip=127.0.0.1 online-mode=false spawn-protection=0 max-players=20 view-distance=6 gamemode=0 difficulty=2 level-name=world level-type=DEFAULT level-seed= generate-structures=true motd=Survival Eaglercraft pvp=true allow-flight=false white-list=false spawn-npcs=true spawn-animals=true spawn-monsters=true enable-command-block=false allow-nether=true use-native-transport=true network-compression-threshold=256 entity-broadcast-range-percentage=75 max-tick-time=60000 enable-rcon=true rcon.port=25575 rcon.password=chunkystart EOF # FIX: Balanced mob spawning - not too many, not too few cat > bukkit.yml << 'EOF' settings: allow-end: true warn-on-overload: true connection-throttle: -1 shutdown-message: Server closed save-user-cache-on-stop-only: true spawn-limits: monsters: 15 animals: 20 water-animals: 3 ambient: 3 chunk-gc: period-in-ticks: 400 ticks-per: animal-spawns: 400 monster-spawns: 2 autosave: 6000 EOF # FIX: Better entity activation ranges for survival gameplay 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 moved-wrongly-threshold: 0.0625 moved-too-quickly-multiplier: 10.0 item-dirty-ticks: 20 player-shuffle: 0 commands: tab-complete: 0 log: false world-settings: default: verbose: false view-distance: 6 mob-spawn-range: 4 entity-activation-range: animals: 24 monsters: 32 raiders: 48 misc: 8 tick-inactive-villagers: true entity-tracking-range: players: 64 animals: 48 monsters: 48 misc: 32 other: 64 ticks-per: hopper-transfer: 8 hopper-check: 1 hopper-amount: 1 max-entity-collisions: 4 merge-radius: exp: 4.0 item: 3.5 arrow-despawn-rate: 300 item-despawn-rate: 6000 nerf-spawner-mobs: false zombie-aggressive-towards-villager: true enable-zombie-pigmen-portal-spawns: true max-tnt-per-tick: 20 EOF cat > windspigot.yml << 'EOF' settings: async: entity-tracking: true path-searching: true tnt-explosions: true lighting: true tnt: optimize-movement: true optimize-liquid-explosions: true fast-operators: true stop-decoding-itemstack-on-place: true modern-tick-loop: enabled: false tcp-fast-open: true reduced-chunk-loads: true lag-compensated-potions: false anti-xray: enabled: false hit-detection: enabled: true threshold: 1.0 combat: knockback: friction: 2.0 horizontal: 0.35 vertical: 0.35 vertical-limit: 0.4 extra-horizontal: 0.425 extra-vertical: 0.085 chunk: threads: 2 players-per-thread: 7 EOF cat > nacho.yml << 'EOF' settings: stop-notify-bungee: true anti-malware: true use-tcp-nodelay: true brand-name: "Survival" EOF setup_op_account > /tmp/paper.log start_windspigot echo " WindSpigot PID: $BACKEND_PID" for i in $(seq 1 120); do if grep -q "Done" /tmp/paper.log 2>/dev/null; then echo " WindSpigot READY (~${i}s)" break fi if ! kill -0 $BACKEND_PID 2>/dev/null; then echo " WINDSPIGOT CRASHED!" tail -30 /tmp/paper.log exit 1 fi [ $((i % 15)) -eq 0 ] && echo " Loading... (${i}s)" sleep 1 done for i in $(seq 1 30); do nc -z 127.0.0.1 25575 2>/dev/null && break sleep 1 done if [ -n "$OP_USERNAME" ]; then mc_command "op ${OP_USERNAME}" echo " OP granted to ${OP_USERNAME} via RCON" fi echo "" echo " === PLUGINS LOADED ===" grep -i "Enabling" /tmp/paper.log | grep -oP "Enabling \K[^\s]+" 2>/dev/null | while read p; do echo " - $p" done echo " ======================" echo "" # ============================================================= # STEP 3: Survival gamerules # ============================================================= echo "[3/7] Setting Survival gamerules..." mc_command "gamerule pvp true" mc_command "gamerule keepInventory false" mc_command "gamerule naturalRegeneration true" mc_command "gamerule doFireTick true" mc_command "gamerule mobGriefing false" mc_command "gamerule doMobSpawning true" mc_command "gamerule doDaylightCycle true" mc_command "gamerule doWeatherCycle true" mc_command "gamerule randomTickSpeed 2" mc_command "gamerule spawnRadius 10" mc_command "gamerule maxEntityCramming 12" mc_command "difficulty 1" mc_command "defaultgamemode survival" echo " Survival gamerules set" echo "" # ============================================================= # STEP 4: Initial idle mode if no players # ============================================================= echo "[4/7] Checking for players..." PLAYER_COUNT=$(get_player_count) if [ "$PLAYER_COUNT" = "0" ]; then enter_idle_mode else echo " Players online: $PLAYER_COUNT" fi echo "" # ============================================================= # STEP 5: Write BungeeCord config # ============================================================= echo "[5/7] Writing BungeeCord config..." cd "$BUNGEE_DIR" cat > config.yml << 'EOF' server_connect_timeout: 5000 remote_ping_cache: -1 forge_support: false player_limit: 20 permissions: default: - bungeecord.command.server admin: - bungeecord.command.alert timeout: 30000 log_commands: false network_compression_threshold: 256 online_mode: false disabled_commands: - disabledcommandhere servers: lobby: motd: '&aEaglercraft Survival' address: 127.0.0.1:25565 restricted: false listeners: - query_port: 25577 motd: '&6Eaglercraft 1.8.8 Survival' tab_list: GLOBAL_PING query_enabled: false proxy_protocol: false forced_hosts: {} ping_passthrough: false priorities: - lobby bind_local_address: true host: 127.0.0.1:25577 max_players: 20 tab_size: 60 force_default_server: true ip_forward: true remote_ping_timeout: 5000 prevent_proxy_connections: false groups: default: - default connection_throttle: -1 connection_throttle_limit: 0 stats: none log_pings: false EOF echo " BungeeCord config.yml written" echo "" # ============================================================= # STEP 6: EaglerXBungee generation + Start BungeeCord # ============================================================= LISTENERS_FILE=$(find_listeners_yml) if [ -z "$LISTENERS_FILE" ]; then echo "[6/7] Generating EaglerXBungee config..." cd "$BUNGEE_DIR" $JAVA "${BUNGEE_JVM_FLAGS[@]}" \ -cp "sqlite-jdbc.jar:BungeeCord.jar" \ net.md_5.bungee.Bootstrap >> /tmp/bungee-gen.log 2>&1 & GEN_PID=$! for i in $(seq 1 60); do if nc -z 127.0.0.1 8081 2>/dev/null || nc -z 127.0.0.1 7860 2>/dev/null; then echo " EaglerXBungee started (~$((i*2))s)" break fi if ! kill -0 $GEN_PID 2>/dev/null; then echo " Generation failed" tail -20 /tmp/bungee-gen.log break fi sleep 2 done sleep 3 kill $GEN_PID 2>/dev/null wait $GEN_PID 2>/dev/null for i in $(seq 1 15); do nc -z 127.0.0.1 8081 2>/dev/null || break sleep 1 done sleep 2 else echo "[6/7] EaglerXBungee config exists" fi echo " Starting BungeeCord..." patch_eagler_port # MOTD Patch LISTENERS_NOW=$(find_listeners_yml) if [ -n "$LISTENERS_NOW" ]; then sed -i 's/An EaglercraftX server/\&a\&lSurvival Server/g' "$LISTENERS_NOW" fi EAGLER_DIR=$(dirname "$(find_listeners_yml)" 2>/dev/null) if [ -n "$EAGLER_DIR" ]; then mkdir -p "$EAGLER_DIR/drivers" cp -f "$BUNGEE_DIR/sqlite-jdbc.jar" "$EAGLER_DIR/drivers/sqlite-jdbc.jar" 2>/dev/null fi > /tmp/bungee.log start_bungee echo " BungeeCord PID: $BUNGEE_PID" PORT_READY=false for i in $(seq 1 45); do if nc -z 127.0.0.1 7860 2>/dev/null; then PORT_READY=true echo " Port 7860 OPEN (~$((i*2))s)" break fi if ! kill -0 $BUNGEE_PID 2>/dev/null; then echo " BungeeCord crashed!" tail -20 /tmp/bungee.log break fi sleep 2 done if [ "$PORT_READY" = true ]; then echo "" echo "============================================" echo " SURVIVAL SERVER READY — EaglerCraft :7860" [ -n "$OP_USERNAME" ] && echo " OP: $OP_USERNAME (level 4)" echo "============================================" else echo " Port 7860 NOT open!" for port in 7860 8081 25565 25577; do nc -z 127.0.0.1 $port 2>/dev/null && echo " OK $port" || echo " FAIL $port" done LISTENERS_NOW=$(find_listeners_yml) if [ -n "$LISTENERS_NOW" ] && grep -q ":8081" "$LISTENERS_NOW"; then kill $BUNGEE_PID 2>/dev/null wait $BUNGEE_PID 2>/dev/null sleep 3 patch_eagler_port start_bungee sleep 20 nc -z 127.0.0.1 7860 2>/dev/null && echo " Port 7860 open!" || echo " Failed" fi fi # ============================================================= # STEP 7: Final confirmation # ============================================================= echo "" echo "[7/7] Final status check..." echo " === ACTIVE PLUGINS ===" RELOAD_CHECK=$(mc_command "plugins") echo " $RELOAD_CHECK" echo " ======================" echo "" # ============================================================= # Sync loop # ============================================================= hf_sync_loop & SYNC_PID=$! # ============================================================= # Shutdown # ============================================================= graceful_shutdown() { echo " Shutting down..." mc_command "gamerule randomTickSpeed 3" mc_command "save-all" sleep 5 hf_push_saves kill $SYNC_PID 2>/dev/null mc_command "stop" sleep 5 kill $BUNGEE_PID 2>/dev/null kill -0 $BACKEND_PID 2>/dev/null && kill $BACKEND_PID 2>/dev/null exit 0 } trap graceful_shutdown SIGTERM SIGINT SIGHUP # ============================================================= # Monitor loop # ============================================================= echo "" echo "Monitor loop started..." LAST_LOG_LINE=$(wc -l < /tmp/paper.log 2>/dev/null || echo 0) LOOP_COUNT=0 while true; do LOOP_COUNT=$((LOOP_COUNT + 1)) if ! kill -0 $BACKEND_PID 2>/dev/null; then echo "[$(date '+%H:%M:%S')] WindSpigot crashed — restarting..." hf_push_saves IDLE_MODE=false start_windspigot sleep 45 for i in $(seq 1 30); do nc -z 127.0.0.1 25575 2>/dev/null && break sleep 1 done if [ -n "$OP_USERNAME" ]; then mc_command "op ${OP_USERNAME}" fi mc_command "difficulty 1" mc_command "gamerule doMobSpawning true" mc_command "gamerule randomTickSpeed 2" fi if ! kill -0 $BUNGEE_PID 2>/dev/null; then echo "[$(date '+%H:%M:%S')] BungeeCord crashed — restarting..." patch_eagler_port start_bungee fi if ! kill -0 $SYNC_PID 2>/dev/null; then hf_sync_loop & SYNC_PID=$! fi if kill -0 $BACKEND_PID 2>/dev/null; then PLAYER_COUNT=$(get_player_count) if [ "$PLAYER_COUNT" != "0" ] && [ "$IDLE_MODE" = true ]; then exit_idle_mode elif [ "$PLAYER_COUNT" = "0" ] && [ "$IDLE_MODE" = false ]; then enter_idle_mode fi fi # Log monitoring if [ $((LOOP_COUNT % 5)) -eq 0 ]; then CURRENT_LINE=$(wc -l < /tmp/paper.log 2>/dev/null || echo 0) if [ "$CURRENT_LINE" -gt "$LAST_LOG_LINE" ]; then NEW_ERRORS=$(tail -n +"$((LAST_LOG_LINE + 1))" /tmp/paper.log | grep -c "ERROR\|SEVERE" || echo 0) [ "$NEW_ERRORS" -gt 0 ] && echo "[$(date '+%H:%M:%S')] $NEW_ERRORS errors" && \ tail -n +"$((LAST_LOG_LINE + 1))" /tmp/paper.log | grep "ERROR\|SEVERE" | tail -3 LAST_LOG_LINE=$CURRENT_LINE fi fi # Log trimming if [ $((LOOP_COUNT % 30)) -eq 0 ]; then for LF in /tmp/paper.log /tmp/bungee.log; do LS=$(stat -c%s "$LF" 2>/dev/null || echo 0) if [ "$LS" -gt 10485760 ]; then tail -1000 "$LF" > "${LF}.tmp" && mv "${LF}.tmp" "$LF" echo "[$(date '+%H:%M:%S')] Trimmed $(basename $LF)" fi done LAST_LOG_LINE=$(wc -l < /tmp/paper.log 2>/dev/null || echo 0) fi # Status output if [ $((LOOP_COUNT % 5)) -eq 0 ]; then RSS=$(ps -p $BACKEND_PID -o rss= 2>/dev/null | awk '{printf "%.0f", $1/1024}') echo "[STATUS] Players: ${PLAYER_COUNT:-?} | RAM: ${RSS:-?}MB | $([ "$IDLE_MODE" = true ] && echo IDLE || echo ACTIVE)" fi sleep 60 done