#!/usr/bin/env bash
# gamecheck.sh - alerts for your own game server: DOWN when it crashes or
# stops answering, Back UP when it returns, and (optionally) who joined.
# Quiet otherwise, so run it every minute.
#
#   ./gamecheck.sh -g minecraft -l /srv/minecraft/logs/latest.log
#   ./gamecheck.sh -g valheim -l /home/steam/valheim/server.log
#   ./gamecheck.sh -n "Factorio" -p factorio            (any game: by process)
#   ./gamecheck.sh -n "Lobby" -t 192.168.1.20:7777      (any game: by TCP port)
#
# Linux (most game servers run there). Every minute with cron (crontab -e):
#   * * * * * /home/you/gamecheck.sh -g minecraft -l /srv/minecraft/logs/latest.log
#
# Options:
#   -g GAME     preset: minecraft, valheim, terraria or palworld (sets the
#               usual process / port; minecraft and valheim also know their
#               "player joined" log line)
#   -n NAME     name used in alerts (default: the preset's, or "Game server")
#   -p PROCESS  server is up only while a process with this name is running
#               (part of the name is enough: -p valheim_server)
#   -t HOST:PORT  ...and only while it answers on this TCP port
#   -l LOGFILE  watch the server log: alerts "Steve joined" (several joins in
#               one minute come as one alert) and any -m match
#   -m REGEX    also alert on log lines matching this (e.g. -m 'Exception|crash')
#   -f N        say DOWN only after N failed checks in a row (default 2, so a
#               normal restart doesn't set it off)
#   -J          don't alert on joins (still watches -m lines)
#
# Remembers state in ~/.useralertx_gamecheck/ so it only alerts on a change.
# API key: USERALERTX_API_KEY env var, else ~/.useralertx_key (create it once
# with uxsend.sh). USERALERTX_DRYRUN=1 prints instead of sending.

URL_API="${USERALERTX_URL:-https://useralertx.com/cgi-bin/itmessages.cgi}"
NAME=""; PROC=""; HOSTPORT=""; LOG=""; MATCH=""; FAILS=2; JOINS=1; JOINRE=""

usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
while getopts "g:n:p:t:l:m:f:Jh" o; do
  case "$o" in
    g) case "$OPTARG" in
         minecraft) NAME=${NAME:-Minecraft}; HOSTPORT=${HOSTPORT:-localhost:25565}; JOINRE=': ([A-Za-z0-9_]{3,16}) joined the game' ;;
         valheim)   NAME=${NAME:-Valheim};   PROC=${PROC:-valheim_server};          JOINRE='Got character ZDOID from (.+) : ' ;;
         terraria)  NAME=${NAME:-Terraria};  HOSTPORT=${HOSTPORT:-localhost:7777} ;;
         palworld)  NAME=${NAME:-Palworld};  PROC=${PROC:-PalServer} ;;
         *) echo "gamecheck: unknown game '$OPTARG' (minecraft, valheim, terraria, palworld)"; exit 2 ;;
       esac ;;
    n) NAME="$OPTARG" ;;  p) PROC="$OPTARG" ;;  t) HOSTPORT="$OPTARG" ;;
    l) LOG="$OPTARG" ;;   m) MATCH="$OPTARG" ;;  f) FAILS="$OPTARG" ;;  J) JOINS="" ;;
    *) usage ;;
  esac
done
NAME=${NAME:-Game server}
[ -z "$PROC$HOSTPORT$LOG" ] && usage

send() {
  if [ -n "${USERALERTX_DRYRUN:-}" ]; then echo "[dry run] would send: $1"; return 0; fi
  local key="${USERALERTX_API_KEY:-}"
  [ -z "$key" ] && [ -f "$HOME/.useralertx_key" ] && key="$(tr -d '[:space:]' < "$HOME/.useralertx_key")"
  [ -z "$key" ] && { echo "gamecheck: no API key (set USERALERTX_API_KEY or run uxsend.sh once to save it)"; return 1; }
  curl -s -o /dev/null -A 'UserAlertX-example-gamecheck/1.0' -H "X-Api-Key: $key" --data-urlencode "body=$1" "$URL_API"
}

STATE_DIR="$HOME/.useralertx_gamecheck"; mkdir -p "$STATE_DIR"
ID="$STATE_DIR/$(printf '%s|%s|%s|%s' "$NAME" "$PROC" "$HOSTPORT" "$LOG" | cksum | cut -d' ' -f1)"
NOWT="$(date '+%H:%M %b %d')"

# --- up / down ----------------------------------------------------------------
if [ -n "$PROC$HOSTPORT" ]; then
  WHY=""
  # by process NAME, not command line - so this script's own "-p NAME" never counts
  if [ -n "$PROC" ] && ! pgrep -- "$PROC" > /dev/null 2>&1; then WHY="$PROC isn't running"; fi
  if [ -z "$WHY" ] && [ -n "$HOSTPORT" ]; then
    H="${HOSTPORT%:*}"; P="${HOSTPORT##*:}"
    timeout 5 bash -c "exec 3<>/dev/tcp/$H/$P" 2>/dev/null || WHY="not answering on $HOSTPORT"
  fi
  LAST=up; DOWNSINCE=0; MISSES=0
  [ -f "$ID.state" ] && read -r LAST DOWNSINCE MISSES < "$ID.state"
  if [ -z "$WHY" ]; then
    if [ "$LAST" = down ]; then
      MINS=$(( ($(date +%s) - DOWNSINCE + 30) / 60 ))
      send "$NAME server back UP on $(hostname -s) at $NOWT (down ${MINS}m)"
    fi
    echo "up 0 0" > "$ID.state"
    echo "$NAME: up"
  else
    MISSES=$((MISSES + 1))
    if [ "$LAST" = up ] && [ "$MISSES" -ge "$FAILS" ]; then
      send "$NAME server DOWN on $(hostname -s): $WHY (at $NOWT)"
      echo "down $(date +%s) $MISSES" > "$ID.state"
    else
      echo "$LAST ${DOWNSINCE:-0} $MISSES" > "$ID.state"
    fi
    echo "$NAME: $WHY (check $MISSES, DOWN after $FAILS)"
  fi
fi

# --- log: joins and -m matches ------------------------------------------------
if [ -n "$LOG" ] && [ -f "$LOG" ]; then
  SIZE=$(wc -c < "$LOG")
  if [ ! -f "$ID.pos" ]; then echo "$SIZE" > "$ID.pos"; exit 0; fi   # first run: start at the end, no old news
  POS=$(cat "$ID.pos"); [ "$SIZE" -lt "$POS" ] && POS=0                # log was rotated / restarted
  NEW="$(tail -c +$((POS + 1)) "$LOG" | head -c "$((SIZE - POS))")"
  echo "$SIZE" > "$ID.pos"
  if [ -n "$JOINS" ] && [ -n "$JOINRE" ]; then
    WHO="$(printf '%s\n' "$NEW" | sed -nE "s/.*${JOINRE}.*/\1/p" | awk '!seen[$0]++' | head -20 | paste -sd, - | sed 's/,/, /g')"
    if [ -n "$WHO" ]; then
      N=$(printf '%s' "$WHO" | awk -F', ' '{print NF}')
      [ "$N" -eq 1 ] && send "$NAME: $WHO joined ($NOWT)" || send "$NAME: $N players joined - $WHO ($NOWT)"
    fi
  fi
  if [ -n "$MATCH" ]; then
    HIT="$(printf '%s\n' "$NEW" | grep -E -- "$MATCH" | tail -1 | cut -c1-200)"
    [ -n "$HIT" ] && send "$NAME log: $HIT"
  fi
fi
