# Theria dev shell helpers — launch the game client from source for a local # playtest, straight from the terminal (no editor or extra tooling needed). # Source from ~/.zshrc: # [[ -f /Users/antonhahn/theria/env.zsh ]] && source /Users/antonhahn/theria/env.zsh # # Public interface — one verb dispatcher, `run`: # run launch the client to its connect menu (Practice/Host/Join) # run [flags] local practice as (lion cheetah hyena snake spider chameleon) # run --host | --join | … pass flags straight to the client (you pick the mode) # run menu explicit connect menu (same as no args) # run import refresh Godot's import / global-class cache only # run stop stop the running client # run log [N] show the last N lines of the launch log (default 40) # run help # # It launches windowed FROM SOURCE (never builds a .pck, never touches an # installed app, never commits). The client's boot scene now # ignores the installed update payload on a source/editor run, so this always plays # the working tree — not the last-downloaded shipped build. # # env overrides: # THERIA_GODOT= the godot binary (else `command -v godot`, then /opt/homebrew/bin/godot) # THERIA_NO_IMPORT=1 skip the pre-launch import refresh (faster relaunch when no # new `class_name` script was added since the last run) # Resolve the directory of this file once at source time — it IS the project root # (env.zsh lives next to project.godot). Inside a function ${0:A:h} would name the # function, so capture %x while it still points at the file being sourced. typeset -g _THERIA_DIR="${${(%):-%x}:A:h}" typeset -g _THERIA_RED=$'\033[0;31m' typeset -g _THERIA_GREEN=$'\033[0;32m' typeset -g _THERIA_YELLOW=$'\033[1;33m' typeset -g _THERIA_NC=$'\033[0m' # The hero pool, for messages + tab-completion. Mirrors AbilityData.TRIBE — Solane # (lion cheetah hyena) and Verdani (snake spider chameleon). typeset -ga _THERIA_HEROES=(lion cheetah hyena snake spider chameleon) # Where a background launch's stdout/stderr lands. A temp path, not the repo, so a # launch never leaves an untracked log in the public tree. typeset -g _THERIA_LOG="${${TMPDIR:-/tmp}%/}/theria-run.log" # ── shared primitives ──────────────────────────────────────────────────────── # red/yellow to stderr, green to stdout — same idiom as the FlashOS helpers. _theria_err() { print -u2 -- "${_THERIA_RED}$*${_THERIA_NC}"; } _theria_warn() { print -u2 -- "${_THERIA_YELLOW}$*${_THERIA_NC}"; } _theria_ok() { print -- "${_THERIA_GREEN}$*${_THERIA_NC}"; } # Echo the godot binary path on stdout, or error on stderr. Honors $THERIA_GODOT # verbatim, then PATH, then the Homebrew default the skill falls back to. _theria_godot() { emulate -L zsh local bin="${THERIA_GODOT:-$(command -v godot 2>/dev/null)}" [[ -n "$bin" ]] || bin=/opt/homebrew/bin/godot if [[ ! -x "$bin" ]]; then _theria_err "godot binary not found ($bin) — set \$THERIA_GODOT" return 1 fi print -r -- "$bin" } # Stop with a clear message if the binary or the project is missing (wrong machine / # moved tree), so a launch never half-starts. _theria_verify() { emulate -L zsh _theria_godot >/dev/null || return 1 if [[ ! -f "$_THERIA_DIR/project.godot" ]]; then _theria_err "no project.godot at $_THERIA_DIR" return 1 fi } # Refresh the import / global-class cache (foreground, wait for it). A run started # right after a new `class_name` script was added otherwise fails to register it. _theria_import() { emulate -L zsh local godot; godot="$(_theria_godot)" || return 1 _theria_ok "refreshing import / class cache…" if ! "$godot" --headless --path "$_THERIA_DIR" --import >/dev/null 2>&1; then _theria_warn "import pass reported a problem — launching anyway" fi } # Launch windowed in the background (the terminal stays free), logging to # $_THERIA_LOG. Everything in argv is forwarded to the game after a bare `--`. # Imports first unless $THERIA_NO_IMPORT is set. After launch it waits a beat and # checks the process is still alive — a parse error or missing-asset boot dies # immediately, so the log tail is surfaced instead of declaring success blindly. _theria_launch() { emulate -L zsh _theria_verify || return 1 [[ -n "$THERIA_NO_IMPORT" ]] || _theria_import || return 1 local godot; godot="$(_theria_godot)" || return 1 local label="connect menu" (( $# > 0 )) && label="$*" _theria_ok "launching theria: ${label}" # `&!` backgrounds AND disowns, so the client outlives this function without a # job-table entry; $! still holds its pid for the liveness check + `run stop`. nohup "$godot" --path "$_THERIA_DIR" -- "$@" >| "$_THERIA_LOG" 2>&1 &! local pid=$! sleep 2 if ! kill -0 "$pid" 2>/dev/null; then _theria_err "client exited immediately — boot failed. Log tail:" tail -n 20 "$_THERIA_LOG" >&2 return 1 fi _theria_ok "running (pid ${pid}) — window opening. logs: $_THERIA_LOG" print -- "stop with: run stop · logs: run log" } # Build the launch flags from the dispatcher args, mirroring the skill: no args → # connect menu; a leading `--flag` → pass everything through; otherwise the first # bare word is a hero and starts a LOCAL practice match as it, keeping later flags. _theria_launch_from_args() { emulate -L zsh local -a flags if (( $# == 0 )); then flags=() elif [[ "$1" == --* ]]; then flags=("$@") else flags=(--local --hero "$1"); shift; flags+=("$@") fi _theria_launch "${flags[@]}" } # Stop the running client (matches the windowed launch by its --path argument). _theria_stop() { emulate -L zsh if pkill -f "godot.*--path ${_THERIA_DIR}" 2>/dev/null; then _theria_ok "stopped the running theria client" else _theria_warn "no running theria client found" fi } # Show the last N lines of the most recent launch log (default 40). _theria_log() { emulate -L zsh if [[ ! -s "$_THERIA_LOG" ]]; then _theria_warn "no launch log yet: $_THERIA_LOG" return 1 fi tail -n "${1:-40}" "$_THERIA_LOG" } _theria_usage() { print -- "usage: run [hero | --flags | ]" print -- " (no args) launch to the connect menu (Practice/Host/Join)" print -- " [flags] local practice as (${_THERIA_HEROES})" print -- " --host | --join | --local | --bot-difficulty easy|normal|hard | --netsim l,j,loss" print -- " pass flags straight to the client" print -- " menu explicit connect menu" print -- " import refresh Godot import / global-class cache" print -- " stop stop the running client" print -- " log [N] last N lines of the launch log (default 40)" print -- " help this text" print -- "env: THERIA_GODOT= override the godot binary" print -- " THERIA_NO_IMPORT=1 skip the pre-launch import refresh" } # run — the single public entry point (named like FlashOS's # `run`). Reserved verbs win; no hero shares a name with one, so a bare hero word # still launches a practice match. run() { emulate -L zsh case "${1:-}" in stop) _theria_stop ;; import) _theria_verify && _theria_import ;; log) shift; _theria_log "$@" ;; menu) _theria_launch ;; help|-h|--help) _theria_usage ;; *) _theria_launch_from_args "$@" ;; esac } # ── completion ──────────────────────────────────────────────────────────────── # zsh tab-completion for the `run` dispatcher: first arg offers the heroes, the # verbs, and the passthrough flags; `--join`/`--bot-difficulty` then suggest values. _theria_completion() { local -a verbs flags verbs=( 'menu:connect menu (Practice/Host/Join)' 'import:refresh Godot import / class cache' 'stop:stop the running client' 'log:tail the last launch log' 'help:usage' ) flags=( '--local:local practice match' '--host:host a listen-server' '--join:join a server by address' '--bot-difficulty:set bot skill' '--netsim:shape the link (latency,jitter,loss)' '--no-update:skip the updater' ) if (( CURRENT == 2 )); then _describe -t heroes 'hero' _THERIA_HEROES _describe -t verbs 'verb' verbs _describe -t flags 'flag' flags elif (( CURRENT == 3 )); then case "${words[2]}" in --bot-difficulty) _values 'difficulty' easy normal hard ;; --join) _message 'server address (host or ip)' ;; esac fi } if (( $+functions[compdef] )); then compdef _theria_completion run fi