mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-13 17:04:15 +00:00
481 lines
16 KiB
Bash
Executable File
481 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Unattended flyby run of a test Angelica jar inside the real Prism GTNH instance, one launch per variant
|
|
#
|
|
# Usage: scripts/flyby-prism.sh <name> <variant>...
|
|
# Variants: sdl-off | sdl-pack | gl-off | gl-pack (SDL-GPU vs OpenGL x shader pack off vs on)
|
|
# gl-pack writes <pack>.txt from the pack's profile.HIGH first (never Ultra on OpenGL) and fails before launching without it.
|
|
# Output: tmp/prism/<name>/<variant>/ (fml-client-latest.log, flyby-shot-*.png, console from the checker).
|
|
# Prints one PASS/FAIL line per variant; exits non-zero if any fails. A run that never reaches the world
|
|
# (no 'Flyby: world saving is off' in the log) is FAILED and stops the batch.
|
|
# Needs a hands-off machine: the game must keep focus (waitForFocus) and runs fullscreen.
|
|
#
|
|
# PRISM_INSTANCE - instance id (default: GTNH-daily-2026-10-01+766-mmcprism-java17-26).
|
|
# PRISM_BIN - launcher binary (default: /Applications/Prism Launcher.app/Contents/MacOS/prismlauncher).
|
|
# PRISM_TIMEOUT - seconds before a variant's game is killed (default: 900).
|
|
# PRISM_KEEP_JAR - 1 leaves the test jar installed and the old angelica jar(s) disabled after the script ends (default: 0).
|
|
# PRISM_WORLD - save for QuickPlay (default: FlybyGTNH). No spaces.
|
|
# PRISM_ENTRIES - id=status,... passed as -Dangelica.flyby.debugEntries on every variant and graded with
|
|
# --require-renderers (default: all nine world renderers inOverlay).
|
|
#
|
|
# Sets fullscreen:true in the instance options.txt per variant (avoids vsync pacing).
|
|
# Restores instance.cfg, options.txt, config/shaders.properties and the pack option file on exit (cmp-verified).
|
|
# scripts/flyby-prism.sh --helper <jvmargs|profile> ... runs one pure step in isolation.
|
|
|
|
set -euo pipefail
|
|
|
|
# shellcheck source=flyby-lib.sh
|
|
. "$(dirname "$0")/flyby-lib.sh"
|
|
|
|
HELPER="$(cat <<'PY'
|
|
import re, sys
|
|
|
|
|
|
def read(path):
|
|
with open(path, newline="") as f:
|
|
return f.read()
|
|
|
|
|
|
def write(path, text):
|
|
with open(path, "w", newline="") as f:
|
|
f.write(text)
|
|
|
|
|
|
def unquote(raw):
|
|
if len(raw) >= 2 and raw[0] == '"' and raw[-1] == '"':
|
|
return True, raw[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
|
return False, raw
|
|
|
|
|
|
def requote(value, quoted):
|
|
if quoted or re.search(r"[\s,;\"]", value):
|
|
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
return value
|
|
|
|
|
|
def key_of(token):
|
|
return token.split("=", 1)[0]
|
|
|
|
|
|
def jvmargs(cfg, original, backend, extra):
|
|
src = read(original)
|
|
m = re.search(r"^JvmArgs=([^\r\n]*)", src, re.M)
|
|
quoted, value = unquote(m.group(1)) if m else (True, "")
|
|
new = ["-Dangelica.sdlgpu.enable=" + backend] + extra
|
|
for t in new:
|
|
if re.search(r"\s", t) or '"' in t:
|
|
raise SystemExit("error: jvm arg contains whitespace or a quote: " + repr(t))
|
|
drop = {key_of(t) for t in new}
|
|
kept = [t for t in value.split() if key_of(t) not in drop]
|
|
line = "JvmArgs=" + requote(" ".join(kept + new), quoted)
|
|
text = read(cfg)
|
|
if re.search(r"^JvmArgs=[^\r\n]*", text, re.M):
|
|
text = re.sub(r"^JvmArgs=[^\r\n]*", lambda _: line, text, count=1, flags=re.M)
|
|
elif re.search(r"^\[General\][^\r\n]*", text, re.M):
|
|
text = re.sub(r"^(\[General\][^\r\n]*)(\r?\n)", lambda g: g.group(1) + g.group(2) + line + g.group(2), text, count=1, flags=re.M)
|
|
else:
|
|
text = line + "\n" + text
|
|
if re.search(r"^OverrideJavaArgs=[^\r\n]*", text, re.M):
|
|
text = re.sub(r"^OverrideJavaArgs=[^\r\n]*", "OverrideJavaArgs=true", text, count=1, flags=re.M)
|
|
else:
|
|
text = re.sub(r"^(JvmArgs=[^\r\n]*)(\r?\n|$)", lambda g: g.group(1) + (g.group(2) or "\n") + "OverrideJavaArgs=true" + g.group(2), text, count=1, flags=re.M)
|
|
write(cfg, text)
|
|
print(line)
|
|
|
|
|
|
def logical_lines(text):
|
|
out = []
|
|
buf = ""
|
|
for raw in text.splitlines():
|
|
if buf:
|
|
piece = raw.strip()
|
|
else:
|
|
piece = raw.rstrip()
|
|
if piece.endswith("\\"):
|
|
buf += piece[:-1] + " "
|
|
continue
|
|
out.append(buf + piece)
|
|
buf = ""
|
|
if buf:
|
|
out.append(buf)
|
|
return out
|
|
|
|
|
|
def profile_tokens(defs, name, seen):
|
|
if name in seen:
|
|
raise SystemExit("error: profile cycle at " + name)
|
|
if name not in defs:
|
|
raise SystemExit("error: profile." + name + " not defined")
|
|
out = []
|
|
for tok in defs[name].split():
|
|
if tok.startswith("profile."):
|
|
out.extend(profile_tokens(defs, tok[len("profile."):], seen | {name}))
|
|
else:
|
|
out.append(tok)
|
|
return out
|
|
|
|
|
|
def profile(props, out_txt, name):
|
|
defs = {}
|
|
for line in logical_lines(read(props)):
|
|
s = line.strip()
|
|
if s.startswith("#") or s.startswith("!"):
|
|
continue
|
|
m = re.match(r"profile\.(\w+)\s*=(.*)$", s)
|
|
if m:
|
|
defs[m.group(1)] = m.group(2)
|
|
if name not in defs:
|
|
raise SystemExit("error: profile." + name + " not found in " + props)
|
|
options = {}
|
|
for tok in profile_tokens(defs, name, frozenset()):
|
|
if "=" in tok:
|
|
k, v = tok.split("=", 1)
|
|
elif tok.startswith("!"):
|
|
k, v = tok[1:], "false"
|
|
else:
|
|
k, v = tok, "true"
|
|
if not re.match(r"^[A-Za-z_][A-Za-z0-9_.]*$", k) or v == "":
|
|
raise SystemExit("error: cannot parse profile token " + repr(tok))
|
|
options[k] = v
|
|
if not options:
|
|
raise SystemExit("error: profile." + name + " is empty")
|
|
write(out_txt, "".join(k + "=" + v + "\n" for k, v in options.items()))
|
|
print(len(options))
|
|
|
|
|
|
def main(argv):
|
|
cmd = argv[1]
|
|
if cmd == "jvmargs":
|
|
jvmargs(argv[2], argv[3], argv[4], argv[5:])
|
|
elif cmd == "profile":
|
|
profile(argv[2], argv[3], argv[4] if len(argv) > 4 else "HIGH")
|
|
else:
|
|
raise SystemExit("error: unknown helper " + cmd)
|
|
|
|
|
|
main(sys.argv)
|
|
PY
|
|
)"
|
|
|
|
helper() {
|
|
python3 -c "$HELPER" "$@"
|
|
}
|
|
|
|
if [ "${1:-}" = "--helper" ]; then
|
|
shift
|
|
helper "$@"
|
|
exit $?
|
|
fi
|
|
|
|
NAME="${1:?usage: flyby-prism.sh <name> <variant>...}"
|
|
shift
|
|
if [ $# -eq 0 ]; then
|
|
echo "usage: flyby-prism.sh <name> <sdl-off|sdl-pack|gl-off|gl-pack>..." >&2
|
|
exit 2
|
|
fi
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
PRISM_INSTANCE="${PRISM_INSTANCE:-GTNH-daily-2026-10-01+766-mmcprism-java17-26}"
|
|
PRISM_BIN="${PRISM_BIN:-/Applications/Prism Launcher.app/Contents/MacOS/prismlauncher}"
|
|
PRISM_TIMEOUT="${PRISM_TIMEOUT:-900}"
|
|
PRISM_KEEP_JAR="${PRISM_KEEP_JAR:-0}"
|
|
PRISM_WORLD="${PRISM_WORLD:-FlybyGTNH}"
|
|
PRISM_ENTRIES="${PRISM_ENTRIES:-minecraft:chunk_borders=inOverlay,minecraft:chunk_section_visibility=inOverlay,minecraft:visualize_block_light_levels=inOverlay,minecraft:visualize_sky_light_levels=inOverlay,minecraft:visualize_heightmap=inOverlay,minecraft:visualize_collision_boxes=inOverlay,minecraft:visualize_solid_faces=inOverlay,minecraft:visualize_water_levels=inOverlay,minecraft:visualize_entity_supporting_block=inOverlay}"
|
|
|
|
for v in "$@"; do
|
|
case "$v" in
|
|
sdl-off|sdl-pack|gl-off|gl-pack) ;;
|
|
*) echo "error: variant must be sdl-off, sdl-pack, gl-off or gl-pack, got: $v" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
case "$PRISM_WORLD" in
|
|
*[[:space:]\"]*|"") echo "error: PRISM_WORLD must be non-empty without spaces or quotes: '$PRISM_WORLD'" >&2; exit 2 ;;
|
|
esac
|
|
case "$PRISM_ENTRIES" in
|
|
*[[:space:]\"]*) echo "error: PRISM_ENTRIES must not contain spaces or quotes" >&2; exit 2 ;;
|
|
esac
|
|
|
|
INST="$HOME/Library/Application Support/PrismLauncher/instances/$PRISM_INSTANCE"
|
|
MC="$INST/.minecraft"
|
|
CFG="$INST/instance.cfg"
|
|
OPTS="$MC/options.txt"
|
|
SHADERS="$MC/config/shaders.properties"
|
|
for f in "$CFG" "$OPTS" "$SHADERS"; do
|
|
if [ ! -f "$f" ]; then
|
|
echo "error: missing $f" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if [ ! -x "$PRISM_BIN" ]; then
|
|
echo "error: launcher not executable: $PRISM_BIN" >&2
|
|
exit 1
|
|
fi
|
|
|
|
INST_TAIL="${PRISM_INSTANCE#*+}"
|
|
GAME_PAT="java.*$INST_TAIL"
|
|
if pgrep -x prismlauncher >/dev/null 2>&1; then
|
|
echo "error: Prism Launcher is already running; quit it first" >&2
|
|
exit 1
|
|
fi
|
|
if pgrep -f "$GAME_PAT" >/dev/null 2>&1; then
|
|
echo "error: a process matching '$GAME_PAT' (the instance's game?) is running" >&2
|
|
exit 1
|
|
fi
|
|
if pgrep -f 'GradleStart|retrofuturabootstrap' >/dev/null 2>&1; then
|
|
echo "error: a dev client is running" >&2
|
|
exit 1
|
|
fi
|
|
|
|
./gradlew assemble
|
|
JAR=""
|
|
for f in "$ROOT"/build/libs/angelica-*.jar; do
|
|
case "$f" in
|
|
*-dev.jar|*-sources.jar|*-api.jar|*-dev-preshadow.jar|*-predowngrade.jar|*shadow-stripped.jar) continue ;;
|
|
esac
|
|
if [ -f "$f" ] && { [ -z "$JAR" ] || [ "$f" -nt "$JAR" ]; }; then
|
|
JAR="$f"
|
|
fi
|
|
done
|
|
if [ -z "$JAR" ]; then
|
|
echo "error: no production angelica jar in build/libs" >&2
|
|
exit 1
|
|
fi
|
|
JAR_NAME="$(basename "$JAR")"
|
|
echo "==> test jar: $JAR"
|
|
|
|
OUT="$ROOT/tmp/prism/$NAME"
|
|
BAK="$OUT/backup"
|
|
|
|
PACK="$(awk '/^shaderPack=/ { sub(/^[^=]*=/, ""); sub(/\r$/, ""); print; exit }' "$SHADERS")"
|
|
PACK_TXT=""
|
|
if [ -n "$PACK" ]; then
|
|
PACK_TXT="$MC/shaderpacks/$PACK.txt"
|
|
fi
|
|
for v in "$@"; do
|
|
if [ "$v" = "gl-pack" ] || [ "$v" = "sdl-pack" ]; then
|
|
if [ -z "$PACK" ] || [ ! -d "$MC/shaderpacks/$PACK" ]; then
|
|
echo "error: shaderPack '$PACK' missing from $MC/shaderpacks" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
STALE_BAK=""
|
|
if [ -d "$BAK" ]; then
|
|
STALE_BAK="$(find "$BAK" -mindepth 1 -maxdepth 1 | head -n 1)"
|
|
fi
|
|
STALE_DISABLED=""
|
|
for f in "$MC"/mods/angelica-*.jar.disabled; do
|
|
if [ -e "$f" ]; then
|
|
STALE_DISABLED="$STALE_DISABLED $f"
|
|
fi
|
|
done
|
|
if [ -n "$STALE_BAK" ] || [ -n "$STALE_DISABLED" ]; then
|
|
echo "error: leftovers from an interrupted run; nothing was touched" >&2
|
|
echo "undo steps:" >&2
|
|
echo " 1. copy back from $BAK (if present): instance.cfg -> $CFG, options.txt -> $OPTS, shaders.properties -> $SHADERS, pack.txt -> ${PACK_TXT:-n/a}" >&2
|
|
echo " 2. remove the installed test jar from $MC/mods (originals are listed in $BAK/renamed.list)" >&2
|
|
echo " 3. drop the .disabled suffix from:${STALE_DISABLED:- (none found)}" >&2
|
|
echo " 4. rm -rf '$BAK'" >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p "$BAK"
|
|
|
|
flyby_copy "$CFG" "$BAK/instance.cfg"
|
|
flyby_copy "$OPTS" "$BAK/options.txt"
|
|
flyby_copy "$SHADERS" "$BAK/shaders.properties"
|
|
if [ -n "$PACK_TXT" ]; then
|
|
flyby_backup_file "$PACK_TXT" "$BAK/pack.txt"
|
|
fi
|
|
|
|
RENAMED="$BAK/renamed.list"
|
|
: >| "$RENAMED"
|
|
|
|
PRISM_PID=""
|
|
GAME_PID=""
|
|
|
|
kill_game() {
|
|
local pids
|
|
pids="$(pgrep -f "$GAME_PAT" || true)"
|
|
if [ -n "$pids" ]; then
|
|
# shellcheck disable=SC2086
|
|
kill $pids 2>/dev/null || true
|
|
sleep 3
|
|
pids="$(pgrep -f "$GAME_PAT" || true)"
|
|
if [ -n "$pids" ]; then
|
|
# shellcheck disable=SC2086
|
|
kill -9 $pids 2>/dev/null || true
|
|
fi
|
|
fi
|
|
if [ -n "$PRISM_PID" ] && kill -0 "$PRISM_PID" 2>/dev/null; then
|
|
kill "$PRISM_PID" 2>/dev/null || true
|
|
sleep 1
|
|
kill -9 "$PRISM_PID" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
RESTORED=0
|
|
restore() {
|
|
set +e
|
|
if [ "$RESTORED" = "1" ]; then
|
|
return 0
|
|
fi
|
|
RESTORED=1
|
|
trap - EXIT INT TERM HUP
|
|
kill_game
|
|
local ok=1
|
|
flyby_restore_file "$BAK/instance.cfg" "$CFG" || ok=0
|
|
flyby_restore_file "$BAK/options.txt" "$OPTS" || ok=0
|
|
flyby_restore_file "$BAK/shaders.properties" "$SHADERS" || ok=0
|
|
if [ -n "$PACK_TXT" ]; then
|
|
flyby_restore_file "$BAK/pack.txt" "$PACK_TXT" || ok=0
|
|
fi
|
|
if [ "$PRISM_KEEP_JAR" != "1" ]; then
|
|
rm -f "$MC/mods/$JAR_NAME" || { echo "warning: could not remove $MC/mods/$JAR_NAME" >&2; ok=0; }
|
|
while IFS= read -r f; do
|
|
if [ -n "$f" ] && [ -f "$f.disabled" ]; then
|
|
mv -f "$f.disabled" "$f" || { echo "warning: could not re-enable $f" >&2; ok=0; }
|
|
fi
|
|
done < "$RENAMED"
|
|
if [ "$ok" = "1" ]; then
|
|
rm -rf "$BAK"
|
|
fi
|
|
echo "==> test jar removed, original angelica jar(s) re-enabled"
|
|
else
|
|
echo "==> test jar LEFT installed: $MC/mods/$JAR_NAME"
|
|
echo "==> to undo: rm -f '$MC/mods/$JAR_NAME'; then drop the .disabled suffix from: $(tr '\n' ' ' < "$RENAMED")"
|
|
fi
|
|
}
|
|
trap restore EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
trap 'exit 129' HUP
|
|
|
|
for f in "$MC"/mods/angelica-*.jar; do
|
|
if [ -f "$f" ]; then
|
|
printf '%s\n' "$f" >> "$RENAMED"
|
|
mv -n "$f" "$f.disabled"
|
|
fi
|
|
done
|
|
flyby_copy "$JAR" "$MC/mods/$JAR_NAME"
|
|
echo "==> installed $JAR_NAME; disabled: $(tr '\n' ' ' < "$RENAMED")"
|
|
|
|
game_pid() {
|
|
pgrep -f "$GAME_PAT" | head -n 1 || true
|
|
}
|
|
|
|
FAILED=0
|
|
for VARIANT in "$@"; do
|
|
VDIR="$OUT/$VARIANT"
|
|
flyby_fresh_dir "$VDIR"
|
|
|
|
case "$VARIANT" in
|
|
sdl-*) BACKEND=true ;;
|
|
*) BACKEND=false ;;
|
|
esac
|
|
case "$VARIANT" in
|
|
*-pack) SHADERS_ON=true ;;
|
|
*) SHADERS_ON=false ;;
|
|
esac
|
|
|
|
EXTRA=(
|
|
-Dangelica.debug.tooling=true
|
|
-Dangelica.flyby.route=static
|
|
-Dangelica.flyby.length=400
|
|
-Dangelica.flyby.warmupTicks=200
|
|
-Dangelica.flyby.exitWhenDone=true
|
|
-Dangelica.flyby.waitForFocus=true
|
|
-Dangelica.flyby.screenshots=4
|
|
-Dangelica.flyby.debugHud=true
|
|
-Dangelica.flyby.debugDump=true
|
|
"-DquickPlaySingleplayer=$PRISM_WORLD"
|
|
)
|
|
CHECK=(--modpack --shots 4 --expect-mode OVERLAY --require-group botania:botania --require-group journeymap:journeymap --forbid-dup --require-mod etfuturum)
|
|
EXTRA+=("-Dangelica.flyby.debugEntries=$PRISM_ENTRIES")
|
|
RENDERERS="$(printf '%s\n' "$PRISM_ENTRIES" | tr ',' '\n' | sed -e '/^$/d' -e 's/=[^=]*$//' | paste -sd, -)"
|
|
CHECK+=(--require-renderers "$RENDERERS")
|
|
|
|
flyby_restore_file "$BAK/instance.cfg" "$CFG"
|
|
echo "==> $NAME/$VARIANT: JvmArgs"
|
|
helper jvmargs "$CFG" "$BAK/instance.cfg" "$BACKEND" "${EXTRA[@]}" | tee "$VDIR/jvmargs.txt"
|
|
|
|
flyby_restore_file "$BAK/shaders.properties" "$SHADERS"
|
|
flyby_set_key "$SHADERS" enableShaders "$SHADERS_ON" =
|
|
|
|
if [ -n "$PACK_TXT" ]; then
|
|
flyby_restore_file "$BAK/pack.txt" "$PACK_TXT"
|
|
fi
|
|
if [ "$VARIANT" = "gl-pack" ]; then
|
|
PROPS="$MC/shaderpacks/$PACK/shaders/shaders.properties"
|
|
if [ ! -f "$PROPS" ] || ! COUNT="$(helper profile "$PROPS" "$PACK_TXT" HIGH)"; then
|
|
echo "FAIL $VARIANT: pack profile.HIGH could not be read from $PROPS; not launching"
|
|
FAILED=1
|
|
continue
|
|
fi
|
|
echo "==> $NAME/$VARIANT: wrote $COUNT HIGH options to $PACK_TXT"
|
|
fi
|
|
|
|
flyby_restore_file "$BAK/options.txt" "$OPTS"
|
|
flyby_set_key "$OPTS" fullscreen true :
|
|
|
|
mkdir -p "$MC/screenshots"
|
|
rm -f "$MC"/screenshots/flyby-shot-*.png "$MC/logs/fml-client-latest.log"
|
|
flyby_crash_list "$MC/crash-reports" >| "$VDIR/crashes.before"
|
|
|
|
echo "==> $NAME/$VARIANT: launching (timeout ${PRISM_TIMEOUT}s)"
|
|
"$PRISM_BIN" -l "$PRISM_INSTANCE" >| "$VDIR/launcher.log" 2>&1 &
|
|
PRISM_PID=$!
|
|
|
|
GAME_PID=""
|
|
WAITED=0
|
|
while [ "$WAITED" -lt 240 ]; do
|
|
GAME_PID="$(game_pid)"
|
|
if [ -n "$GAME_PID" ]; then
|
|
break
|
|
fi
|
|
if ! kill -0 "$PRISM_PID" 2>/dev/null; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
WAITED=$((WAITED + 2))
|
|
done
|
|
|
|
STATUS="0"
|
|
if [ -z "$GAME_PID" ]; then
|
|
STATUS="nogame"
|
|
echo "==> $NAME/$VARIANT: no game process appeared within ${WAITED}s"
|
|
else
|
|
DEADLINE=$(( $(date +%s) + PRISM_TIMEOUT ))
|
|
while kill -0 "$GAME_PID" 2>/dev/null; do
|
|
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
|
echo "==> $NAME/$VARIANT: timeout, killing game"
|
|
STATUS="timeout"
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
fi
|
|
kill_game
|
|
PRISM_PID=""
|
|
sleep 2
|
|
|
|
NEW_CRASHES="$(flyby_new_crashes "$MC/crash-reports" "$VDIR/crashes.before" "$VDIR/crashes.after")"
|
|
flyby_collect "$MC" "$VDIR" fml-client-latest.log
|
|
|
|
if ! grep -qF "Flyby: world saving is off" "$VDIR/fml-client-latest.log" 2>/dev/null; then
|
|
echo "FAIL $VARIANT: never entered the world (no 'Flyby: world saving is off' in the log; QuickPlay did not fire?); stopping the batch"
|
|
FAILED=1
|
|
break
|
|
fi
|
|
|
|
if [ "$STATUS" = "nogame" ]; then
|
|
STATUS="1"
|
|
fi
|
|
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES" "${CHECK[@]}"; then
|
|
FAILED=1
|
|
fi
|
|
done
|
|
|
|
exit "$FAILED"
|