Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,31 @@ export CONNECT_PASSWORD=
# watchable via the "WATCH (HLS)" URL in the logs.
# export DEBUG_STREAM=1
# export DEBUG_STREAM_ID=debug # publish path (default: MATCH_ID)

# --- Nade lineup previews ---------------------------------------------------
# `game-streamer.sh nade-previews` connects to a nade practice server as a
# PLAYER (the practice plugin can only teleport a live pawn) and records one
# first-person clip per lineup in NADE_BATCH_JOBS. Required: NADE_BATCH_JOBS,
# CONNECT_ADDR, CONNECT_PASSWORD (the practice session password).
#
# The plugin's verbs are client commands, sw_ on SwiftlyS2 and css_ on
# CounterStrikeSharp — only SwiftlyS2 can re-emit a stored throw at all.
# export NADE_CMD_PREFIX=sw_
# export NADE_CMD_LOAD='sw_load {name}' # {name} = the lineup's name
# export NADE_CMD_THROW=sw_rethrow
# export NADE_CMD_JOIN='jointeam 3' # empty string = never join a team
#
# The recorder stops on the observed detonation, never on a timer. It reads
# the GSI grenade feed (spec-server /nade/watch) and, if the plugin is taught
# to print a line on a ghost detonation, this regex against cs2's console.log:
# export NADE_DETONATE_LOG_RE='nade_practice: detonated'
# Last resort when neither signal exists — derives the cut from the lineup's
# recorded flight time and marks the clip unverified_timing:
# export NADE_ALLOW_TIMED_DETONATION=1
#
# Window shaping (all derived from the grenade type + recorded flight time):
# export NADE_PREROLL_MS=1200 # alignment held before the throw
# export NADE_SMOKE_BLOOM_MS=2600 # smoke held to this GSI effecttime
# export NADE_INFERNO_HOLD_MS=3000 # molotov fire-spread tail
# export NADE_TAIL_MS=1200 # flash/HE/decoy tail after detonation
# export NADE_MAX_CLIP_MS=30000 # hard backstop
218 changes: 218 additions & 0 deletions src/flows/run-nades.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/usr/bin/env bash
# Launch CS2 against a nade practice server and render every lineup in
# NADE_BATCH_JOBS from that one session.
# Required env: NADE_BATCH_JOBS, CONNECT_ADDR (+ CONNECT_PASSWORD).

set -uo pipefail
SCRIPT_TAG=run-nades

# shellcheck disable=SC1091
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../lib/common.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/xorg.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/shader-cache.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/audio.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/steam.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/cs2-perf.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/cs2-options.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/cs2-tune.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/hud-manager.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/status-reporter.sh"
# shellcheck disable=SC1091
. "$LIB_DIR/snapshot.sh"

load_env
require_env NADE_BATCH_JOBS

start_status_reporter

if [ -n "${NADE_CONNECT_ADDR:-}" ]; then
CS2_CONNECT_ADDR="$NADE_CONNECT_ADDR"
CS2_CONNECT_PASSWORD="${NADE_CONNECT_PASSWORD:-}"
elif [ -n "${CONNECT_ADDR:-}" ]; then
CS2_CONNECT_ADDR="$CONNECT_ADDR"
CS2_CONNECT_PASSWORD="${CONNECT_PASSWORD:-}"
else
die "no practice server to connect to — set CONNECT_ADDR (+CONNECT_PASSWORD)"
fi

# The api resolves this via get_server_host: an SDR relay token ([A:1:...]) on
# a relay region, else host:port. A relay server answers ONLY over Steam
# datagram, so a raw ip:port here lands the client on cs2's loopback map.
log "connect target: $CS2_CONNECT_ADDR"

: "${NADE_OUT_DIR:=/tmp/game-streamer/nades}"
: "${NADE_OUTPUT_FPS:=60}"
# A render that keeps snapshotting should not throttle to the 30s "playing"
# cadence the moment GSI flows -- keep it dense so a throw is actually visible.
: "${SNAPSHOT_INTERVAL_SECONDS:=${SNAPSHOT_BOOT_INTERVAL_SECONDS:-5}}"
export SNAPSHOT_INTERVAL_SECONDS
# The capture samples cs2's swapchain, so cs2 must render ABOVE the capture
# rate for every sample to be a fresh frame — same 2x headroom the demo flow
# uses on the vkcapture path.
if vkcapture_available; then
: "${CS2_FPS_MAX:=$(( NADE_OUTPUT_FPS * 2 ))}"
else
: "${CS2_FPS_MAX:=$NADE_OUTPUT_FPS}"
fi
: "${CS2_WINDOW_TIMEOUT:=300}"
cs2_autotune

steam_pipe_up || die "Steam isn't running"
xorg_running || die "Xorg isn't up"
restore_real_steamclient

start_snapshot_loop || warn "start_snapshot_loop failed — continuing without thumbnails"

pkill -9 -f '/linuxsteamrt64/cs2' 2>/dev/null || true
sleep 1
rm -f /tmp/source_engine_*.lock
rm -f "$CS2_DIR/game/csgo/steam_appid.txt" \
"$CS2_DIR/game/bin/linuxsteamrt64/steam_appid.txt" 2>/dev/null || true

CS2_CFG_DIR="$CS2_DIR/game/csgo/cfg"
mkdir -p "$CS2_CFG_DIR" "$NADE_OUT_DIR"
write_cs2_video_cfg demo

# The clip IS the alignment reference, so the crosshair and viewmodel stay on;
# only the chrome a viewer can't act on is trimmed. cl_draw_only_deathnotices
# keeps the crosshair while dropping the rest of the HUD.
read -r -d '' NADE_VIEW_CMDS <<'EOF' || true
snd_mute_losefocus 0
engine_no_focus_sleep 0
volume 1.0
r_drawviewmodel 1
cl_draw_only_deathnotices 1
cl_showfps 0
net_graph 0
r_fullscreen_gamma 2
EOF

printf '// see nade_autoexec.cfg\n' > "$CS2_CFG_DIR/autoexec.cfg"
cat > "$CS2_CFG_DIR/nade_autoexec.cfg" <<EOF
con_enable 1
$NADE_VIEW_CMDS
$(cs2_perf_autoexec_block)
$(spec_static_binds_block)
password "$CS2_CONNECT_PASSWORD"
connect $CS2_CONNECT_ADDR
EOF

# Pre-create empty so cs2's `exec 5stack_exec` (the BACKSPACE bind spec-server
# flushes console commands through) doesn't error before the first write.
: > "$CS2_CFG_DIR/5stack_exec.cfg"

# The camera check demands a GSI reading no older than NADE_GSI_MAX_AGE_MS
# (2s) taken while the player stands still at the lineup -- which is exactly
# when cs2 stops emitting state changes. At the default 10s heartbeat the check
# is only evaluable for ~2s out of every 10, and reported "GSI is stale" for
# the rest. Pulse faster than the freshness window it is checked against.
: "${GSI_HEARTBEAT:=0.5}"
export GSI_HEARTBEAT

# A live-pawn render never receives the observer-only GSI grenade feed (cs2
# only sends allgrenades to a spectator), so the recorded flight time is the
# ONLY detonation signal available. An `exact` lineup carries an accurate
# flight time, so the clip is correctly timed even though the pod flags it
# unverified. Without this the throw step dies "no detonation signal".
: "${NADE_ALLOW_TIMED_DETONATION:=1}"
export NADE_ALLOW_TIMED_DETONATION
write_gsi_cfg

for base in libpangoft2-1.0 libpango-1.0; do
if [ ! -e "$CS2_DIR/game/bin/linuxsteamrt64/${base}.so" ] \
&& [ -e "$CS2_DIR/game/bin/linuxsteamrt64/${base}.so.0" ]; then
ln -sf "${base}.so.0" "$CS2_DIR/game/bin/linuxsteamrt64/${base}.so" || true
fi
done

CS2_BIN="$CS2_DIR/game/bin/linuxsteamrt64/cs2"
[ -x "$CS2_BIN" ] || die "CS2 binary missing at $CS2_BIN"
cd "$(dirname "$CS2_BIN")"

report_status status=launching_cs2
export PULSE_SINK="${PULSE_SINK_NAME:-cs2}"
: "${PULSE_SERVER:=tcp:${PULSE_TCP_HOST:-127.0.0.1}:${PULSE_TCP_PORT:-4713}}"
export PULSE_SERVER

do_applaunch() {
local thread_args=()
[ "${CS2_THREADS:-0}" != 0 ] && thread_args=(-threads "$CS2_THREADS")
# -condebug tees cs2's console to csgo/console.log, which is where the
# optional NADE_DETONATE_LOG_RE signal is read from.
local cs2_args=(
-windowed -noborder
-width "$CS2_WIDTH" -height "$CS2_HEIGHT"
-novid -nojoy -high -console -condebug
"${thread_args[@]}"
-disable_loadingplaque
+cl_disablehtmlmotd 1
+fps_max "$CS2_FPS_MAX"
+exec nade_autoexec
+password "$CS2_CONNECT_PASSWORD"
+connect "$CS2_CONNECT_ADDR")
export_cs2_shader_cache_env
compute_cpu_split
local cs2_pin=(); mapfile -t cs2_pin < <(cs2_cpu_pin)
if [ "${#cs2_pin[@]}" -gt 0 ]; then
log "cs2 pinned to cores ${GS_CS2_CPUS} (off the capture cores ${GS_CAPTURE_CPUS}); nproc=$(nproc)"
fi
local cmd=("${cs2_pin[@]}" "$STEAM_HOME/ubuntu12_32/steam" -applaunch 730 "${cs2_args[@]}")
spawn_logged cs2-launch "${cmd[@]}"
}
do_applaunch
wait_for_cs2_process do_applaunch

minimize_steam_windows
trim_steam_webhelper

report_status status=connecting_to_game
WIN=""
for _ in $(seq 1 "$CS2_WINDOW_TIMEOUT"); do
WIN=$(xwininfo -display "$DISPLAY" -root -tree 2>/dev/null \
| awk '/"Counter-Strike 2"/{print $1; exit}')
[ -n "$WIN" ] && break
if ! kill -0 "$CS2_PID" 2>/dev/null; then
tail -60 "$STEAM_LIBRARY/steam/logs/console-linux.txt" 2>/dev/null
die "cs2 EXITED early"
fi
sleep 1
done
[ -n "$WIN" ] || {
tail -60 "$STEAM_LIBRARY/steam/logs/console-linux.txt" 2>/dev/null
die "no CS2 window after ${CS2_WINDOW_TIMEOUT}s"
}

# Nothing reassigns X input focus after the Steam windows are destroyed, and
# every console command we send is an XTest keystroke — so cs2 must hold focus.
timeout 5 xdotool windowfocus --sync "$WIN" 2>/dev/null || true

(
while kill -0 "$CS2_PID" 2>/dev/null; do sleep 5; done
warn "cs2 (pid=$CS2_PID) exited"
command -v report_status >/dev/null 2>&1 \
&& report_status status=errored "error=cs2 process exited unexpectedly"
) &

# Keep watching. The loop normally stops here so the screen grab cannot
# compete with a clip capture -- but the connect/join/wait phase (where a
# render most often wedges) has no capture running, and a live view of it is
# the whole point while debugging. NADE_KEEP_SNAPSHOTS=0 restores the old
# stop-before-filming behaviour.
if [ "${NADE_KEEP_SNAPSHOTS:-1}" = "1" ]; then
log "snapshot: keeping the loop running through the batch (NADE_KEEP_SNAPSHOTS=1)"
else
stop_snapshot_loop
fi
# shellcheck disable=SC1091
. "$LIB_DIR/batch-nades.sh"
process_nade_jobs
exit 0
2 changes: 2 additions & 0 deletions src/flows/setup-steam.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ fi
HUD_DEFERRED=0
if [ "${CLIP_BATCH_MODE:-0}" = "1" ]; then
log "CLIP_BATCH_MODE=1 — skipping hud-manager"
elif [ "${NADE_BATCH_MODE:-0}" = "1" ]; then
log "NADE_BATCH_MODE=1 — skipping hud-manager"
elif [ -n "${BAKE_NODE_ID:-}" ]; then
log "shader bake — skipping hud-manager"
elif [ -x "$HUD_BIN" ]; then
Expand Down
7 changes: 7 additions & 0 deletions src/game-streamer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ usage: $(basename "$0") <command>
demo setup Steam + download \$DEMO_URL + play it back + capture
batch-highlights demo flow with CLIP_BATCH_MODE=1 — renders \$CLIP_BATCH_JOBS
sequentially against the same cs2 instance, then exits
nade-previews connect to a nade practice server and record \$NADE_BATCH_JOBS
(one preview clip per lineup) from that one session, then exits
warm-shaders boot CS2, run the Vulkan shader precache to completion,
then exit — pre-warms this node's cache (no match needed)
EOF
Expand Down Expand Up @@ -174,6 +176,11 @@ case "$cmd" in
export CLIP_BATCH_MODE=1
run_demo_flow "$@"
;;
nade-previews)
export NADE_BATCH_MODE=1
"$FLOWS_DIR/setup-steam.sh" "$@" || exit $?
exec "$FLOWS_DIR/run-nades.sh" "$@"
;;
warm-shaders)
# Pre-warm this node's shader cache (no match). Run as a per-node Job.
"$FLOWS_DIR/setup-steam.sh" "$@" || exit $?
Expand Down
Loading