#!/bin/bash

### BEGIN INIT INFO
# Provides: Factorio
# Required-Start: $local_fs $network $remote_fs
# Required-Stop: $local_fs $network $remote_fs
# Default-Start:  2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop Factorio server
# Description: A init script for Factorio, try the help command
### END INIT INFO

debug() {
  if [ "${DEBUG-0}" -gt 0 ]; then
    echo "DEBUG: $*"
  fi
}

error() {
  echo "$*" 1>&2
}

info() {
  echo "$*"
}

load_config() {
  # unless a path is provided as the first argument,
  # assume there's a "config" file in our current directory.
  config_file="${1-./config}"; shift
  debug "Trying to load config file '${config_file}'."

  # check that the file exists
  [ -f "${config_file}" ] || { error "Config file '${config_file}' does not exist!"; return 1; }
  # and we can read it
  [ -r "${config_file}" ] || { error "Unable to read config file '${config_file}'!"; return 1; }
  # then try to source it
  # shellcheck disable=SC1090
  source "${config_file}" || { error "Unable to source config file '${config_file}'!"; return 1; }

  config_defaults "$1"
  return $?
}

config_defaults() {
  command=$1
  debug "Check/Loading config defaults for command '${command}'"

  ME=$(whoami)

  if [ -z "${SERVICE_NAME}" ]; then
    SERVICE_NAME="Factorio"
  fi
  
  if [ -z "${USERGROUP}" ]; then
    USERGROUP=${USERNAME}
  fi

  if [ -z "${HEADLESS}" ]; then
    HEADLESS=1
  fi

  if [ -z "${UPDATE_EXPERIMENTAL}" ]; then
    UPDATE_EXPERIMENTAL=0
  fi  

  if [ -z "${LATEST_HEADLESS_URL}" ]; then
    if [ "${UPDATE_EXPERIMENTAL}" -gt 0 ]; then
      LATEST_HEADLESS_URL="https://www.factorio.com/get-download/latest/headless/linux64"
    else
      LATEST_HEADLESS_URL="https://www.factorio.com/get-download/stable/headless/linux64"
    fi
  fi

  if [ -z "${UPDATE_PREVENT_RESTART}" ]; then
    UPDATE_PREVENT_RESTART=0
  fi

  if [ -z "${UPDATE_PERSIST_TMPDIR}" ]; then
    UPDATE_PERSIST_TMPDIR=0
  fi

  if [ -z "${NONCMDPATTERN}" ]; then
    NONCMDPATTERN='(^\s*(\s*[0-9]+\.[0-9]+|\)|\())|(Players:$)'
  fi

  if [ -z "${FACTORIO_PATH}" ]; then
    FACTORIO_PATH="/opt/factorio"
  fi

  if [ -z "${BINARY}" ]; then
    BINARY="${FACTORIO_PATH}/bin/x64/factorio"
  fi

  if [ -z "${BINARYB}" ]; then
    BINARYB="${BINARY}"
  fi
  
  if [ -z "${ALT_GLIBC}" ]; then
    ALT_GLIBC=0
  fi

  if [ -z "${WAIT_PINGPONG}" ]; then
    WAIT_PINGPONG=0
  fi

  if [ -z "${ADMINLIST}" ]; then
    ADMINLIST="${FACTORIO_PATH}/data/server-adminlist.json"
  fi

  if [ "${ALT_GLIBC}" -gt 0 ]; then
    if [ -z "${ALT_GLIBC_DIR}" ]; then
      ALT_GLIBC_DIR="/opt/glibc-2.18"
    fi

    if [ -z "${ALT_GLIBC_VER}" ]; then
      ALT_GLIBC_VER="2.18"
    fi
    
    # flip BINARY to include alt glibc
    oldbinary="${BINARY}"
    BINARY="${ALT_GLIBC_DIR}/lib/ld-${ALT_GLIBC_VER}.so --library-path ${ALT_GLIBC_DIR}/lib ${oldbinary}"
    EXE_ARGS_GLIBC="--executable-path ${BINARYB}"
  fi
  
  if [ -z "${FCONF}" ]; then
    FCONF="${FACTORIO_PATH}/config/config.ini"
  fi

  if [ -z "${SERVER_SETTINGS}" ]; then
    SERVER_SETTINGS="${FACTORIO_PATH}/data/server-settings.json"
  fi

  if [ -z "${SAVELOG}" ]; then
    SAVELOG=0
  fi

  if [ -z "${PORT}" ]; then
    PORT=34197
  fi

  if [ -z "${INSTALL_CACHE_TAR}" ]; then
    INSTALL_CACHE_TAR=0
  fi

  if [ -z "${INSTALL_CACHE_DIR}" ]; then
    INSTALL_CACHE_DIR=/tmp/factorio-install.cache
  fi

  # perform some sanity checks in order to properly load the configuration
  case $command in
    install|help|listcommands|version|"")
      debug "Skip check/loading defaults for command '${command}'"
      ;;
    *)
      if ! [ -e "${BINARYB}" ]; then
        error "Could not find factorio binary! ${BINARYB}"
        error "(if you store your binary some place else, override BINARY='/your/path' in the config)"
        return 1
      fi
    
      if ! [ -e "${SERVER_SETTINGS}" ]; then
        error "Could not find factorio server settings file: ${SERVER_SETTINGS}"
        error "Update your config and point SERVER_SETTINGS to a modified version of data/server-settings.example.json"
        return 1
      fi
    
      if ! [ -e "${FCONF}" ]; then
        echo "Could not find factorio config file: ${FCONF}"
        echo "If this is the first time you run this script you need to generate the config.ini by starting the server manually."
        echo "(also make sure you have a save to run or the server will not start)"
        echo
        echo "Create save: sudo -u ${USERNAME} ${BINARY} --create ${FACTORIO_PATH}/saves/my_savegame ${EXE_ARGS_GLIBC}"
        echo "Start server: sudo -u ${USERNAME} ${BINARY} --start-server-load-latest ${EXE_ARGS_GLIBC}"
        echo
        echo "(If you rather store the config.ini in another location, set FCONF='/your/path' in this scripts config file)"
        return 1
      fi

      if [ -z "${WRITE_DIR}" ]; then
        # figure out the write-data path (where factorio looks for saves and mods)
        # Note - this is a hefty little operation, possible cause of head ache down the road
        # as it relies on the factorio write dir to live ../../ up from the binary if __PATH__executable__
        # is used in the config file.. for now, that's the default so cross your fingers it will not change ;)
        debug "Determining WRITE_DIR based on ${FCONF}, IF you edited write-data from the default, this probably fails"
        WRITE_DIR=$(dirname "$(grep "^write-data=" "$FCONF" |cut -d'=' -f2 |sed -e 's#__PATH__executable__#'"$(dirname ${BINARYB})"/..'#g')")
      fi
      debug "write path: $WRITE_DIR"
    
      PIDFILE="${WRITE_DIR}/server.pid"

      if [ -z "${FIFO}" ];then
        FIFO="${WRITE_DIR}/server.fifo"
      fi

      if [ -z "${CMDOUT}" ];then
        CMDOUT="${WRITE_DIR}/server.out"
      fi

      # Finally, set up the invocation
      INVOCATION="${BINARY} --config ${FCONF} --port ${PORT} --start-server-load-latest --server-settings ${SERVER_SETTINGS} --server-adminlist ${ADMINLIST}"
      if [ -n "${WHITELIST}" ] && [ -e "${WHITELIST}" ]; then
        INVOCATION+=" --server-whitelist ${WHITELIST} --use-server-whitelist"
      fi
      if [ -n "${BANLIST}" ] && [ -e "${BANLIST}" ]; then
        INVOCATION+=" --server-banlist ${BANLIST}"
      fi
      INVOCATION+=" ${EXTRA_BINARGS}"
      ;;
  esac

  return 0
}
  
usage() {
  echo -e "\
Usage: $0 COMMAND

Available commands:
  start \t\t\t\t\t\t Starts the server
  stop \t\t\t\t\t\t\t Stops the server
  restart \t\t\t\t\t\t Restarts the server
  status \t\t\t\t\t\t Displays server status
  players-online \t\t\t\t\t Shows online players
  players \t\t\t\t\t\t Shows all players
  cmd [command/message] \t\t\t\t Open interactive commandline or send a single command to the server
  chatlog [--tail|-t] \t\t\t\t\t Print the current chatlog, optionally tail the log to follow in real time
  new-game name [map-gen-settings] [map-settings] \t Stops the server and creates a new game with the specified
  \t\t\t\t\t\t\t name using the specified map gen settings and map settings json files
  save-game name \t\t\t\t\t Stops the server and saves game to specified save
  load-save name \t\t\t\t\t Stops the server and loads the specified save
  install [tarball] \t\t\t\t\t Installs the server with optional specified tarball
  \t\t\t\t\t\t\t (omit to download and use the latest headless server from Wube)
  update [--dry-run] \t\t\t\t\t Updates the server
  invocation \t\t\t\t\t\t Outputs the invocation for debugging purpose
  listcommands \t\t\t\t\t\t List all init-commands
  listsaves \t\t\t\t\t\t List all saves
  version \t\t\t\t\t\t Prints the binary version
  mod \t\t\t\t\t\t\t Manage mods (see $0 mod help for more information)
  help \t\t\t\t\t\t\t Shows this help message
"
}

as_user() {
  debug "as_user: $1}"
  if [ "$ME" == "$USERNAME" ]; then # Are we the factorio user?
    bash -c "$1"
  elif [ "$(id -u)" == "0" ]; then # Are we root?
    su "$USERNAME" -s /bin/bash -c "$1"
  else
    # To prevent odd permission behaviour, either
    # run this script as the configured user or as root
    # (please do not run as root btw!)
    echo "Run this script as the $USERNAME user!"
    exit 1
  fi
}

is_running() {
  if [ -e "${PIDFILE}" ]; then
    if kill -0 "$(cat "${PIDFILE}")" 2> /dev/null; then
      debug "${SERVICE_NAME} is running with pid $(cat "${PIDFILE}")"
      return 0
    else
      debug "Found ${PIDFILE}, but the server is not running. It's possible that your server has crashed"
      debug "Check the log for details"
      rm "${PIDFILE}" 2> /dev/null
      return 2
    fi
  fi
  return 1
}

wait_pingpong() {
  until ping -c1 pingpong1.factorio.com &>/dev/null; do :; done
  until ping -c1 pingpong2.factorio.com &>/dev/null; do :; done
}

start_service() {
  if is_running; then
      echo "${SERVICE_NAME} is already running!"
      return 1
  fi

  if ! check_permissions; then
    return $?
  fi

  # ensure we have a binary to start
  if ! [ -e "${BINARYB}" ]; then
    echo "Can't find ${BINARYB}. Please check your config!"
    return 1
  fi

  # ensure we have a fifo
  if ! [ -p "${FIFO}" ]; then
    if ! as_user "mkfifo ${FIFO}"; then
      echo "Failed to create pipe for stdin, if applicable, remove ${FIFO} and try again"
      return 1
    fi
  fi

  if [ ${SAVELOG} -eq 0 ]; then
    debug "Erasing log ${CMDOUT}"
    echo "" > "${CMDOUT}"
  fi

  if ! [ -e ${ADMINLIST} ]; then
    debug "${ADMINLIST} does not exist!  Creating empty file."
    echo "[]" > ${ADMINLIST}
    chown "${USERNAME}:${USERGROUP}" ${ADMINLIST}
  fi

  if [ ${WAIT_PINGPONG} -gt 0 ]; then
    wait_pingpong
  fi

  as_user "tail -f ${FIFO} |${INVOCATION} ${EXE_ARGS_GLIBC}>> ${CMDOUT} 2>&1 & echo \$! > ${PIDFILE}"

  if ps -p "$(cat "${PIDFILE}")" > /dev/null 2>&1; then
    echo "Started ${SERVICE_NAME}, please see log for details"
  else
    as_user "cat ${CMDOUT} |grep -v -P '^$'"
    echo -e "\nUnable to start ${SERVICE_NAME}"
    return 1
  fi
}

stop_service() {
  if [ -e "${PIDFILE}" ]; then
    echo -n "Stopping ${SERVICE_NAME}: "
    if kill -TERM "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null; then
      sec=1
      while [ "$sec" -le 15 ]; do
        if [ -e "${PIDFILE}" ]; then
          if kill -0 "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null; then
            echo -n ". "
            sleep 1
          else
            break
          fi
        else
          break
        fi
        sec=$((sec+1))
      done
    fi

    if kill -0 "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null; then
      echo "Unable to shut down nicely, killing the process!"
      kill -KILL "$(cat "${PIDFILE}" 2> /dev/null)" 2> /dev/null
    else
      echo "complete!"
    fi

    # Start the reader (in case tail stopped already)
    cat "${FIFO}" &
    # Open pipe for writing.
    exec 3> "${FIFO}"
    # Write a newline to the pipe, this triggers a SIGPIPE and causes tail to exit
    echo "" >&3
    # Close pipe.
    exec 3>&-

    rm "${PIDFILE}" 2> /dev/null
    return 0 # we've either shut down gracefully or killed the process
  else
    echo "${SERVICE_NAME} is not running (${PIDFILE} does not exist)"
    return 1
  fi
}

send_cmd(){
  NEED_OUTPUT=0
  if [ "xx$1" == "xx-o" ]; then
    NEED_OUTPUT=1
    shift
  fi
  if is_running; then
    if [ -p "${FIFO}" ]; then
      # Generate two unique log markers
      TIMESTAMP=$(date +"%s")
      START="FACTORIO_INIT_CMD_${TIMESTAMP}_START"
      END="FACTORIO_INIT_CMD_${TIMESTAMP}_END"

      # Whisper that unknown player to place start marker in log
      echo "/w $START" > "${FIFO}"
      # Run the actual command
      echo "$*" > "${FIFO}"
      # Whisper that unknown player again to place end marker in log after the command terminated
      echo "/w $END" > "${FIFO}"

      if [ ${NEED_OUTPUT} -eq 1 ]; then
        # search for the start marker in the log file, then follow and print the log output in real time until the end marker is found
        sleep 1
        awk "/Player $START doesn't exist./{flag=1;next}/Player $END doesn't exist./{exit}flag" < "${CMDOUT}"
      fi
    else
      echo "${FIFO} is not a pipe!"
      return 1
    fi
  else
    echo "Unable to send cmd to a stopped server!"
    return 1
  fi
}

cmd_players(){
  players=$(send_cmd -o "/p")
  if [ -z "${players}" ]; then
    echo "No players found!"
    return 1
  fi

  if [ "$1" == "online" ]; then
    echo "${players}" |grep -E '.+ \(online\)$' |sed -e 's/ (online)//g'
  else
    echo "${players}"
  fi
}

check_permissions(){
  if ! as_user "test -w ${WRITE_DIR}" ; then
    echo "Check Permissions. Cannot write to ${WRITE_DIR}"
    return 1
  fi

  if ! as_user "touch ${PIDFILE}" ; then
    echo "Check Permissions. Cannot touch pidfile ${PIDFILE}"
    return 1
  fi

  if ! as_user "touch ${CMDOUT}" ; then
    echo "Check Permissions. Cannot touch cmd output file ${CMDOUT}"
    return 1
  fi
}

test_deps(){
  return 0 # TODO: Implement ldd check on $BINARY
}

install(){
  # Prevent accidential overwrites
  if [ -e "${FACTORIO_PATH}" ]; then
    if [ -n "$(ls -A ${FACTORIO_PATH} 2>&1)" ]; then
      error "Aborting install, '${FACTORIO_PATH}' is not empty!"
      return 1
    fi
  fi

  tarball="$1"
  if [ -z "${tarball}" ]; then
    downloadlatest=1
  elif ! [ -e "${tarball}" ]; then
    error "Aborting install, '${tarball}' does not exist!"
    return 1
  fi

  target="${FACTORIO_PATH}"
  if ! test -w "${target}"; then
    error "Aborting install, unable to write to '${target}'!"
    return 1
  fi

  if [ "${downloadlatest}" = 1 ]; then
    # fetch a http HEAD response, we need it to know what to download later
    debug "Checking for latest headless version."
    if ! httpresponse="$(curl -LIs "${LATEST_HEADLESS_URL}" 2>&1)"; then
      info "${httpresponse}"
      error "Aborting install, unable to curl '${LATEST_HEADLESS_URL}'"
      return 1
    else
      httpstatus="$(echo "${httpresponse}" |grep HTTP |tail -n -1 |grep -oP '(?<= )\d{3}(?= )')"
      if ! [ "${httpstatus}" = "200" ]; then
        info "${httpresponse}"
        error "Aborting install, expected HTTP 200 from '${LATEST_HEADLESS_URL}', got '${httpstatus}'."
        return 1
      fi
    fi
    
    # parse the response
    filename="$(echo "${httpresponse}" |grep attachment |sed 's/\r$//' |sed -e 's/Content-Disposition: attachment; filename=//' 2>&1)"
    #location=$(echo "${httpresponse}" |grep -E '^Location: ' |tail -n -1 |sed -e 's/Location: //' 2>&1)
    debug "Found, latest version: '${filename}'"

    if [ "${INSTALL_CACHE_TAR}" = 1 ]; then
      # we want to cache the tarballs, ensure we have somewhere to save them
      if ! [ -e "${INSTALL_CACHE_DIR}" ]; then
        if ! as_user "mkdir -p \"${INSTALL_CACHE_DIR}\""; then
          error "Aborting install, unable to create cache '${INSTALL_CACHE_DIR}'."
          return 1
        fi
      fi
      if ! as_user "test -w \"${INSTALL_CACHE_DIR}\""; then
        error "Aborting install, unable to write to cache '${INSTALL_CACHE_DIR}'."
        return 1
      fi
      # we have a usable cache dir, check if there's a hit for our wanted tarball
      tarball="${INSTALL_CACHE_DIR}/${filename}"
      if [ -f "${tarball}" ]; then
        debug "Found cached '${tarball}'."
      else
        debug "No cache hit for '${filename}'."
        tarball=
      fi
    fi
  fi
  
  if [ -z "${tarball}" ]; then
    if [ "${INSTALL_CACHE_TAR}" = 1 ]; then
      tarball="${INSTALL_CACHE_DIR}/${filename}"
      if ! as_user "wget -O \"${tarball}\" \"${LATEST_HEADLESS_URL}\""; then
        error "Aborting install, unable to download & cache '${tarball}'."
        return 1
      fi
      if ! as_user "tar --strip-components 1 -xf \"${tarball}\" -C \"${target}\""; then
        error "Aborting install, unable to extract '${tarball}'."
        return 1
      fi  
    else
      if ! as_user "wget -O - \"${LATEST_HEADLESS_URL}\" |tar --strip-components 1 -xC \"${target}\""; then
        error "Aborting install, unable to download and extract tarball."
        return 1
      fi
    fi
  else
    if ! as_user "tar --strip-components 1 -xf \"${tarball}\" -C \"${target}\""; then
      error "Aborting install, unable to extract '${tarball}'."
      return 1
    fi
  fi

  # Generate default config & create a default save-game to play on
  debug "EXE_ARGS_GLIBC: ${EXE_ARGS_GLIBC}"
  if as_user "${BINARY} --create ${target}/saves/server-save ${EXE_ARGS_GLIBC}"; then
    if ! as_user "cp \"${target}/data/server-settings.example.json\" \"${target}/data/server-settings.json\""; then
      error "WARNING! Unable to copy server settings, may need to be resolved manually."
    fi
    info "Installation complete, edit '${target}/data/server-settings.json' and start your server."
    return 0
  else
    error "Failed to create save, review the output above to recover"
    return 1
  fi
}

get_bin_version(){
  as_user "$BINARY --version |egrep '^Version: [0-9\.]+' |egrep -o '[0-9\.]+' |head -n 1"
}

get_bin_arch(){
  as_user "$BINARY --version |egrep '^Binary version: ' |egrep -o '[0-9]{2}'"
}

update(){
  if ! [ -e "${UPDATE_SCRIPT}" ]; then
    echo "Failed to find update script, blatantly refusing to continue!"
    echo "Try cloning into git@github.com:narc0tiq/factorio-updater.git and set the UPDATE_SCRIPT config before you try again."
    return 1
  fi

  # Assume the user wants a dry run? (our only argument to this function)
  if [ -n "$1" ]; then
    echo "Running updater in --dry-run mode, no patches will be applied"
    dryrun=1
  else
    dryrun=0
  fi

  if [ ${HEADLESS} -gt 0 ]; then
    package="core-linux_headless$(get_bin_arch)"
  else
    package="core-linux$(get_bin_arch)"
  fi

  version=$(get_bin_version)
  if [ -z "${UPDATE_TMPDIR}" ]; then
    UPDATE_TMPDIR=/tmp
  fi

  if [ $UPDATE_PERSIST_TMPDIR -gt 0 ]; then
    # check/create tmpdir to ensure updater can download patches here
    tmpdir="${UPDATE_TMPDIR}/factorio-update"
    debug "Checking/creating update directory (persistant): ${tmpdir}"
    if ! [ -e "${tmpdir}" ]; then
      if ! as_user "mkdir -p ${tmpdir}"; then
        echo "Aborting update! Unable to create tmpdir: ${tmpdir}"
        return 1
      fi
    fi
    if ! as_user "test -w ${tmpdir}"; then
      echo "Aborting update! Unable to write to tmpdir: ${tmpdir}"
      return 1
    fi
  else
    # Create tmpdir and ensure automatic cleanup
    debug "Creating update tmpdir: ${tmpdir}"
    if ! tmpdir=$(as_user "mktemp -d -p ${UPDATE_TMPDIR} factorio-update.XXXXXXXXXX"); then
      echo "Aborting update! Unable to create tmpdir: ${tmpdir}"
      return 1
    fi
    trap 'rm -rf "${tmpdir}"' EXIT
  fi

  invocation="python3 ${UPDATE_SCRIPT} --for-version ${version} --package ${package} --output-path ${tmpdir}"
  if [ ${UPDATE_EXPERIMENTAL} -gt 0 ]; then
    invocation="${invocation} --experimental"
  fi
  if [ "${DEBUG}" -gt 0 ]; then
    invocation="${invocation} --verbose"
  fi

  if [ ${HEADLESS} -eq 0 ]; then
    #GoodGuy Wube Software allows you to download the headless for free - yay! but you still have to
    #buy the game if you want to download the sound/gfx client
    invocation="${invocation} --user ${UPDATE_USERNAME} --token ${UPDATE_TOKEN}"
  fi

  echo "Checking for updates..."
  result=$(as_user "${invocation} --dry-run")
  exitcode=$?
  if [ ${exitcode} -eq 1 ] || [ ${exitcode} -gt 2 ]; then
    debug "Invocation: ${invocation}"
    debug "${result}"
    echo "Update check failed!"
    return 1
  else
    newversion=$(echo "${result}" |grep -E '^Dry run: ' |grep -E -o '[0-9\.]+' |tail -n 1)
  fi

  if [ -z "${newversion}" ]; then
    echo "No new updates for ${package} ${version}"
    return 0
  else
    echo "New version ${package} ${newversion}"
  fi

  # Go or no Go?
  if [ ${dryrun} -gt 0 ]; then
    debug "This is a dry-run, not taking any further actions."
    # allow scripts to read return code 0 for no updates and 2 if there are updates to apply
    if [ -n "${newversion}" ]; then
      return 2
    fi
    return 0
  fi

  # Prevent update restart, ie require the server to be stopped before we apply updates
  if [ $UPDATE_PREVENT_RESTART -gt 0 ]; then
    debug "Preventing update restarts ..."
    if is_running; then
      echo "Factorio is running, aborting update - stop the server and re-run the update command!"
      return 3
    else
      debug "Server wasn't running, continue with the update."
    fi
  else
    debug "Will not prevent update restarts."
  fi

  # Time to download the updates
  if ! as_user "${invocation}"; then
    echo "Aborting update!"
    return 1
  fi

  # Stop the server if it is running.
  is_running
  was_running=$?
  if [ ${was_running} -eq 0 ]; then
    send_cmd "Updating to new Factorio version, be right back"
    stop_service
    if is_running; then
      echo "Aborting update! The server is still running."
      return 1
    fi
  fi

  for patch in $(find "${tmpdir}" -type f -name "*.zip" | sort -V); do
    echo "Applying ${patch} ..."
    result=$(as_user "$BINARY --apply-update ${patch} ${EXE_ARGS_GLIBC}")
    exitcode=$?
    if [ $exitcode -gt 0 ]; then
      echo "${result}"
      echo
      echo "Error! Failed to apply update"
      if [ $UPDATE_PERSIST_TMPDIR -gt 0 ]; then
        echo "You can try to apply it manually with:"
        echo "su ${USERNAME} -c \"${BINARY} --apply-update ${patch} ${EXE_ARGS_GLIBC}\""
      fi
      return 1
    fi
  done

  # Restarts the server if it was running
  if [ ${was_running} -eq 0 ]; then
    start_service
  fi

  echo "Successfully updated factorio"
  return 0
}

mod() {
  mod_usage() {
    cat <<EOH
Usage:
  mod update                                  Updates all installed mods
  mod (install|remove|enable|disable) MOD...  Install, remove, enable or disable all MODs
  mod list                                    List all installed mods, along with their status
Flags:
  --dry-run, -n       Don't change anything, just print actions
  --verbose, -v       Be verbose
EOH
  }
  if ! [ -e "${MOD_SCRIPT_DIR}" ]; then
    echo "Failed to find mod script, blatantly refusing to continue!"
    echo "Try cloning into https://github.com/Tantrisse/Factorio-mods-manager.git and set the MOD_SCRIPT_DIR config before you try again."
    exit 1
  fi
  mod_script="python3 ${MOD_SCRIPT_DIR} --path-to-factorio ${WRITE_DIR} --user ${UPDATE_USERNAME} --token ${UPDATE_TOKEN}"
  cmd="$1"
  shift
  declare -a args
  for arg in "$@"; do
    case "$arg" in
      "--dry-run"|"-n") mod_script="$mod_script --dry-run" ;;
      "--verbose"|"-v") mod_script="$mod_script --verbose" ;;
      *) args+=("$arg") ;;
    esac
  done
  case "$cmd" in
    update)
      $mod_script --update
      ;;
    install)
      for mod in "${args[@]}"; do
        mod_script="$mod_script --install $mod"
      done
      $mod_script
      ;;
    enable)
      for mod in "${args[@]}"; do
          mod_script="$mod_script --enable $mod"
      done
      $mod_script
      ;;
    disable)
      for mod in "${args[@]}"; do
          mod_script="$mod_script --disable $mod"
      done
      $mod_script
      ;;
    remove)
      for mod in "${args[@]}"; do
        mod_script="$mod_script --remove $mod"
      done
      $mod_script
      ;;
    list)
      $mod_script --list
      ;;
    help)
      mod_usage
      ;;
    *)
      echo "Unknown command: $cmd"
      mod_usage
      return 1
      ;;
  esac
}

run_main(){
  
  config_file=$1; shift
  command=$1

  case "${command}" in
    help|listcommands)
      debug "Skip loading config for command '${command}'"
      ;;
    *)
      load_config "${config_file}" "$*" || return $?
      ;;
  esac
  
  case "${command}" in
    start)
      start_service
      return $?
      ;;

    stop)
      # Stops the server
      if is_running; then
        send_cmd "Server is being shut down on request"
        if ! stop_service; then
          echo "Could not stop $SERVICE_NAME"
          return 1
        fi
      else
        echo "No running server."
        return 0
      fi
      ;;

    restart)
      # Restarts the server
      if is_running; then
        send_cmd "Server is being restarted on request, be right back!"
        if stop_service; then
          if ! start_service; then
            echo "Could not start $SERVICE_NAME after restart!"
            return 1
          fi
        else
          echo "Failed to stop $SERVICE_NAME, aborting restart!"
          return 1
        fi
      else
        echo "No running server to restart, starting it..."
        if ! start_service; then
          echo "Could not start $SERVICE_NAME"
          return 1
        fi
      fi
      ;;

    status)
      # Shows server status
      if is_running; then
        echo "$SERVICE_NAME is running."
      else
        echo "$SERVICE_NAME is not running."
        return 1
      fi
      ;;
    cmd)
      if [ -z "$2" ]; then
        trap 'clear' SIGTERM EXIT

        clear
        echo "Type any command or send chat messages"
        echo "This interactive commandline adds additional commands:"
        echo ""
        echo -e "\texit\t\texit the commandline"
        echo -e "\tclear\t\tclear the commandline screen"
        echo ""

        while true; do
          read -r -e -p "server@${SERVICE_NAME}> " cmd
          [ "${cmd}" == "exit" ] && return 0
          [ "${cmd}" == "clear" ] && clear && continue
          echo "${cmd}" > "${FIFO}"
          sleep 1
        done
      else
        send_cmd "${@:2}"
      fi
      ;;
    chatlog)
      case $2 in
        --tail|-t)
          tail -F -n +0 "${CMDOUT}" |grep -E -v "${NONCMDPATTERN}"
          ;;
        *)
          grep -E -v "${NONCMDPATTERN}" "${CMDOUT}"
          ;;
      esac
      ;;
    players)
      cmd_players
      ;;
    players-online|online)
      cmd_players online
      ;;
    new-game)
      if [ -z "$2" ]; then
        echo "You must specify a save name for your new game"
        return 1
      fi
      savename="${WRITE_DIR}/saves/$2"
      createsavecmd="$BINARY --create \"${savename}\""

      # Check if user wants to use custom map-gen-settings
      if [ -n "$3" ]; then
        if [ -e "${WRITE_DIR}/data/$3" ]; then
          createsavecmd="$createsavecmd --map-gen-settings=${WRITE_DIR}/data/$3"
        else
          echo "Specified map-gen-settings json file does not exist in server's /data directory"
          return 1
        fi
      fi

      # Check if user wants to use custom map-settings
      if [ -n "$4" ]; then
        if [ -e "${WRITE_DIR}/data/$4" ]; then
          createsavecmd="$createsavecmd --map-settings=${WRITE_DIR}/data/$4"
        else
          echo "Specified map-settings json file does not exist in server's /data directory"
          return 1
        fi
      fi
      
      if ! as_user "$createsavecmd ${EXE_ARGS_GLIBC}"; then
        echo "Failed to create new game"
        return 1
      else
        echo "New game created: ${savename}.zip"
      fi
      ;;

    save-game)
      savename="${WRITE_DIR}/saves/$2.zip"

      # Stop Service
      if is_running; then
        send_cmd "Stopping server to save game"
        if ! stop_service; then
          echo "Failed to stop server, unable to save as \"$2\""
          return 1
        fi
      fi

      lastsave=$(find "${WRITE_DIR}/saves" -type f -printf '%T@ %p\n' | sort -n | tail -1 | cut -f2- -d" ")
      if ! as_user "cp \"${lastsave}\" \"${savename}\""; then
        echo "Error! Failed to save game"
        return 1
      fi
      ;;

    load-save)
      # Ensure we get a new save file name
      newsave=${WRITE_DIR}/saves/$2.zip
      if [ ! -f "${newsave}" ]; then
        echo "Save \"${newsave}\" does not exist, aborting action!"
        return 1
      fi

      # Since stopping the server causes a save we have to stop the server to do this
      if is_running; then
        send_cmd "Stopping server to load a saved game"
        if ! stop_service; then
          echo "Aborting, unable to stop $SERVICE_NAME"
          return 1
        fi
      fi

      # Touch the new save file
      as_user "touch \"${newsave}\""
      ;;
    install)
      install "$2"
      return $?
      ;;
    update)
      update "$2"
      return $?
      ;;
    inv|invocation)
      echo "${INVOCATION}"
      ;;
    help)
      usage
      ;;
    listcommands)
      usage |grep -oP '(?<=  )\w+'
      ;;
    listsaves)
      find "${WRITE_DIR}"/saves -type f -name "*.zip" -exec basename {} \; |sed -e 's/.zip//'
      ;;
    version)
      get_bin_version
      ;;
    mod)
      shift
      mod "$@"
      ;;
    *)
      echo "No such command!"
      echo
      usage
      return 1
      ;;
  esac
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  if [ -L "${0}" ]; then
    config_file=$(readlink -e "$0" | sed "s:[^/]*$:config:")
  else
    # shellcheck disable=SC2001
    config_file=$(echo "$0" | sed "s:[^/]*$:config:")
  fi

  run_main "$config_file" "$@"
  exit $?
fi
