#!/usr/bin/env bash # # Provisions a real NeoForge dedicated server, drops the built mod into it and drives it through a # scripted session — once with the default (zero-instrumentation) configuration and once with deep # mode on. The resulting worlds are left in build/e2e/ for ChunkInspectorE2ETest to assert on. # # Runnable on its own: ./e2e/run-servers.sh # set -euo pipefail project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" neo_version="${NEO_VERSION:-$(sed -n 's/^neo_version=//p' "$project_dir/gradle.properties")}" mod_jar="${MOD_JAR:-$(ls "$project_dir"/build/libs/chunkinspector-*.jar 2>/dev/null | head -1)}" out_dir="${E2E_OUT:-$project_dir/build/e2e}" cache_dir="${E2E_CACHE:-$HOME/.cache/chunkinspector-e2e}" boot_timeout="${E2E_BOOT_TIMEOUT:-420}" # The forceloaded square the assertions look for: chunks (100, 100)..(103, 103) — 4x4, far enough # from spawn to form its own cluster and small enough to stay under the report's suspect cap. # /forceload takes *block* coordinates, so these are the corners of those chunks: 100 * 16 = 1600 # and 103 * 16 + 15 = 1663. force_from="1600 1600" force_to="1663 1663" if [[ -z "$mod_jar" || ! -f "$mod_jar" ]]; then echo "no mod jar found; run ./gradlew jar first" >&2 exit 1 fi log() { printf '[e2e] %s\n' "$*"; } # --------------------------------------------------------------------- provision installer="$cache_dir/neoforge-$neo_version-installer.jar" template="$cache_dir/server-$neo_version" mkdir -p "$cache_dir" if [[ ! -f "$installer" ]]; then log "downloading NeoForge $neo_version installer" curl -fsSL -o "$installer.part" \ "https://maven.neoforged.net/releases/net/neoforged/neoforge/$neo_version/neoforge-$neo_version-installer.jar" mv "$installer.part" "$installer" fi # The installation is cached and copied per run so the two modes can never share a world. if [[ ! -f "$template/.installed" ]]; then log "installing dedicated server into $template" rm -rf "$template" mkdir -p "$template" # From the cache directory: the installer drops its own .log file next to the working directory. (cd "$cache_dir" && java -jar "$installer" --installServer "$template") > "$cache_dir/install.log" 2>&1 \ || { tail -40 "$cache_dir/install.log" >&2; exit 1; } touch "$template/.installed" fi args_file="libraries/net/neoforged/neoforge/$neo_version/unix_args.txt" [[ -f "$template/$args_file" ]] || { echo "missing $args_file in the installed server" >&2; exit 1; } # ------------------------------------------------------------------------- run # run run() { local mode="$1" shift local dir="$out_dir/$mode" log "preparing $mode server in $dir" rm -rf "$dir" mkdir -p "$dir" cp -a "$template/." "$dir/" rm -f "$dir/.installed" mkdir -p "$dir/mods" cp "$mod_jar" "$dir/mods/" echo "eula=true" > "$dir/eula.txt" cat > "$dir/server.properties" <<'PROPERTIES' level-name=world level-type=minecraft:flat level-seed=e2e online-mode=false max-players=1 view-distance=10 simulation-distance=10 spawn-protection=0 sync-chunk-writes=false enable-jmx-monitoring=false enable-status=false max-tick-time=-1 PROPERTIES local fifo="$dir/console-in" mkfifo "$fifo" ( cd "$dir" exec java -Xmx2G "$@" "@$args_file" --nogui < console-in > console.log 2>&1 ) & local server=$! # Opening the write end unblocks the server's read end; keeping it open keeps stdin alive. exec 9> "$fifo" log "waiting for $mode server to finish loading" local waited=0 until grep -q 'Done (' "$dir/console.log" 2>/dev/null; do if ! kill -0 "$server" 2>/dev/null; then exec 9>&- tail -40 "$dir/console.log" >&2 echo "$mode server exited before it finished loading" >&2 exit 1 fi sleep 1 waited=$((waited + 1)) if [[ $waited -ge $boot_timeout ]]; then exec 9>&- kill "$server" 2>/dev/null || true echo "$mode server did not start within ${boot_timeout}s" >&2 exit 1 fi done say() { printf '%s\n' "$1" >&9; log " > $1"; sleep "${2:-2}"; } # A leaking chunk loader, simulated with the one permanent ticket every vanilla server can make. # The pause covers generating the 4x4 square, which has never been visited before. say "forceload add $force_from $force_to" 5 say "chunkinspector status" say "chunkinspector why 102 102" say "chunkinspector snapshot full" 5 # Long enough for at least one periodic snapshot (interval is 200 ticks = 10s) to also land. say "chunkinspector status" 12 say "stop" 0 log "waiting for $mode server to shut down" wait "$server" || true exec 9>&- rm -f "$fifo" local summaries="$dir/world/chunk-summaries" [[ -d "$summaries" ]] || { tail -60 "$dir/console.log" >&2; echo "no $summaries produced" >&2; exit 1; } log "$mode done: $(ls "$summaries" | wc -l) files in chunk-summaries" } mkdir -p "$out_dir" # Tier one: nothing injected into the game at all. run light \ -Dchunkinspector.interval=200 \ -Dchunkinspector.retention=5 # Tier two: the opt-in expensive analysis. run deep \ -Dchunkinspector.deep=true \ -Dchunkinspector.attribution=true \ -Dchunkinspector.interval=200 \ -Dchunkinspector.retention=5 log "all servers completed"