OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / distribute / hypertps-win64 / scripts / dev-run.sh

160 lines · bash ref:
#!/usr/bin/env bash
# dev-run — the SOURCE-TREE equivalent of the installed `hyper-tps` launcher.
#
#   scripts/dev-run.sh [extra godot args...]
#   scripts/dev-run.sh --daemon-only     # daemon, no game
#   scripts/dev-run.sh --no-daemon       # game only, as before
#
# WHY THIS EXISTS. Running godot against the source tree by hand gets you the
# game and nothing else:
#
#   godot.linuxbsd.editor.x86_64 --path <repo> --rendering-driver vulkan
#
# The installed launcher is `bare index.js`, and index.js starts keet-daemon
# BEFORE the game binary. Skip it and the game boots with no daemon: the UDS
# client retries /tmp/keet-daemon.sock forever, room state never loads
# ("room-state load timed out — proceeding with defaults"), and every durable
# write lives only in GameState's in-memory mirror until you quit. Which means
# persistence cannot be tested from source at all — you get a session that looks
# like it is saving and loses everything on exit.
#
# So: same two processes the launcher runs, but the SOURCE tree as the game
# rather than the exported .pck. Edit a .gd, restart, keep the daemon.
#
# THE ENGINE IS THE FORK, NOT SYSTEM godot4. project.godot declares
# config/features=["4.7"] and the system binary is 4.6.stable — the odd one out.
# CI uses this same nvidia-rtx-godot editor build (see .pear-git/ci.yaml).
#
# NOT a replacement for scripts/daemon-session.sh, which manages long-lived
# headless peers off the INSTALLED launcher. This is the interactive dev loop.

set -uo pipefail

REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GODOT="${HYPER_TPS_GODOT:-/storage/projects/code/godot/nvidia-rtx-godot/bin/godot.linuxbsd.editor.x86_64}"
BARE="${HYPER_TPS_BARE:-$(command -v bare || echo "$HOME/.local/share/store/apps/hyper-tps/runtime/bare")}"
SOCK="${KEET_SOCK:-/tmp/keet-daemon.sock}"
LOG="${HYPER_TPS_DEV_LOG:-/tmp/hyper-tps-dev-daemon.log}"

_die() { echo "dev-run: $*" >&2; exit 1; }

# A live socket, not merely a socket FILE — index.js makes the same distinction,
# because a daemon killed with -9 leaves the node behind and everything after
# would then wait on something that will never answer.
# python3, NOT `bare -e`: bare cannot resolve node_modules from -e source, so a
# `require('bare-net')` probe aborts with SIGABRT (134) from any cwd — which is
# neither "alive" nor "dead" and would have made every branch below wrong.
_daemon_alive() {
  python3 -c "
import socket,sys
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.settimeout(2)
try:
    s.connect('$SOCK'); s.close(); sys.exit(0)
except Exception:
    sys.exit(1)
" 2>/dev/null
}

want_daemon=1
daemon_only=0
godot_args=()
# Rooms to OPEN AND INDEX at daemon boot. StateBase is lazy — "a bare daemon
# never indexes a room on its own" (index.js) — so without a pin the room is not
# opened until the game first asks for it.
#
# MEASURED, because game.gd's _await_room_state comment claims 15-20 s for this
# and that is wrong by more than an order of magnitude:
#
#   cold daemon boot -> UDS socket      1.40 s
#   first state-list (opens the room)   1.18 s
#   subsequent state-list (warm)        0.05 s   (1014 keys)
#
# So pinning saves about a second, and is worth having, but it is NOT where a
# slow start goes. Reading local state was never the bottleneck.
pin_rooms=()
[ -n "${HYPER_TPS_PIN_ROOM:-}" ] && pin_rooms+=(--pin-room "$HYPER_TPS_PIN_ROOM")
for a in "$@"; do
  case "$a" in
    --no-daemon)   want_daemon=0 ;;
    --daemon-only) daemon_only=1 ;;
    --pin-room=*)  pin_rooms+=(--pin-room "${a#*=}") ;;
    *)             godot_args+=("$a") ;;
  esac
done

[ -x "$GODOT" ] || _die "engine missing at $GODOT (set HYPER_TPS_GODOT)"
[ -x "$BARE" ] || _die "bare missing (set HYPER_TPS_BARE)"

started_daemon=0
if [ "$want_daemon" = 1 ]; then
  if _daemon_alive; then
    echo "dev-run: keet-daemon already running — reusing it"
  else
    # Clear a stale node so the client is not left retrying a dead socket.
    [ -S "$SOCK" ] && rm -f "$SOCK"
    echo "dev-run: starting keet-daemon from source -> $LOG"
    [ ${#pin_rooms[@]} -gt 0 ] && echo "dev-run: pinning ${#pin_rooms[@]} room(s) so their StateBase is warm"
    ( cd "$REPO" && setsid "$BARE" index.js --daemon-only --no-game "${pin_rooms[@]}" >"$LOG" 2>&1 & echo $! >/tmp/hyper-tps-dev-daemon.pid )
    started_daemon=1
    for _ in $(seq 1 60); do
      _daemon_alive && break
      sleep 0.5
    done
    if _daemon_alive; then
      echo "dev-run: keet-daemon ready"
    else
      echo "dev-run: keet-daemon did NOT come up — see $LOG" >&2
      echo "dev-run: continuing without it; room state will not load" >&2
    fi
  fi
fi

# Leave a daemon we did not start alone: it may be a pinned fleet peer, and
# killing someone else's always-on node to end a dev session would be rude and
# hard to notice.
_cleanup() {
  if [ "$started_daemon" = 1 ] && [ -f /tmp/hyper-tps-dev-daemon.pid ]; then
    local pid; pid="$(cat /tmp/hyper-tps-dev-daemon.pid)"
    echo "dev-run: stopping the keet-daemon we started (pgid $pid)"
    kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null
    rm -f /tmp/hyper-tps-dev-daemon.pid
  fi
}
trap _cleanup EXIT INT TERM

if [ "$daemon_only" = 1 ]; then
  echo "dev-run: daemon only — Ctrl-C to stop"
  trap - EXIT            # a daemon asked for explicitly outlives this shell
  exit 0
fi

# TEE TO A FILE, because the terminal cannot be trusted here. NGX's logger prints
# RAW BINARY ("called from module ... at <bytes>"), and those control codes wreck
# the terminal: everything the game prints afterwards is still emitted but is not
# readable on screen. That cost a full diagnosis cycle — the log LOOKED truncated
# at NGX init, so I concluded the game had died when it was running fine.
#
# Filtering the stream would drop legitimate UTF-8 from our own logs, so the file
# stays raw and is read with grep.
GAME_LOG="${HYPER_TPS_GAME_LOG:-/tmp/hyper-tps-game-dev.log}"
echo "dev-run: launching $(basename "$GODOT") against $REPO"
echo "dev-run: full output -> $GAME_LOG"
# ELAPSED SECONDS ON EVERY LINE. Without them the log says what happened and
# never when, so "it takes about five seconds" cannot be attributed to NGX init,
# shader compilation, the 1183-brush import or the state load — and guessing
# between those has already cost this session twice.
#
# Binary-safe on purpose: NGX prints raw bytes, so this reads and writes
# sys.stdin.buffer rather than decoding text, and flushes per line so a crash
# still leaves an accurate tail.
DISPLAY="${DISPLAY:-:0}" "$GODOT" --path "$REPO" --rendering-driver vulkan "${godot_args[@]}" 2>&1 \
  | python3 -u -c '
import sys, time
t0 = time.monotonic()
out = sys.stdout.buffer
for raw in sys.stdin.buffer:
    out.write(("[%8.3f] " % (time.monotonic() - t0)).encode())
    out.write(raw)
    out.flush()
' | tee "$GAME_LOG"