#!/usr/bin/env bash set -euo pipefail export LC_ALL=C SERVER='https://factoriobench.com' ASSETS='https://assets.factoriobench.com' TICKS=1000 RUNS=5 FACTORIO="${FACTORIO_BIN:-}" MAP='' MAP_HASH='' YES=0 SUBMIT=1 usage() { printf '%s\n' 'Usage: benchmark.sh [--factorio PATH] [--map HASH|FILE] [--map-hash HASH]' \ ' [--ticks N] [--runs N] [--server URL] [--yes] [--no-submit]' } while (($#)); do case "$1" in --factorio) FACTORIO=${2:?missing path}; shift 2 ;; --map) MAP=${2:?missing map}; shift 2 ;; --map-hash) MAP_HASH=${2:?missing hash}; shift 2 ;; --ticks) TICKS=${2:?missing ticks}; shift 2 ;; --runs) RUNS=${2:?missing runs}; shift 2 ;; --server) SERVER=${2%/}; shift 2 ;; --yes) YES=1; shift ;; --no-submit) SUBMIT=0; shift ;; -h|--help) usage; exit 0 ;; *) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;; esac done find_factorio() { local candidate if [[ -n "$FACTORIO" && -x "$FACTORIO" ]]; then return; fi for candidate in \ "$(command -v factorio 2>/dev/null || true)" \ "$PWD/bin/x64/factorio" "$PWD/factorio" \ "$HOME/.local/opt/factorio/bin/x64/factorio" \ "$HOME/.local/share/Steam/steamapps/common/Factorio/bin/x64/factorio" \ "$HOME/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/MacOS/factorio"; do if [[ -n "$candidate" && -x "$candidate" ]]; then FACTORIO=$candidate; return; fi done printf '%s\n' 'Could not locate Factorio. Pass --factorio /path/to/factorio.' >&2 exit 1 } sha256_file() { if command -v sha256sum >/dev/null; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null; then shasum -a 256 "$1" | awk '{print $1}' else printf '%s\n' 'sha256sum or shasum is required.' >&2; exit 1 fi } version_number() { local major=0 minor=0 patch=0 IFS=. read -r major minor patch _ <<<"$1" printf '%d\n' "$((10#$major * 1000000 + 10#${minor:-0} * 1000 + 10#${patch:-0}))" } find_factorio VERSION_LINE=$("$FACTORIO" --version | head -n 1) FACTORIO_VERSION=$(sed -nE 's/^[^0-9]*([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/p' <<<"$VERSION_LINE") FACTORIO_BUILD=$(sed -nE 's/.*build[[:space:]]+([0-9]+).*/\1/p' <<<"$VERSION_LINE") PLATFORM=$(sed -nE 's/.*build[[:space:]]+[0-9]+,[[:space:]]*([^,)]*).*/\1/p' <<<"$VERSION_LINE") DISTRIBUTION=$(sed -nE 's/.*build[[:space:]]+[0-9]+,[[:space:]]*[^,)]*,[[:space:]]*([^)]*).*/\1/p' <<<"$VERSION_LINE") if [[ -z "$FACTORIO_VERSION" ]]; then printf 'Could not parse Factorio version from: %s\n' "$VERSION_LINE" >&2; exit 1; fi printf 'Found Factorio %s at %s\n' "$FACTORIO_VERSION" "$FACTORIO" TEMP_MAP='' TEMP_MODS=$(mktemp -d) cleanup() { [[ -z "$TEMP_MAP" ]] || rm -f "$TEMP_MAP"; rm -rf "$TEMP_MODS"; } trap cleanup EXIT if [[ -f "$MAP" ]]; then MAP_FILE=$MAP MAP_SHA=$(sha256_file "$MAP_FILE") MAP_HASH=${MAP_HASH:-$MAP_SHA} else if [[ -n "$MAP" ]]; then MAP_HASH=$MAP else number=$(version_number "$FACTORIO_VERSION") if ((number < 17036)); then MAP_HASH='7ddeeb9bacf723a81d420674714bca09'; MAP_SHA='0441629e570a674c2cee86ea6bb55a1d1f0a1e9304ada85cad349dfb77df2a87' elif ((number < 1001000)); then MAP_HASH='4b0f2609a9f9fe2cd642d59dad8be163b2464e3d988dedb3cc2e36ec8da1e1bb'; MAP_SHA='4b0f2609a9f9fe2cd642d59dad8be163b2464e3d988dedb3cc2e36ec8da1e1bb' else MAP_HASH='4c5f65003d84370f16d6950f639be1d6f92984f24c0240de6335d3e161705504'; MAP_SHA='4c5f65003d84370f16d6950f639be1d6f92984f24c0240de6335d3e161705504' fi fi [[ "$MAP_HASH" =~ ^[0-9a-f]{32}([0-9a-f]{32})?$ ]] || { printf '%s\n' 'The requested comparison map is unavailable.' >&2; exit 1; } if [[ -z "${MAP_SHA:-}" ]]; then case "$MAP_HASH" in '7ddeeb9bacf723a81d420674714bca09') MAP_SHA='0441629e570a674c2cee86ea6bb55a1d1f0a1e9304ada85cad349dfb77df2a87' ;; '4b0f2609a9f9fe2cd642d59dad8be163b2464e3d988dedb3cc2e36ec8da1e1bb') MAP_SHA='4b0f2609a9f9fe2cd642d59dad8be163b2464e3d988dedb3cc2e36ec8da1e1bb' ;; '4c5f65003d84370f16d6950f639be1d6f92984f24c0240de6335d3e161705504') MAP_SHA='4c5f65003d84370f16d6950f639be1d6f92984f24c0240de6335d3e161705504' ;; esac fi [[ -n "${MAP_SHA:-}" ]] || { printf '%s\n' 'No verified SHA-256 is published for this map.' >&2; exit 1; } TEMP_MAP=$(mktemp) MAP_FILE=$TEMP_MAP printf 'Downloading map %s...\n' "$MAP_HASH" curl --fail --location --progress-bar "$ASSETS/maps/$MAP_HASH.zip" --output "$MAP_FILE" ACTUAL_SHA=$(sha256_file "$MAP_FILE") [[ "$ACTUAL_SHA" == "$MAP_SHA" ]] || { printf '%s\n' 'Downloaded map checksum mismatch.' >&2; exit 1; } fi printf 'Running %s benchmark runs of %s ticks...\n' "$RUNS" "$TICKS" FACTORIO_COMMAND=("$FACTORIO") if command -v stdbuf >/dev/null; then FACTORIO_COMMAND=(stdbuf -oL "$FACTORIO"); fi exec 3>&1 set +e FACTORIO_LOG=$("${FACTORIO_COMMAND[@]}" --mod-directory "$TEMP_MODS" --benchmark "$MAP_FILE" \ --benchmark-ticks "$TICKS" --benchmark-runs "$RUNS" --benchmark-verbose all --benchmark-sanitize 2>&1 | tee >(while IFS= read -r line; do [[ "$line" =~ Performed[[:space:]]+[0-9]+[[:space:]]+updates[[:space:]]+in[[:space:]]+[0-9.]+[[:space:]]+ms ]] && printf '%s\n' "$line" >&3 done)) STATUS=$? set -e exec 3>&- ((STATUS == 0)) || { printf '%s\n' "$FACTORIO_LOG" >&2; exit "$STATUS"; } RUN_TIMES=$(awk '$1=="Performed" && $5 ~ /^[0-9.]+$/ {print $5}' <<<"$FACTORIO_LOG") [[ $(wc -w <<<"$RUN_TIMES") -ge 1 ]] || { printf '%s\n' 'Factorio finished but no benchmark timings were found.' >&2; exit 1; } TIMES_CSV=$(paste -sd, <<<"$RUN_TIMES") BEST=$(sort -n <<<"$RUN_TIMES" | head -n 1) UPS=$(awk -v ticks="$TICKS" -v ms="$BEST" 'BEGIN {printf "%.3f", ticks * 1000 / ms}') case "$(uname -s)" in Linux*) CPU=$(awk -F: '/^model name/{sub(/^[ \t]+/,"",$2); print $2; exit}' /proc/cpuinfo) MEMORY_GIB=$(awk '/MemTotal/{printf "%.2f", $2 / 1048576}' /proc/meminfo) ;; Darwin*) CPU=$(sysctl -n machdep.cpu.brand_string) MEMORY_GIB=$(awk -v bytes="$(sysctl -n hw.memsize)" 'BEGIN {printf "%.2f", bytes / 1073741824}') ;; *) printf '%s\n' 'Unsupported operating system. Use the PowerShell script on Windows.' >&2; exit 1 ;; esac CPU=${CPU:-Unknown CPU} MEMORY_GIB=${MEMORY_GIB:-1} printf '\nResult: %s UPS\nMap: %s\nFactorio: %s (build %s, %s, %s)\nCPU: %s\nMemory: %s GiB\n' \ "$UPS" "$MAP_HASH" "$FACTORIO_VERSION" "${FACTORIO_BUILD:-0}" "${PLATFORM:-unknown}" "${DISTRIBUTION:-unknown}" "$CPU" "$MEMORY_GIB" if ((SUBMIT == 0)); then printf '%s\n' 'Submission disabled.'; exit 0; fi if ((YES == 0)); then if [[ ! -t 0 ]]; then printf '%s\n' 'Not submitting non-interactively without --yes.'; exit 0; fi read -r -p 'Submit these displayed fields to Factorio Bench? [y/N] ' answer [[ "$answer" =~ ^[Yy]$ ]] || { printf '%s\n' 'Not submitted.'; exit 0; } fi RESPONSE=$(curl --fail --silent --show-error "$SERVER/result" \ --data-urlencode 'schema=4' \ --data-urlencode "map_hash=$MAP_HASH" \ --data-urlencode "ticks=$TICKS" \ --data-urlencode "runs=$RUNS" \ --data-urlencode "run_times_ms=$TIMES_CSV" \ --data-urlencode "factorio_version=$FACTORIO_VERSION" \ --data-urlencode "factorio_build=${FACTORIO_BUILD:-0}" \ --data-urlencode "platform=${PLATFORM:-unknown}" \ --data-urlencode "distribution=${DISTRIBUTION:-unknown}" \ --data-urlencode "cpu=$CPU" \ --data-urlencode "memory_gib=$MEMORY_GIB" \ --data-urlencode 'memory_modules=[]') RESULT_URL=$(sed -nE 's/.*"url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' <<<"$RESPONSE") printf 'Submitted: %s\n' "${RESULT_URL:-$RESPONSE}"