Getting Started

Setting Up a Windows Environment for Bash Scripts

I’ve written a number of useful command-line scripts for Ark Management in Bash.
Bash is normally used in Linux environments, however these scripts can also be run in Windows using Git Bash.

This guide sets up the small environment needed to run them.

You do not need to know Linux, Git, programming, or Bash to follow this guide.

What are we installing?

There are four things:

  • Git Bash — provides the Bash environment used to run the scripts.
  • jq.exe — used by some scripts to read and modify JSON files. (Optional)
  • mcrcon.exe — used by scripts that need to communicate with an ARK server through RCON.
  • A PATH entry — tells Windows where to find these command-line programs.

None of this replaces Windows components or modifies your server configuration in any dangerous way.

1. Install Git for Windows

Download and install Git for Windows from the official website:

https://git-scm.com/download/win

Git Bash is included with Git for Windows, so there is nothing else to install for Bash.

Once installed, open Git Bash from the Windows Start menu.

You can check that it is working with:

git --version

Seeing a version number means Git is installed correctly.

2. Create a folder for command-line tools

Create a folder somewhere convenient for your scripts and utilities.

For example:

C:\Scripts

This folder will contain the small .exe utilities used by the scripts.

3. Add mcrcon.exe

Download the Windows version of mcrcon and place the executable in:

C:\Scripts

Make sure it is named:

mcrcon.exe

4. Add jq.exe (Optional)

(Optional – jq.exe is only used for the ArkShop CLI)
jq is a lightweight and flexible command-line JSON processor – it allows a user (or scripts) to interact with and manipulate JSON data, such as is found in ArkShop’s config.json

Download the Windows version of jq and rename the executable to:

jq.exe

Place it in:

C:\Scripts

5. Add C:\Scripts to your PATH

Windows needs to know that command-line programs can be found in C:\Scripts.

This is safe and simple. You are not replacing Windows files or changing system settings. You are simply adding one folder to the list of locations Windows checks when a command is entered.

Follow the separate guide:

Adding a Folder to the Windows PATH

7. That’s it

Your Windows server now has the same basic command-line environment that I use for my Bash scripts.

Individual scripts may require additional configuration, but the underlying environment is now ready.

You do not need to install Linux or learn Git to use the scripts.

Git Bash & Editing your Server’s PATH to include your Scripts Directory

Adding a Folder to the Windows PATH

This is a simple and safe Windows setting. You are not changing or replacing Windows system files – you are simply telling Windows where it can look for command-line programs.

For ArkShop, this allows you to place jq.exe in a folder such as C:\Scripts and have the arkshop command find it automatically.

1. Open Environment Variables

Open the Windows Start menu and search for environment.

Select Edit environment variables for your account.

2. Open your User PATH

In the Environment Variables window, look at the User variables section at the top.

Select Path, then click Edit.

Make sure you are editing the User variables section, not the System variables section.

3. Add the folder

Click New and enter the folder containing your jq.exe.

For example:

C:\Scripts

Do not change or delete any of the existing entries. You are simply adding one new entry to the list.

Click OK, then OK again to close the Environment Variables windows.

That’s it

This does not install anything, remove anything, or modify Windows itself. It simply adds one folder to the list of places Windows checks when you type a command.

Open a new terminal window and test it with:

jq --version

If jq is installed correctly, you should see its version number.

Nothing else needs to be changed.

Rcon CLI


Editor’s Note:
For this CLI I’m just using mcrcon as it’s a pretty standard & lightweight cross-platform rcon tool.
If you’ve got a better rcon tool, go ahead and use that, in my experience they all tend to be functionally the same tool anyway.

Obviously, before use one must setup the global variables at the top of the script:
HostIP
Rcon tool Location – in this instance I’m using mcrcon.exe living in a directory I’ve created and added to the Windows PATH. (Windows PATH Guide)
Servers – In the array on line-55 we set the server names & RCON ports
RconPass – in the array on line 63 we set the server RCON Passwords
if all passwords are different, then define each here, otherwise there is a global variable at the top which you can use instead.

Dependencies:
Not strictly a dependency, however in the rcon -commands output one can see a number of ‘commonly used commands’ and a list of ‘extended rcon commands’
These refer to plugins which we use within our cluster as part of the AkrAPI package:
– Arkshop
– Permissions
– ExtendedRcon
I recommended ExtendedRcon especially, though at the time of writing this it is in dire need of a number of updates – some commands I have greyed out, as they instantly crash a server in it’s current state.



Description:
A oneshot rcon CLI for sending rcon commands to single or multiple servers at once.

Options:
 rcon -help, -h           – This help menu
 rcon -commands, -c       – List of useful & extended commands

 rcon listall, -la             – Return a list of all currently online players
 rcon all, -a <command>        – Send a command to ALL servers

 rcon ball, -ba <message>      – Send Broadcast to ALL servers
 rcon chatall, -ca <message>   – Send ServerChat to ALL servers
 rcon notall, -na <message>    – Send Notification to ALL servers

General rcon use:
 Usage:     rcon <server> <command>
 Servers:   los | rag | gen | ext | isl | ast

Examples:
 rcon ast listplayers
 rcon la
 rcon isl saveworld
 rcon gen broadcast “Server restarting in 10 minutes”
 rcon -ba This is a clusterwide broadcast


Download rcon.sh

#!/bin/bash
#
# One-shot RCON command sender
#

####################
# RCON CONFIGURATION
#####################
HOST="YOUR-HOST-IP"
MCRCON="$HOME/mcrcon/mcrcon"
RconPass="YOUR-RCON-PASSWORD"


##########
# ARRAYS
###########
declare -A PORTS=(
    [isl]=0000
    [rag]=0000
    [abe]=0000
    [ext]=0000
    [ast]=0000
    [gen]=0000
)
declare -A PASSWORDS=(
    [isl]="$RconPass"
    [rag]="$RconPass"
    [abe]="$RconPass"
    [ext]="$RconPass"
    [ast]="$RconPass"
    [gen]="$RconPass"
)

servers=$(printf "%s | " "${!PORTS[@]}")
servers=${servers% | } # Remove the trailing " | "

# remove leading dash (so -rag works) <-- do we need this?
key="${1#-}"

#
# END CONFIGURATION
#

###################
# Global Variables
##################
VERSION="Odium's Rcon CLI v3.5.1"

####################
# COLOUR FORMATTING
#####################
RESET="\033[0m"
BOLD="\033[1m"

CLR_RED="\033[38;5;196m"        # Bright Red
CLR_GREEN="\033[38;5;46m"       # Bright Lime Green
CLR_GOLD="\033[38;5;228m"         # Gold
CLR_PURPLE="\033[38;5;141m"       # Light Purple / Lavender
CLR_BLUE="\033[38;5;81m"         # Bright Sky Blue
CLR_DGREEN="\033[38;5;2m"         # Dark Forest Green
CLR_ORANGE="\033[38;5;208m"       # Orange
CLR_DGREY="\033[38;5;243m"        # dark Grey
CLR_TEXT="\033[38;5;254m"         # Light Grey
CLR_PROMPT="${BOLD}\033[38;5;51m" # Aqua + Bold
CLR_FRAME="\033[38;5;14m"         # Teal
CLR_TITLE="\033[38;5;147m"        # Pale Purple

set -e

#######################
# Unicode box Chars
########################
H="${CLR_FRAME}─${RESET}"
V="${CLR_FRAME}│${RESET}"

TL="${CLR_FRAME}┌"
TR="┐${RESET}"
BL="${CLR_FRAME}└"
BR="┘${RESET}"

LT="${CLR_FRAME}├"
RT="┤${RESET}"
TT="┬"
BT="┴"
CR="┼"



#############
# FUNCTIONS
##############
error() {
    echo
    echo -e "${CLR_ERROR}  Error: ${RESET}$1"
    echo
}

usage() {
    echo -e "${CLR_TITLE}${BOLD}${VERSION}${RESET}"
    echo
    echo -e "${BOLD}Options:${RESET}"
    echo -e "${CLR_TEXT}  rcon -help, -h           - This help menu"
    echo -e "  rcon -commands, -c       - List of useful & extended commands"
    echo
    echo -e "  rcon listall, -la             - Return a list of all currently online players"
    echo -e "  rcon all, -a <command>        - Send a command to ALL servers"
    echo
    echo -e "  rcon ball, -ba <message>      - Send Broadcast to ALL servers"
    echo -e "  rcon chatall, -ca <message>   - Send ServerChat to ALL servers"
    echo -e "  rcon notall, -na <message>    - Send Notification to ALL servers${RESET}"
    echo
    echo -e "${BOLD}General rcon use:${RESET}"
    echo -e "${CLR_GOLD}  Usage:${RESET}${CLR_TEXT}     rcon <server> <command>"
    echo -e "${CLR_GOLD}  Servers:${RESET}   ${servers}"
    echo
    echo -e "${BOLD}Examples:${RESET}"
    echo -e "${CLR_TEXT}  rcon ast listplayers"
    echo -e "  rcon la"
    echo -e "  rcon isl saveworld"
    echo -e '  rcon gen broadcast "Server restarting in 10 minutes"'
    echo -e "  rcon -ba This is a clusterwide broadcast"

    echo -e "${RESET}"
    exit 0
}

send_rcon() {
    local server="$1"
    shift
    local command="$*"
    local result

    result=$(
        "$MCRCON" \
            -H "$HOST" \
            -P "${PORTS[$server]}" \
            -p "${PASSWORDS[$server]}" \
            "$command" 2>&1 |
            sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g'
    )

    printf '%s\n' "$result"
}

cmd_rcon_all() {
    # Broadcast to all servers

    local bstatus
    local command="$*"

    echo
    echo -e "${CLR_GOLD}  ▶ Sending command to ALL servers${RESET}"
    echo
    echo "────────────────────"
    for servername in "${!PORTS[@]}"; do
        echo -e "${CLR_LABEL}${BOLD}${servername^^}${RESET}"
        bstatus=$(send_rcon "$servername" "${command}")
        if [[ "$bstatus" == *"Server received, But no response!!"* ]]; then
            echo "▶ Command Delieverd"
            echo
            continue
        fi
        echo "$bstatus"
    done
    echo "────────────────────"
    echo
    exit 0
}

arrcon_send_rcon() {
    local server="$1"
    local command="$2"
    local result

    result=$(
        "$MCRCON" \
            -H "$HOST" \
            -P "${PORTS[$server]}" \
            -p "${PASSWORDS[$server]}" \
            -Q \
            "$command" 2>&1 |
            sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g'
    )
    printf '%s\n' "$result"
}

cmd_showcommands() {
    echo -e "$(
        cat <<EOF

              ${CLR_GOLD}${BOLD}Common Commands${RESET}
              ───────────────

  rcon <map> ListPlayers
  rcon <map> KickPlayer <SteamID64>

  rcon <map> AddPoints <EOSID> <Points>
  rcon <map> SetPoints <EOSID> <Points>
  rcon <map> GetPlayerPoints <EOSID>

  rcon <map> permissions.add <EOSID> <Group>
  rcon <map> permissions.Remove <EOSID> <Group>


                      ${CLR_GOLD}${BOLD}Extended RCON Commands${RESET}
                      ──────────────────────

${BOLD}Get Commands             List Commands              Set Commands${RESET}
────────────────────     ────────────────────       ────────────────────
GetActorPos              ListAllPlayerPos           SetDinoColor
GetAllActorsPos          ListAllPlayerEOSId         SetDinoPos
GetAllActors             ListAllStructures          SetFacialHair
GetAllActorsBp           ListAllStructuresInRadius  SetHeadHair
GetAllDinos              ListOnlineTribes           SetImprintQuality
GetDinoBP                ListOnlinePlayers          SetImprintToPlayer
GetDinoPos               ListPlayerDinos            SetPlayerName
GetInGameTime            ListTribeDinos             SetPlayerPos
GetPlayerName            ListTribePlayers           SetPlayerTribeAdmin
GetPlayerPos             ListTribeStructures        SetPlayerTribeOwner
GetServerFrames          ListUnclaimedDinos         SetTribeName
GetTotalTames            ListUnclaimedDinosAmount
GetTotalWilds
GetTribeIdOfPlayer       ${BOLD}Other                     Kill & Kick${RESET}
GetTribeLog              ────────────────────      ────────────────────
GetTribeName             TribeChatMsg              KickAllPlayers
GetTribeStructureCount   TribeLogMsg               KillAllPlayers
GetTribeStructurePos     UnlockBoss                KillDino
GetMapName               UnlockEngram              KillPlayer
GetOnlineNum             SpawnDinoBP               KillPlayerId
                                                   KillUnclaimedDinos
                                                   KillAllWildDinos

${BOLD}Give Commands            Teleport                  Destroy Commands${RESET}
────────────────────     ────────────────────      ──────────────────────────
GiveDinoXP               ${CLR_DGREY}TeleportAllPlayers${RESET}        DestroyActorAll
GivePlayerXP             ${CLR_DGREY}TeleportToPlayer${RESET}          DestroyActorWild
${CLR_DGREY}GiveHexagon${RESET}                                        DestroyTribeAll
GiveArmorSet             ${BOLD}Send Commands${RESET}             DestroyTribeDinos
GiveItemIndex            ───────────────────       DestroyTribePlayers
GiveItemIndexToAll       SendMessageToChat         DestroyTribeStructures
GiveItemToAll            SendMessageToNotification DestroyTribeStructuresByBP
GiveItemToEOSId          SendPlayerScriptCommand   DestroyPlayerProfile
GiveTribeDinosFood       SendScriptCommand

EOF
    )"
    exit 0
}

cmd_list() {
    # List all players

    local PlayersList
    totalPlayers=0

    echo
    echo -e "${CLR_GOLD}  ▶ Listing all connected players${RESET}"
    echo
    for servername in "${!PORTS[@]}"; do
        # Calculate total online on cluster
        onlineNum=$(send_rcon "$servername" getonlinenum)
        totalPlayers=$((totalPlayers + onlineNum))

        PlayersList=$(send_rcon "$servername" listplayers)
        if [[ "$PlayersList" == *"No Players Connected"* ]]; then
            continue
        fi

        echo -e "${TL}─────────────────────────────${TR}"
        printf "${V}${BOLD}${CLR_LABEL}             %-15s${RESET} ${V}\n" "${servername^^}"
        echo -e "${BL}─────────────────────────────${BR}"

        echo "$PlayersList"
    done
    echo
    echo " ──────────────────────────"
    echo -e "  Total Players Online: ${CLR_GOLD}${totalPlayers}${RESET}"
    echo " ──────────────────────────"
    echo
    exit 0
}

cmd_ball() {
    # Broadcast to all servers

    local bstatus
    local message="$*"

    echo
    echo -e "${CLR_GOLD}  ▶ Broadcasting to all servers${RESET}"
    echo
    echo "────────────────────"
    for servername in "${!PORTS[@]}"; do
        echo -e "${CLR_LABEL}${servername^^}${RESET}"
        bstatus=$(send_rcon "$servername" broadcast "${message}")
        if [[ "$bstatus" == *"Server received, But no response!!"* ]]; then
            echo "▶ Brodcast Delieverd"
            echo
            continue
        fi
    done
    echo "────────────────────"
    echo
    exit 0
}

cmd_chatall() {
    # Serverchat to all servers
    local chstatus
    local message="$*"

    echo
    echo -e "${CLR_ACTION}  ▶ Sending ServerChat to all servers${RESET}"
    echo
    echo "────────────"
    for servername in "${!PORTS[@]}"; do
        echo -e "${CLR_LABEL}${servername^^}${RESET}"
        echo "$message"
        chstatus=$(send_rcon "$servername" serverchat "${message}")
        if [[ "$chstatus" == *"Server received, But no response!!"* ]]; then
            echo "▶ Serverchat Delieverd"
            echo
            continue
        fi
    done
    echo "────────────────────"
    echo
    exit 0
}

cmd_notifyall() {
    # Serverchat to all servers
    local notstatus
    local message="$*"

    echo
    echo -e "${CLR_ACTION}  ▶ Sending Notification to all servers${RESET}"
    echo
    echo "────────────"
    for servername in "${!PORTS[@]}"; do
        echo -e "${CLR_LABEL}${servername^^}${RESET}"
        echo "$message"
        chstatus=$(send_rcon "$servername" SendMessageToNotification "${message}")
        if [[ "$chstatus" == *"Server received, But no response!!"* ]]; then
            echo "▶ Notification Delieverd"
            echo
            continue
        fi
    done
    echo "────────────────────"
    echo
    exit 0
}

#############
# COMMANDS
##############

if [[ $# -eq 0 ]]; then
    usage
    exit 0
fi

case "${1:-}" in
    -commands | -c)
        shift
        cmd_showcommands "$@"
        exit 0
        ;;
    listall | -la | la)
        shift
        cmd_list "$1"
        exit 0
        ;;
    ball | -ball | -ba)
        shift
        cmd_ball "$@"
        exit 0
        ;;
    chatall | -ca)
        shift
        cmd_chatall "$@"
        exit 0
        ;;
    notall | -na)
        shift
        cmd_notifyall "$@"
        exit 0
        ;;
    all | -a)
        shift
        cmd_rcon_all "$@"
        exit 0
        ;;
    help | -h)
        usage
        exit 0
        ;;
esac

# If $1 wasn't a flag, it must be a valid server
if [[ -z ${PORTS[$1]+x} ]]; then
    echo
    error "Unknown server or command: '$1'"
    echo
    usage
    exit 1
fi

##########
# MAIN
##########

# get credentials
key="$1"
port="${PORTS[$key]}"
password="${PASSWORDS[$key]}"

# remove server argument
shift

# no command supplied
if [[ $# -eq 0 ]]; then
    echo
    error "No Command Supplied"
    echo
    echo -e "${CLR_GOLD}  Usage:${RESET}     rcon <server> <command>"
    echo -e "${CLR_GOLD}  Servers:${RESET}   ${servers}"
    echo
    exit 1
fi

# build command string
command="$*"

# execute
"$MCRCON" \
    -H "$HOST" \
    -P "$port" \
    -p "$password" \
    "$command"

Viewlog Log-Streamer CLI

Description:
 View a number of live log feeds from the entire cluster in a single stream
 Or just a single server, should you prefer.
 Further capabilities allow searching all archived logs for upto 2 keywords at a time.

 Also view a number of other logs depending on your cluster’s features
 including Raffle Logs, API Logs, as well as viewing and searching your ArkShop Logs.

Usage:

   viewlog -a                  – View this session’s ServerLogs (All Maps) (LIVE)
   viewlog -o <map>            – View this session’s ServerLog (One Map) (LIVE)
   viewlog -s <search>         – Search ALL EXISTING Serverlogs (All Maps)
   viewlog -s1 <map> <search>  – Search ALL EXISTING Serverlogs (All Maps)

   viewlog -as            – View All ArkShop Logs (LIVE)
   viewlog -sas <search>  – Search All ArkShop logs

   viewlog -r             – View the Raffle Logs (LIVE)
   viewlog -w             – View the Raffle Winners Log  

   viewlog -api           – View this session’s ArkAPI Logs  

 Map Keys:
   Abe Ext Rag Ast Isl Gen  

 Examples:
   viewlog -a
   viewlog -o ast
   viewlog -s Player
   viewlog -s Player Froze
   viewlog -sas Astraeos

Download viewlog.sh

#!/bin/bash
# tail logfiles
#
####################
# CONFIGURATION
####################

# Raffle Logfile Locations
Raffle_log="/c/Ark/Raffle/raffle-log.txt"
Raffle_win="/c/Ark/Raffle/raffle-winners.txt"

# ARK SERVERS
declare -A SERVER_PATHS=(
    [Island]="/C/ARK/Island"
    [Ragnarok]="/C/ARK/Ragnarok"
    [Aberration]="/C/ARK/Aberration"
    [Extinction]="/C/ARK/Extinction"
    [Astraeos]="/C/ARK/Astraeos"
    [Genesis]="/C/ARK/Genesis"
)

# Server Title Colours
BOLD="\033[1m"

CLR_ISL="${BOLD}\033[38;5;105m"
CLR_RAG="${BOLD}\033[38;5;143m"
CLR_ABE="${BOLD}\033[38;5;79m"
CLR_EXT="${BOLD}\033[38;5;175m"
CLR_AST="${BOLD}\033[38;5;191m"
CLR_GEN="${BOLD}\033[38;5;189m"

declare -A SERVER_COLOURS=(
    [Island]="$CLR_ISL"
    [Ragnarok]="$CLR_RAG"
    [Aberration]="$CLR_ABE"
    [Extinction]="$CLR_EXT"
    [Astraeos]="$CLR_AST"
    [Genesis]="$CLR_GEN"
)

#
# END CONFIGURATION
#

VERSION="Viewlog v3.2.1"

####################
# DATA ARRAYS
####################

declare -A CURRENT_LOGS
declare -A SHOPLOGS
declare -A API_LOGS

shopt -s nullglob

ALL_LOGS=()

for map in "${!SERVER_PATHS[@]}"; do

    path="${SERVER_PATHS[$map]}"

    CURRENT_LOGS[$map]="$path/ShooterGame/Saved/Logs/ShooterGame.log"

    ALL_LOGS+=("$path/ShooterGame/Saved/Logs/"*.log)

    SHOPLOGS[$map]="$path/ShooterGame/Binaries/Win64/ArkApi/Plugins/ArkShop/"

    API_LOGS[$map]="$path/ShooterGame/Binaries/Win64/logs"

done

shopt -u nullglob

# Generate map keys from first 3 letters of map-name - eg: extinction > ext
all=""
for map in "${!CURRENT_LOGS[@]}"; do
    all+="${map:0:3} "
done

####################
# COLOUR VARIABLES
####################

# Terminal Formatting
RESET="\033[0m"


# UI Colours
CLR_RED="\033[38;5;196m"  # Bright Red
CLR_GREEN="\033[38;5;46m" # Bright Lime Green

CLR_GOLD="\033[38;5;226m"         # Gold
CLR_PURPLE="\033[38;5;141m"       # Light Purple / Lavender
CLR_BLUE="\033[38;5;4m"           # Bright Sky Blue
CLR_DGREEN="\033[38;5;2m"         # Dark Forest Green
CLR_ORANGE="\033[38;5;208m"       # Orange
CLR_AQUA="\033[38;5;51m"          # Aqua
CLR_PINK="\033[38;5;199m"         # PINK
CLR_TEXT="\033[38;5;250m"         # Light Grey
CLR_TITLE="\033[38;5;147m"        # Pale Blue
CLR_SAVE="\033[38;5;245m"         # Darker Grey
CLR_PAQUA="\033[38;5;195m"        # Pale Aqua
CLR_LEFT="\033[38;5;9m"           # Pale Red
CLR_HIDE="\033[38;5;234m"         # Almost Black
CLR_FROZEN="\033[38;5;147m"       # Pale Purple
CLR_TAMED="\033[38;5;146m"

set -e

####################
# FUNCTIONS
####################
usage() {
    echo
    echo -e "${CLR_TITLE}${BOLD}${VERSION}${RESET}"
    echo
    echo -e "${BOLD}Description:${RESET}"
    echo -e "  ${CLR_TEXT}View a number of live log feeds from the entire cluster in a single stream"
    echo -e "  ${CLR_TEXT}Or just a single server, should you prefer."
    echo -e "  ${CLR_TEXT}Further capabilities allow searching all archived logs for upto 2 keywords at a time."
    echo
    echo -e "  ${CLR_TEXT}Also view a number of other logs depending on your cluster's features"
    echo -e "  ${CLR_TEXT}including Raffle Logs, API Logs, as well as viewing and searching your ArkShop Logs.${RESET}"
    echo
    echo -e "${BOLD}Usage:${RESET}"
    echo
    echo -e "  ${CLR_TEXT}  viewlog -a                  - View this session's ServerLogs (All Maps)${CLR_GREEN} (LIVE)${CLR_TEXT}"
    echo -e "  ${CLR_TEXT}  viewlog -o <map>            - View this session's ServerLog (One Map)${CLR_GREEN} (LIVE)${CLR_TEXT}"
    echo -e "  ${CLR_TEXT}  viewlog -s <search>         - Search ${BOLD}ALL EXISTING${RESET}${CLR_TEXT} Serverlogs (All Maps)"
    echo -e "  ${CLR_TEXT}  viewlog -s1 <map> <search>  - Search ${BOLD}ALL EXISTING${RESET}${CLR_TEXT} Serverlogs (All Maps)"
    echo
    echo -e "  ${CLR_TEXT}  viewlog -as            - View All ArkShop Logs ${CLR_GREEN}(LIVE)${CLR_TEXT}"
    echo -e "  ${CLR_TEXT}  viewlog -sas <search>  - Search All ArkShop logs"
    echo
    echo -e "  ${CLR_TEXT}  viewlog -r             - View the Raffle Logs ${CLR_GREEN}(LIVE)"
    echo -e "  ${CLR_TEXT}  viewlog -w             - View the Raffle Winners Log ${RESET}"
    echo
    echo -e "  ${CLR_TEXT}  viewlog -api           - View this session's ArkAPI Logs ${RESET}"
    echo
    echo -e "  ${BOLD}Map Keys:${CLR_TEXT}"
    echo -e "    $all${RESET}"
    echo
    echo -e "  ${BOLD}Examples:${RESET}${CLR_TEXT}"
    echo -e "    viewlog -a"
    echo -e "    viewlog -o ast"
    echo -e "    viewlog -s Player"
    echo -e "    viewlog -s Player Froze"
    echo -e "    viewlog -sas Astraeos${RESET}"

    exit 1
}

#
# Server Logs
#
cmd_all() {
    echo
    echo "Loading Live Feed..."
    echo -e "${CLR_AQUA}${BOLD}  CTRL + C to Exit${RESET}"
    sleep 1
    tail -n 50 -F "${CURRENT_LOGS[@]}" | colour_logs
}

cmd_one() {
    echo
    echo "Loading Live Feed..."
    echo -e "${CLR_AQUA}${BOLD}  CTRL + C to Exit${RESET}"
    sleep 1

    shopt -s nocasematch
    for map in "${!CURRENT_LOGS[@]}"; do
        if [[ "${map:0:3}" == "$1" ]]; then
            server="${CURRENT_LOGS[$map]}"
            tail -n 500 -F "$server" | colour_logs
            shopt -u nocasematch
            return
        fi
    done
    shopt -u nocasematch

    echo "Unknown map: $1"
    exit 1
}

cmd_search() {
    echo "Searching ALL existing logs..."
    sleep 2

    if [[ -n $2 ]]; then
        grep -H -i --color=always "$1" "${ALL_LOGS[@]}" |
            grep -i --color=always "$2" |
            sed 's/<RichColor Color="1, 0, 0, 1">//g'
    else
        grep -H -i --color=always "$1" "${ALL_LOGS[@]}" |
            sed 's/<RichColor Color="1, 0, 0, 1">//g'
    fi
}

cmd_search_one() {
    local map_key="$1"
    local search="$2"

    echo "Searching ${map_key^^} logs..."
    sleep 1

    shopt -s nocasematch

    for map in "${!CURRENT_LOGS[@]}"; do
        if [[ "${map:0:3}" == "$map_key" ]]; then
            logdir="$(dirname "${CURRENT_LOGS[$map]}")"

            grep -H -i --color=always "$search" "$logdir"/*.log 2>/dev/null |
                sed 's/<RichColor Color="1, 0, 0, 1">//g'

            shopt -u nocasematch
            return
        fi
    done

    shopt -u nocasematch

    echo "Unknown map: $map_key"
    exit 1
}

#
# ArkAPI Logs
#
cmd_latest_api() {
    for server in "${!API_LOGS[@]}"; do
        logfile=$(ls -t "${API_LOGS[$server]}"/ArkApi_*.log 2>/dev/null | head -n1)

        echo -e "${CLR_DGREEN}───────────────────────────────────────────────────────────────────────────────────────────────────"
        echo -e "${BOLD}${CLR_AQUA}${server^^}:${RESET}${BOLD} $logfile ${RESET}"
        echo -e "${CLR_DGREEN}───────────────────────────────────────────────────────────────────────────────────────────────────${RESET}"
        cat "$logfile"
        echo -e "${CLR_PURPLE}──────────────────────────────────────────────── END LOG ───────────────────────────────────────────────────${RESET}"
        echo
    done
}

#
# Raffle logs
#
cmd_raffle() {
    echo
    echo "Loading Live Raffle Log..."
    echo -e "${CLR_AQUA}${BOLD}  CTRL + C to Exit${RESET}"
    sleep 1
    tail -n 100 -F "$Raffle_log" | colour_logs
}

cmd_winners() {
    echo
    echo "Loading Raffle Winners Log..."
    echo -e "${CLR_AQUA}${BOLD}  CTRL + C to Exit${RESET}"
    sleep 1
    tail -n 200 -F "$Raffle_win" | colour_logs
}

#
# Shop Logs
#
cmd_shop_logs() {
    echo
    echo "Loading Live Feed..."
    echo -e "${CLR_AQUA}${BOLD}  CTRL + C to Exit${RESET}"
    sleep 1
    tail -n 500 -F "${SHOPLOGS[@]}"/*.log | colour_shop_logs
}

cmd_shop_search() {
    echo "Searching existing logs..."
    sleep 1

    local logs=()

    for dir in "${SHOPLOGS[@]}"; do
        logs+=("$dir"/*.log)
    done

    grep -H -i --color=never "$1" "${logs[@]}" | colour_shop_logs
}

####################
# OUTPUT COLOURING
####################
colour_shop_logs() {
    while IFS= read -r line; do
        line=$(sed -E 's|.*/ShopLog_([^/]+)_WP\.log:|\1: |' <<<"$line")
        echo "$line"
    done
}

colour_logs() {
    shopt -s nocasematch

    while IFS= read -r line; do
        #Remove Richcolor Spam
        line=$(sed -E 's/<RichColor[^>]*>//g; s#</RichColor>##g' <<<"$line")

        line=$(sed -E 's|.*/ShopLog_([^/]+)_WP\.log:|\1: |' <<<"$line")

        # Server Log path to Simple Server Name
        for map in "${!SERVER_PATHS[@]}"; do
            path="${SERVER_PATHS[$map]}"

            if [[ "$line" == *"$path"* ]]; then
                echo -e "${SERVER_COLOURS[$map]}[$map]${RESET}"
                continue 2
            fi
        done

        # Remaining server activity colour-coding
        case $line in

            # Server Activity
            *ERROR*)    echo -e "${CLR_RED}$line${RESET}";;
            *warning*)    echo -e "${CLR_GOLD}$line${RESET}";;
            *info*)    echo -e "${CLR_DGREEN}$line${RESET}";;
            *Loaded\ Plugin*)    echo -e "${CLR_DGREEN}$line${RESET}";;

            # Player Activity
            *Tamed\ a*)    echo -e "${CLR_TAMED}$line${RESET}";;
            *FROZEN*)    echo -e "${CLR_FROZEN}$line${RESET}";;
            *KILLED*)   echo -e "${CLR_ORANGE}$line${RESET}";;
            *TRIBE*)    echo -e "${CLR_PAQUA}$line${RESET}";;
            *JOINED*)   echo -e "${CLR_GREEN}$line${RESET}";;
            *incoming*)   echo -e "${CLR_GREEN}$line${RESET}";;
            *LEFT*)     echo -e "${CLR_LEFT}$line${RESET}";;
            *upload\ terminal*)     echo -e "${CLR_LEFT}$line${RESET}";;

            # Server State & Map Updates
            *ADVERTISING*)      echo -e "${BOLD}${CLR_GOLD}$line${RESET}";;
            *SHUTDOWN*) echo -e "${BOLD}${CLR_PINK}$line${RESET}";;
            *WORLD*) echo -e "${CLR_SAVE}$line${RESET}";;
            *Garbage\ Collection*) echo -e "${CLR_HIDE}$line${RESET}";;

            # Genesis Mission
            *MISSION*) echo -e "${CLR_PURPLE}$line${RESET}";;

            # Raffle Logs
            *RAFFLE\ INITIATED*)    echo -e "${CLR_GOLD}$line${RESET}";;
            *Active\ Servers*)    echo -e "${CLR_AQUA}$line${RESET}";;
            *Excluded\ player*)    echo -e "${BOLD}${CLR_ORANGE}$line${RESET}";;
            *Querying\ players*)    echo -e "${CLR_PAQUA}$line${RESET}";;
            *Entry:*)               echo -e "${CLR_PAQUA}$line${RESET}";;
            *▶\ Winner*)               echo -e "${CLR_GREEN}$line${RESET}";;
            *▶*)               echo -e "${CLR_PURPLE}$line${RESET}";;
            *Eligible\ Players*)    echo -e "${BOLD}${CLR_AQUA}$line${RESET}";;
            *\ \-\ WINNER:*)    echo -e "${BOLD}${CLR_GREEN}$line${RESET}";;
            *sent\ to*)    echo -e "${BOLD}${CLR_GREEN}$line${RESET}";;
            *RAFFLE\ COMPLETE*)    echo -e "${CLR_GOLD}$line${RESET}";;

            *) echo "$line" ;;
        esac

    done

    shopt -u nocasematch
}

####################
# COMMANDS
####################
case "$1" in
    -o | --one)
        shift
        cmd_one "$1"
        ;;

    -s | --search)
        shift
        cmd_search "$@"
        ;;
    -s1 | --search-one)
        shift
        cmd_search_one "$@"
        ;;

    -a | --all)
        cmd_all "$@"
    ;;
    -r | --raffle)
        cmd_raffle "$@"
        ;;
    -w | --winners)
        cmd_winners "$@"
        ;;
    -as | --arkshop)
        cmd_shop_logs "$@"
        ;;
    -sas | --searcharkshop)
        shift
        cmd_shop_search "$@"
        ;;
    -api | --api)
        cmd_latest_api
        ;;
    *)
        usage
        ;;
esac

IDLookup CLI

Description:
Quickly lookup any player’s Username, EOSID and First & Last time seen
 as well as automatically copy the full EOSID to Clipboard (when run locally)
 Can search using Partial Playername OR Partial EOSID
Furthermore, provides capability to lookup a Player’s Tribe ID’s
across the entire cluster.

Commands:
idlookup <player>     ────  Search the database for a user’s EOSID  
idlookup -t <player>  ────  Search the database for a user’s TribeID  

idlookup -b           ────  Build EOSID database from ALL available logs  
 idlookup -bt          ────  Build Tribe database from ALL available logs

Examples:
   idlookup wolf
   idlookup -t wolf

Download idlookup.sh

#!/bin/bash
#
#
set -e
####################
# CONFIGURATION
####################

# DESIRED LOCAL DB PATHS
EOSID_DIR="/c/ark/eosid/"

DB_FILE="${EOSID_DIR}eosdb.csv"
TRIBE_DB="${EOSID_DIR}tribedb.csv"

EOSID_SCRAPED="${EOSID_DIR}eosid_scraped_logs.txt"
TRIBE_SCRAPED="${EOSID_DIR}tribe_scraped_logs.txt"

# LOCAL CLIPBOARD
CLIPBOARD="clip.exe"

# ARK SERVERS
#
# Format:
# "Map Name|Server Root Path"
#
SERVERS=(
    "Island|/C/ARK/Island"
    "Ragnarok|/C/ARK/Ragnarok"
    "Aberration|/C/ARK/Aberration"
    "Extinction|/C/ARK/Extinction"
    "Astraeos|/C/ARK/Astraeos"
    "Genesis|/C/ARK/Genesis"
)

####################
# END CONFIGURATION
####################

VERSION="EOSID Lookup v4.2.2"

####################
# DATA ARRAYS
####################

shopt -s nullglob
ALL_LOGS=()
for server in "${SERVERS[@]}"; do
    path="${server#*|}"
    ALL_LOGS+=("$path/ShooterGame/Saved/Logs/"*.log)
done
shopt -u nullglob

declare -A DB_SteamName
declare -A DB_SurvivorName
declare -A DB_FirstSeen
declare -A DB_LastSeen
declare -A DB_TimesSeen

####################
# COLOUR VARIABLES
####################

# Terminal Formatting
RESET="\033[0m"
BOLD="\033[1m"
# UI Colours
CLR_RED="\033[38;5;196m"    # Bright Red
CLR_GREEN="\033[38;5;46m"   # Bright Lime Green
CLR_GOLD="\033[38;5;226m"   # Gold
CLR_PURPLE="\033[38;5;141m" # Light Purple / Lavender
CLR_BLUE="\033[38;5;4m"     # Bright Sky Blue
CLR_DGREEN="\033[38;5;2m"   # Dark Forest Green
CLR_ORANGE="\033[38;5;208m" # Orange
CLR_AQUA="\033[38;5;51m"    # Aqua
CLR_PINK="\033[38;5;199m"   # PINK
CLR_PAQUA="\033[38;5;195m"  # Pale Aqua
CLR_TEXT="\033[38;5;252m"   # Light Grey
CLR_TITLE="\033[38;5;147m"  # Pale Blue
CLR_SAVE="\033[38;5;245m"   # Darker Grey

####################
# FUNCTIONS
####################

usage() {
    echo
    echo -e "${CLR_TITLE}${BOLD}${VERSION}${RESET}"
    echo
    echo -e "${BOLD}Description:${RESET}${CLR_TEXT}"
    echo -e "  Quickly lookup any player's Username, EOSID and First & Last time seen"
    echo -e "  as well as automatically copy the full EOSID to Clipboard"
    echo -e "  Can search using Partial Playername OR Partial EOSID"
    echo -e "  Furthermore, provides capability to lookup a Player's Tribe IDs across the entire cluster.${RESET}"
    echo
    echo -e "${BOLD}Usage:${RESET}${CLR_TEXT}"
    echo -e "  idlookup <Player>     ────  Search the database for a user's EOSID "
    echo -e "  idlookup -t <Player>  ────  Search the database for a user's TribeID "
    echo
    echo -e "  idlookup -b           ────  Build EOSID database from ALL available logs "
    echo -e "  idlookup -bt          ────  Build Tribe database from ALL available logs${RESET}"
    echo
    echo -e "${BOLD}Examples:${RESET}${CLR_TEXT}"
    echo -e "  idlookup wolf"
    echo -e "  idlookup -t wolf${RESET}"
    echo

    exit 1
}

#
# EOSID BUILD
#

cmd_build() {
    if [[ ! -f "$EOSID_SCRAPED" ]]; then
        echo
        echo -e "${CLR_GOLD}${BOLD}Building EOSID Database...${RESET}"
        echo "  Depending on the quantity of your archived logs, the first scan may take several minutes..."
        echo
        echo "  ${BOLD}Be Advised:${RESET} Once this first scan has completed, only new logs will be scanned in the future"
        echo "  this will ${BOLD}dramatically${RESET} reduce the processing time moving forward - from minutes to seconds."
    else
        echo -e "${CLR_GOLD}${BOLD} ▶ Scanning new logs & appending the EOSID Database...${RESET}"
    fi

    #
    # Load existing EOSID database
    #
    if [[ -f "$DB_FILE" ]]; then
        while IFS=',' read -r eosid steam survivor firstseen lastseen timesseen; do
            [[ "$eosid" == "EOSID" ]] && continue

            DB_SteamName["$eosid"]="$steam"
            DB_SurvivorName["$eosid"]="$survivor"
            DB_FirstSeen["$eosid"]="$firstseen"
            DB_LastSeen["$eosid"]="$lastseen"
            DB_TimesSeen["$eosid"]="$timesseen"
        done <"$DB_FILE"
    fi

    # cross-reference the all_logs array with the scaped_logs.txt - remove already scanned logs and add leftovers to
    # a new array NEW_LOGS
    touch "$EOSID_SCRAPED"

    declare -A SCRAPED_LOGS

    while IFS= read -r log; do
        SCRAPED_LOGS["$log"]=1
    done <"$EOSID_SCRAPED"

    NEW_LOGS=()

    for log in "${ALL_LOGS[@]}"; do
        if [[ -z ${SCRAPED_LOGS["$log"]+x} ]]; then
            NEW_LOGS+=("$log")
        fi
    done
    echo
    echo "───────────────────────────"
    echo "Total logs found: ${#ALL_LOGS[@]}"
    echo "New logs to scan:   ${#NEW_LOGS[@]}"
    echo "───────────────────────────"
    if [[ ${#NEW_LOGS[@]} -eq 0 ]]; then
        echo
        echo "No new logs to process."
        echo -e "${CLR_GREEN}All up to date!${RESET}"
        return
    fi

    #
    # PASS 1 - SteamName -> EOSID
    #
    while IFS= read -r line; do

        if [[ $line =~ ([0-9]{4}\.[0-9]{2}\.[0-9]{2}_[0-9]{2}\.[0-9]{2}\.[0-9]{2}):[[:space:]](.+)[[:space:]]\[UniqueNetId:([[:xdigit:]]+) ]]; then

            timestamp="${BASH_REMATCH[1]}"
            player="${BASH_REMATCH[2]}"
            eosid="${BASH_REMATCH[3]}"

            DB_SteamName["$eosid"]="$player"

            if [[ -z ${DB_FirstSeen[$eosid]} || "$timestamp" < "${DB_FirstSeen[$eosid]}" ]]; then
                DB_FirstSeen["$eosid"]="$timestamp"
            fi

            if [[ -z ${DB_LastSeen[$eosid]} || "$timestamp" > "${DB_LastSeen[$eosid]}" ]]; then
                DB_LastSeen["$eosid"]="$timestamp"
            fi

            ((++DB_TimesSeen["$eosid"]))
        fi

    done < <(
        grep -hEi "joined this ARK|left this ARK" "${NEW_LOGS[@]}"
    )

    #
    # PASS 2 - Survivor -> EOSID
    #
    local LastSurvivor=""

    while IFS= read -r line; do

        if [[ $line =~ :[[:space:]]([^:]+)[[:space:]]froze[[:space:]] ]]; then
            LastSurvivor="${BASH_REMATCH[1]}"
            continue
        fi

        if [[ $line =~ Frozen[[:space:]]by[[:space:]]ID:[[:space:]]([[:xdigit:]]+) ]]; then
            eosid="${BASH_REMATCH[1]}"

            if [[ -n "$LastSurvivor" ]]; then
                DB_SurvivorName["$eosid"]="$LastSurvivor"
            fi

            LastSurvivor=""
        fi

    done < <(
        grep -hEi "froze |Frozen by ID:" "${NEW_LOGS[@]}"
    )

    #
    # PASS 3 - Historical chat fallback
    #
    while IFS= read -r line; do

        if [[ $line =~ :[[:space:]](.+)[[:space:]]\(([^()]+)\):[[:space:]] ]]; then
            player="${BASH_REMATCH[1]}"
            survivor="${BASH_REMATCH[2]}"

            for eosid in "${!DB_SteamName[@]}"; do
                if [[ "${DB_SteamName[$eosid]}" == "$player" && -z "${DB_SurvivorName[$eosid]}" ]]; then
                    DB_SurvivorName["$eosid"]="$survivor"
                    break
                fi
            done
        fi

    done < <(
        grep -hE " \([^()]+\): " "${NEW_LOGS[@]}"
    )

    # Record logs that have been scraped
    for log in "${NEW_LOGS[@]}"; do
        if ! grep -Fxq "$log" "${EOSID_SCRAPED}"; then
            echo "$log" >>"${EOSID_SCRAPED}"
        fi
    done

    echo
    echo "────────────────────────────────────"
    echo -e "${CLR_GREEN} ▶ Finished Building EOSID Database!${RESET}"
    echo "────────────────────────────────────"

    cmd_export
}

cmd_export() {
    {
        echo "EOSID,SteamName,SurvivorName,FirstSeen,LastSeen,TimesSeen"

        for eosid in "${!DB_SteamName[@]}"; do
            echo "$eosid,${DB_SteamName[$eosid]},${DB_SurvivorName[$eosid]},${DB_FirstSeen[$eosid]},${DB_LastSeen[$eosid]},${DB_TimesSeen[$eosid]}"
        done

    } >"$DB_FILE"
}

#
# TRIBE
#

cmd_tribebuild() {

    if [[ ! -f "$TRIBE_SCRAPED" ]]; then
        echo
        echo -e "${CLR_GOLD}${BOLD}Building Tribe Database...${RESET}"
        echo "  Depending on the quantity of your archived logs, the first scan may take several minutes..."
        echo
        echo "  ${BOLD}Be Advised:${RESET} Once this first scan has completed, only new logs will be scanned in the future"
        echo "  this will ${BOLD}dramatically${RESET} reduce the processing time moving forward - from minutes to seconds."
    else
        echo -e "${CLR_GOLD}${BOLD} ▶ Scanning new logs & appending the Tribe Database...${RESET}"
    fi

    # cross-reference the all_logs array with the scaped_logs.txt - remove already scanned logs and add leftovers to
    # a new array NEW_LOGS
    touch "$TRIBE_SCRAPED"
    declare -A SCRAPED_LOGS

    while IFS= read -r log; do
        SCRAPED_LOGS["$log"]=1
    done <"$TRIBE_SCRAPED"

    NEW_LOGS=()

    for log in "${ALL_LOGS[@]}"; do
        if [[ -z ${SCRAPED_LOGS["$log"]+x} ]]; then
            NEW_LOGS+=("$log")
        fi
    done
    echo
    echo "───────────────────────────"
    echo "Total logs found: ${#ALL_LOGS[@]}"
    echo "New logs to scan:   ${#NEW_LOGS[@]}"
    echo "───────────────────────────"
    if [[ ${#NEW_LOGS[@]} -eq 0 ]]; then
        echo
        echo "No new logs to process"
        echo -e "${CLR_GREEN}All up to date!${RESET}"
        return
    fi

    declare -A TribeRecords
    declare -A JoinRecords

    #
    # Load existing Tribe database
    #
    if [[ -f "$TRIBE_DB" ]]; then
        while IFS=',' read -r map player playerid tribe tribeid; do
            [[ "$map" == "Map" ]] && continue

            key="$map|$player"

            TribeRecords["$key"]="$map,$player,$playerid,$tribe,$tribeid"

            if [[ "$playerid" != "Unknown" ]]; then
                JoinRecords["$key"]=1
            fi
        done <"$TRIBE_DB"
    fi

    #
    # PASS 1: Authoritative "joined tribe" records
    #
    while IFS= read -r line; do

        map=""

        for server in "${SERVERS[@]}"; do
            server_map="${server%%|*}"
            server_path="${server#*|}"

            if [[ "$line" == *"$server_path"* ]]; then
                map="$server_map"
                break
            fi
        done

        [[ -n "$map" ]] || continue

        joinline="${line%% Invited by Player *}"

        if [[ $joinline =~ Player[[:space:]](.+)[[:space:]]with[[:space:]]ID[[:space:]]([0-9]+)[[:space:]]joined[[:space:]]tribe[[:space:]](.+)[[:space:]]with[[:space:]]ID[[:space:]]([0-9]+)\. ]]; then

            player="${BASH_REMATCH[1]}"
            playerid="${BASH_REMATCH[2]}"
            tribe="${BASH_REMATCH[3]}"
            tribeid="${BASH_REMATCH[4]}"

            key="$map|$player"

            TribeRecords[$key]="$map,$player,$playerid,$tribe,$tribeid"
            JoinRecords[$key]=1
        fi

    done < <(grep -HE "joined tribe .* with ID [0-9]+" "${NEW_LOGS[@]}")

        #
    # PASS 2: Cryopod fallback
    #
    declare -A TribeLastSeen=()

    while IFS= read -r line; do

        map=""

        for server in "${SERVERS[@]}"; do
            server_map="${server%%|*}"
            server_path="${server#*|}"

            if [[ "$line" == *"$server_path"* ]]; then
                map="$server_map"
                break
            fi
        done

        [[ -n "$map" ]] || continue

        if [[ $line =~ \[([0-9]{4}\.[0-9]{2}\.[0-9]{2}-[0-9]{2}\.[0-9]{2}\.[0-9]{2}):[0-9]+\].*Tribe[[:space:]](.+),[[:space:]]ID[[:space:]]([0-9]+):.*:[[:space:]]([^:]+)[[:space:]]froze[[:space:]] ]]; then

            timestamp="${BASH_REMATCH[1]}"
            tribe="${BASH_REMATCH[2]}"
            tribeid="${BASH_REMATCH[3]}"
            player="${BASH_REMATCH[4]}"

            key="$map|$player"

            # Only accept the newest cryopod event for this player.
            if [[ -z ${TribeLastSeen[$key]:-} || "$timestamp" > "${TribeLastSeen[$key]}" ]]; then

                TribeLastSeen["$key"]="$timestamp"

                # Preserve authoritative PlayerID when we already have one.
                if [[ -n ${TribeRecords[$key]:-} ]]; then
                    IFS=',' read -r old_map old_player old_playerid old_tribe old_tribeid <<< "${TribeRecords[$key]}"
                    TribeRecords[$key]="$map,$player,$old_playerid,$tribe,$tribeid"
                else
                    TribeRecords[$key]="$map,$player,Unknown,$tribe,$tribeid"
                fi

            fi
        fi

    done < <(
        grep -HE "Tribe .* ID [0-9]+:.* froze " "${NEW_LOGS[@]}"
    )

    #
    # Record logs that have been scraped
    #
    for log in "${NEW_LOGS[@]}"; do
        if ! grep -Fxq "$log" "$TRIBE_SCRAPED"; then
            echo "$log" >>"$TRIBE_SCRAPED"
        fi
    done

    #
    # Export
    #
    {
        echo "Map,Player,PlayerID,TribeName,TribeID"

        for key in "${!TribeRecords[@]}"; do
            echo "${TribeRecords[$key]}"
        done | sort

    } >"$TRIBE_DB"

    echo
    echo "────────────────────────────────────"
    echo -e "${CLR_GREEN} ▶ Finished Building Tribes Database!${RESET}"
    echo "────────────────────────────────────"
}

#
# SEARCHING
#

cmd_search() {
    local query="$1"
    local found=0

    if [[ ! -f "$DB_FILE" ]]; then
        echo
        echo -e "${CLR_RED}${BOLD} Error:${RESET} EOSID database does not exist:${CLR_TEXT} ${DB_FILE} ${RESET}"
        echo
        echo -e " ▶ Run ${BOLD}idlookup -b${RESET} to scan logfiles and poopulate the database"
        echo
        exit 1
    fi

    echo
    echo "────────────────────────────────────────────────"

    while IFS=',' read -r eosid steam survivor firstseen lastseen timesseen; do

        # Skip the CSV header
        [[ $eosid == "EOSID" ]] && continue

        shopt -s nocasematch

        if [[ $eosid == *"$query"* || $steam == *"$query"* || $survivor == *"$query"* ]]; then

            printf "%s" "$eosid" | $CLIPBOARD

            echo -e "  ${BOLD}Steam${RESET}      : $steam"
            echo -e "  ${BOLD}Survivor${RESET}   : $survivor"
            echo -e "  ${BOLD}EOSID${RESET}      : $eosid"
            echo
            echo -e "  ${BOLD}First Seen${RESET} : $firstseen"
            echo -e "  ${BOLD}Last Seen${RESET}  : $lastseen"
            echo -e "  ${BOLD}Times Seen${RESET} : $timesseen"
            echo "────────────────────────────────────────────────"

            found=1
        fi

        shopt -u nocasematch

    done <"$DB_FILE"

    if [[ $found -eq 0 ]]; then
        echo "No matches found."
    else
        echo
        echo -e " ${CLR_GOLD}▶ EOSID copied to clipboard.${RESET}"
    fi

    echo
}

cmd_tribesearch() {
    local query="$1"
    local player=""
    local playerid=""
    local steam=""
    local survivor=""

    # Try to resolve query via EOS database
    if [[ -f "$DB_FILE" ]]; then

        # First: look for an exact Steam/Survivor/EOSID match
        while IFS=',' read -r eosid db_steam db_survivor firstseen lastseen timesseen; do
            [[ "$eosid" == "EOSID" ]] && continue

            shopt -s nocasematch
            if [[ "$db_steam" == "$query" || "$db_survivor" == "$query" || "$eosid" == "$query" ]]; then
                steam="$db_steam"
                survivor="$db_survivor"
                break
            fi
            shopt -u nocasematch
        done < "$DB_FILE"

        # If no exact match, fall back to partial matching
        if [[ -z "$steam" ]]; then
            while IFS=',' read -r eosid db_steam db_survivor firstseen lastseen timesseen; do
                [[ "$eosid" == "EOSID" ]] && continue

                shopt -s nocasematch
                if [[ "$db_survivor" == *"$query"* || "$db_steam" == *"$query"* || "$eosid" == *"$query"* ]]; then
                    steam="$db_steam"
                    survivor="$db_survivor"
                    break
                fi
                shopt -u nocasematch
            done < "$DB_FILE"
        fi
    fi

    if [[ ! -f "$TRIBE_DB" ]]; then
        echo
        echo -e "${CLR_RED}${BOLD} Error:${RESET} Tribe database does not exist:${CLR_TEXT} ${TRIBE_DB} ${RESET}"
        echo
        echo -e " ▶ Run ${BOLD}idlookup -bt${RESET} to scan logfiles and poopulate the Tribes database"
        echo
        exit 1
    fi

    #
    # If EOS lookup found a player, use their Steam name.
    # Otherwise use the original query.
    #
    [[ -n "$steam" ]] || steam="$query"

    #
    # Find matching player in Tribe DB
    #
    while IFS=',' read -r map db_player db_playerid db_tribe tribeid; do
        [[ "$map" == "Map" ]] && continue

        shopt -s nocasematch

        if [[ "$db_player" == "$steam" ]]; then
            player="$db_player"
            playerid="$db_playerid"
            break
        fi

        shopt -u nocasematch

    done <"$TRIBE_DB"

    shopt -u nocasematch

    if [[ -z "$player" ]]; then
        echo
        echo "No player matches found."
        echo
        return
    fi

    #
    # Display player
    #
    echo
    echo "──────────────────────────────"
    echo -e "${CLR_GOLD}Steam       ${RESET}: $steam"
    echo -e "${CLR_GOLD}Survivor    ${RESET}: $survivor"
    echo -e "${CLR_GOLD}Player ID   ${RESET}: $playerid"
    echo "──────────────────────────────"
    echo -e "${CLR_AQUA}Tribe: ${RESET}"

    #
    # Display every map's tribe + tribe ID
    #
    while IFS=',' read -r map db_player db_playerid db_tribe tribeid; do
        [[ "$map" == "Map" ]] && continue

        if [[ "$db_player" == "$player" ]]; then
            printf "  %-12s : %-18s : %s\n" "$map" "$db_tribe" "$tribeid"
        fi

    done <"$TRIBE_DB"

    echo "──────────────────────────────"
}

####################
# COMMANDS
####################

case "${1:-}" in
    -b)
        cmd_build
        ;;
    -bt)
        cmd_tribebuild
        ;;
    -t)
        [[ -n ${2:-} ]] || usage
        cmd_tribesearch "$2"
        ;;
    "")
        usage
        ;;
    -*)
        usage
        ;;
    *)
        cmd_search "$1"
        ;;
esac

Advert Manager CLI

Description:
Broadcast a random advert to your entire Ark Cluster.
List, Add, Delete or Send your adverts with ease.

Using Task Scheduler(windows) or a cron job(linux) easily schedule a broadcast at regular
intervals (eg: every 60 mins)

Usage:
 advert <command> [arguments]

Commands:
 list,   -ls          List all adverts
 add,    -a  <text>   Add a new advert
 del,    -d  <id>     Delete an advert
 chat,   -ch          Broadcast a random advert to all servers
 help,   -h           Show this help

Examples:
 advert list
 advert add “Join our Discord: https://discord.gg/fYZqNQVqhA”
 advert -a “Vote daily at ScruffyPVE.DuckDNS.org”
 advert del 3
 advert chat

Download advert.sh

#!/bin/bash
#
#

######################
# SERVER CONFIGURATION
#######################
HOST="YOUR-CLUSTER-IP"
RconPass="YOUR-RCON-PASS"

MCRCON="/c/mcrcon/mcrcon.exe"
DATA_FILE="/c/ark/adverts/advert.csv"
DELIM="%"

################
# SERVER ARRAYS
#################
declare -A PORTS=(
    [isl]=1234
    [rag]=1234
    [abe]=1234
    [ext]=1234
    [ast]=1234
    [gen]=1234
)
declare -A PASSWORDS=(
    [isl]="$RconPass"
    [rag]="$RconPass"
    [abe]="$RconPass"
    [ext]="$RconPass"
    [ast]="$RconPass"
    [gen]="$RconPass"
)
servers=$(printf "%s | " "${!PORTS[@]}")
servers=${servers% | } # Remove the trailing " | "

#
# END CONFIGURATION
#

VERSION="Odi's Advert Manager 1.4"

####################
# COLOUR VARIABLES
####################

# Terminal Formatting
RESET="\033[0m"
BOLD="\033[1m"

# UI Colours
CLR_RED="\033[38;5;196m"  # Bright Red
CLR_GREEN="\033[38;5;46m" # Bright Lime Green

CLR_GOLD="\033[38;5;226m"   # Gold
CLR_PURPLE="\033[38;5;141m" # Light Purple / Lavender
CLR_BLUE="\033[38;5;4m"     # Bright Sky Blue
CLR_ORANGE="\033[38;5;208m" # Orange
CLR_AQUA="\033[38;5;51m"    # Aqua
CLR_PINK="\033[38;5;199m"   # PINK
CLR_PBLUEE="\033[38;5;147m" # Pale Blue
CLR_DGREEN="\033[38;5;2m"   # Dark Forest Green
CLR_PAQUA="\033[38;5;195m"  # Pale Aqua
CLR_GREY="\033[38;5;245m"   # Darker Grey
CLR_TEXT="\033[38;5;252m"   # Light Grey
CLR_FRAME="\033[38;5;245m"  # Darker Grey

set -e

#######################
# Unicode box Chars
########################
H="─"
V="│"

TL="┌"
TR="┐"
BL="└"
BR="┘"

LT="├"
RT="┤"
TT="┬"
BT="┴"
CR="┼"

TH_FRAME="${CLR_FRAME}${TL}────${TT}────────────────────────────────────────────────────────────────────────────────────────────────────────────${TR}${RESET}"
MD_FRAME="${CLR_FRAME}${LT}────${CR}────────────────────────────────────────────────────────────────────────────────────────────────────────────${RT}${RESET}"
BH_FRAME="${CLR_FRAME}${BL}────${BT}────────────────────────────────────────────────────────────────────────────────────────────────────────────${BR}${RESET}"

ROW_PREFIX="${CLR_FRAME}${V}${RESET}"

####################
# FUNCTIONS
####################

usage() {
    echo -e "${BOLD}${VERSION}${RESET}"
    echo
    echo -e "${BOLD}Description:${RESET}"
    echo -e "${CLR_TEXT}  Manage and broadcast random server adverts to your entire Ark Cluster.${RESET}"
    echo
    echo -e "${BOLD}Usage:${RESET}"
    echo -e "  ${CLR_TEXT}advert <command> [arguments]${RESET}"
    echo

    echo -e "${BOLD}Commands:${RESET}"
    echo -e "${CLR_TEXT}  list,   -ls          List all adverts"
    echo -e "  add,    -a  <text>   Add a new advert"
    echo -e "  del,    -d  <id>     Delete an advert"
    echo -e "  chat,   -ch          Broadcast a random advert to all servers"
    echo -e "  help,   -h           Show this help${RESET}"
    echo

    echo -e "${BOLD}Examples:${RESET}"
    echo -e "${CLR_TEXT}  advert list"
    echo -e "  advert add \"Join our Discord: https://discord.gg/fYZqNQVqhA\""
    echo -e "  advert -a \"Vote daily at ScruffyPVE.DuckDNS.org\""
    echo -e "  advert del 3"
    echo -e "  advert chat${RESET}"
    echo
}

error() {
    echo
    echo -e "  ${CLR_RED}Error: ${RESET}$1"
    echo
}

advert_action() {
    echo
    echo "────────────────────────"
    echo -e " ▶ ${1}Advert (${RESET}${BOLD}${2}${RESET}${1}) ${3}${RESET}"
    echo "────────────────────────"
}

next_id() {

    if [[ ! -s "$DATA_FILE" ]]; then
        echo 1
        return
    fi

    awk -F'%' '
        $1 > max { max = $1 }
        END { print max + 1 }
    ' "$DATA_FILE"
}

send_rcon() {
    local server="$1"
    shift
    local command="$*"
    local result

    result=$(
        "$MCRCON" \
            -H "$HOST" \
            -P "${PORTS[$server]}" \
            -p "${PASSWORDS[$server]}" \
            "$command" 2>&1 |
            sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g'
    )

    printf '%s\n' "$result"
}

cmd_list() {

    [[ -f "$DATA_FILE" ]] || touch "$DATA_FILE"

    local FMT="$ROW_PREFIX %2b $ROW_PREFIX %-121b $ROW_PREFIX\n"

    echo
    echo -e "${TH_FRAME}"

    printf "$FMT" "${CLR_ORANGE}ID${RESET}" "${CLR_ORANGE}ADVERTISEMENT${RESET}"
    printf "%b" "$RESET"
    echo -e "${MD_FRAME}"

    if [[ ! -s "$DATA_FILE" ]]; then
        echo " No adverts"
        echo -e "${BH_FRAME}"
        echo
        return
    fi

    local first=1

    while IFS="$DELIM" read -r id advert; do

        ((first)) || echo -e "${MD_FRAME}"
        first=0

        printf "$ROW_PREFIX ${CLR_GOLD}%2b${RESET} $ROW_PREFIX %s \n" \
            "$id" "$advert"

    done <"$DATA_FILE"

    echo -e "${BH_FRAME}"
    echo
}

cmd_add() {
    local id advert
    id=$(next_id)
    advert="$*"

    echo "${id}${DELIM}${advert}" >>"$DATA_FILE"

    advert_action "${CLR_GREEN}" "$id" "Created"
}

cmd_delete() {
    local delete_id="$1"

    #
    # Ensure the ID is numerical & Exists
    #

    # If Blank
    if [[ -z "$delete_id" ]]; then
        error "No Ticket ID Specified"
        echo "  Usage: advert -d <id>"
        echo
        return 1
    fi

    # IF input isn't a number
    if [[ ! $delete_id =~ ^[0-9]+$ ]]; then
        error "${delete_id} is not a numerical value"
        exit
    fi

    # If delete_id is 5, this searches specifically for "^5%"
    # to ensure the delete_id exists in $DATA_FILE
    if grep -q "^${delete_id}${DELIM}" "$DATA_FILE"; then

        #
        # Finish checks - continue Deleting
        #

        while IFS="$DELIM" read -r id advert; do
            [[ "$id" == "$delete_id" ]] && continue
            echo "${id}${DELIM}${advert}"
        done <"$DATA_FILE" >"$DATA_FILE.tmp"

        mv "$DATA_FILE.tmp" "$DATA_FILE"

        advert_action "${CLR_RED}" "$delete_id" "Deleted"

    else
        error "Advert '$delete_id' does not exist."
    fi
}

cmd_chatall() {

    [[ -s "$DATA_FILE" ]] || {
        error "No adverts available."
        exit 1
    }

    # Pick a random advert
    local line id message chstatus

    line=$(shuf -n1 "$DATA_FILE")
    id="${line%%${DELIM}*}"
    message="${line#*${DELIM}}"

    echo
    echo -e "${CLR_AQUA} ▶ Sending Advert #${id} to all servers${RESET}"
    echo
    echo "────────────────────────"
    echo -e "${CLR_GOLD} $message ${RESET}"
    echo "────────────────────────"
    echo

    for servername in "${!PORTS[@]}"; do
        echo -e "${CLR_PURPLE}${servername^^}${RESET}"

        chstatus=$(send_rcon "$servername" serverchat "$message")

        if [[ "$chstatus" == *"Server received, But no response!!"* ]]; then
            echo "▶ Advert Delivered"
        else
            echo "$chstatus"
        fi
        echo
    done

    echo "────────────────────────"
    echo
}

####################
# COMMANDS
####################
case "${1:-}" in
    list | -ls)
        cmd_list
        ;;
    add | -a)
        shift
        cmd_add "$@"
        ;;
    delete | del | -d)
        shift
        cmd_delete "$1"
        ;;
    chat | -ch)
        cmd_chatall
        exit 0
        ;;
    help | -h)
        usage
        ;;
    *)
        usage
        exit 1
        ;;
esac

Automated Raffle CLI


Editor’s Note:
For our cluster, I simply setup a Scheduled Task to run every 90 minutes, which executed the ‘raffle’ command within a git-bash environment, running a fully automated periodic raffle.

Optional Setup: Excluded Players
Should you wish to exclude specific players, such as admins, you can enter their EOSID’s in the config under the excluded players heading, so they are excluded from winning the raffle

Dependencies:

Extended Rcon – Because Wildcard’s implementation of giving items via console/rcon is COMPLETELY broken, the Extended Rcon plugin is a requirement so the raffle script can utilize it’s “GiveItemToEOSId” Rcon command.
(newer version available via the developer’s discord)

mcrcon – or whichever Rcon agent you prefer



Description:
CLI for running and maintaining raffles within an Ark Ascended Cluster.
 Raffle will first check for 5 or more players, if met, a raffle is run
 Flags to force a raffle regardless of online players are available

Options:
 raffle         ────  Run a regular raffle (Required: 5 Players)  
 raffle -f      ────  Force a raffle, regardless of how many online players  

 raffle -l      ────  View the last 150 lines of the Raffle log  
 raffle -w      ────  View the last 100 lines of the Winners Log  

 raffle -v      ────  Check Raffle CLI Version

Examples:
 raffle
raffle -f

Download raffle.sh

#!/usr/bin/env bash
#
#

set -euo pipefail

#######################
# RAFFLE CONFIGURATIONS
#######################
serverIP="YOUR-SERVER-IP"
mcrcon="/c/Ark/mcrcon/mcrcon.exe"
rconpass="YOUR-RCON-PASS"

# Minimum numbers of players required for a raffle to be drawn
min_players="5"

logFile="/c/Ark/Logs/raffle-log.txt"
winnerLog="/c/Ark/Logs/raffle-winners.txt"

#################################
# SERVER ARRAYS
#################################
SERVERS=(
    Island
    Ragnarok
    Astraeos
    Extinction
    Aberration
    Genesis1
)

declare -A SERVER_PORT=(
    [Island]=1234
    [Ragnarok]=1234
    [Astraeos]=1234
    [Extinction]=1234
    [Aberration]=1234
    [Genesis1]=1234
)

declare -A SERVER_PASS=(
    [Island]="$rconpass"
    [Ragnarok]="$rconpass"
    [Astraeos]="$rconpass"
    [Extinction]="$rconpass"
    [Aberration]="$rconpass"
    [Genesis1]="$rconpass"
)

#################################
# EXCLUDED PLAYERS
#################################
declare -A EXCLUDED=(
    # APlayerName
    [00012341234123412341234123412341]=1    # < Excluded Player's EOSID

    # APlayerName
    [00012341234123412341234123412341]=1    # < Excluded Player's EOSID
)

#
# END CONFIGURATION
#

#######################
# LOGGING
#######################
mkdir -p \
    "$(dirname "$logFile")" \
    "$(dirname "$winnerLog")"

#######################
# GLOBAL VARIABLES
#######################
OnlinePlayerCount=0
version="3.4.8"

####################
# COLOUR VARIABLES
####################

# Terminal Formatting
RESET="\033[0m"
BOLD="\033[1m"

# UI Colours
CLR_RED="\033[38;5;196m"  # Bright Red
CLR_GREEN="\033[38;5;46m" # Bright Lime Green
CLR_GOLD="\033[38;5;226m"         # Gold
CLR_PURPLE="\033[38;5;141m"       # Light Purple / Lavender
CLR_BLUE="\033[38;5;69m"          # Bright Sky Blue
CLR_DGREEN="\033[38;5;2m"         # Dark Forest Green
CLR_ORANGE="\033[38;5;208m"       # Orange
CLR_AQUA="\033[38;5;51m"          # Aqua
CLR_TEXT="\033[38;5;255m"         # Light Grey
CLR_FRAME="${BOLD}\033[38;5;250m" # Darker Grey
CLR_TITLE="\033[38;5;147m"        # Light Grey (optional)

#################################
# PRIZE PACK FUNCTIONS
#################################

PrizePacks=(
    Prize_CryoPack_Large

    Prize_CryoPack_Small
    Prize_CryoPack_Small

    Prize_Elec250
    Prize_Element

    Prize_ElementDust
    Prize_ElementDust
    Prize_ElementDust

    Prize_ElementShards
    Prize_ElementShards
    Prize_ElementShards

    Prize_Polymer500
    Prize_ResourcePack
    Prize_ShotgunPack
    Prize_ChainsawPack
    Prize_Shit_Large
    Prize_indforge
    Prize_Cons_Mindwipe
    Prize_Cons_Cake
    Prize_Cons_Narcotic
    Prize_Cons_Honey
    Prize_Cons_PrimeJerky
    Prize_Cons_PrimeFish
    Prize_Points
    Prize_Points
    Prize_ConsumablesPack
    Prize_AdvancedRifleAmmo

    Prize_SniperAmmo
    Prize_SniperAmmo

    Prize_ShotgunShells
    Prize_ShotgunShells

    Prize_LazarusChowders
    Prize_FriaCurry
    Prize_MedicalBrews
    Prize_CookedMutton
    Prize_ExtraordinaryKibble
    Prize_ExceptionalKibble
    Prize_Sap
    Prize_RareFlowers
    Prize_Armour2pc

    Prize_RandomSaddle
    Prize_RandomSaddle

    Prize_RandomChibi
    Prize_RandomChibi
    Prize_RandomChibi
)

Armour_Flak=(
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Metal/PrimalItemArmor_MetalHelmet.PrimalItemArmor_MetalHelmet"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Metal/PrimalItemArmor_MetalShirt.PrimalItemArmor_MetalShirt"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Metal/PrimalItemArmor_MetalGloves.PrimalItemArmor_MetalGloves"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Metal/PrimalItemArmor_MetalPants.PrimalItemArmor_MetalPants"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Metal/PrimalItemArmor_MetalBoots.PrimalItemArmor_MetalBoots"
)

Armour_Ghillie=(
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Ghillie/PrimalItemArmor_GhillieHelmet.PrimalItemArmor_GhillieHelmet"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Ghillie/PrimalItemArmor_GhillieShirt.PrimalItemArmor_GhillieShirt"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Ghillie/PrimalItemArmor_GhillieGloves.PrimalItemArmor_GhillieGloves"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Ghillie/PrimalItemArmor_GhilliePants.PrimalItemArmor_GhilliePants"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Ghillie/PrimalItemArmor_GhillieBoots.PrimalItemArmor_GhillieBoots"
)

Armour_Hazmat=(
    "/Game/Aberration/CoreBlueprints/Items/Armor/HazardSuit/PrimalItemArmor_HazardSuitHelmet.PrimalItemArmor_HazardSuitHelmet"
    "/Game/Aberration/CoreBlueprints/Items/Armor/HazardSuit/PrimalItemArmor_HazardSuitShirt.PrimalItemArmor_HazardSuitShirt"
    "/Game/Aberration/CoreBlueprints/Items/Armor/HazardSuit/PrimalItemArmor_HazardSuitGloves.PrimalItemArmor_HazardSuitGloves"
    "/Game/Aberration/CoreBlueprints/Items/Armor/HazardSuit/PrimalItemArmor_HazardSuitPants.PrimalItemArmor_HazardSuitPants"
    "/Game/Aberration/CoreBlueprints/Items/Armor/HazardSuit/PrimalItemArmor_HazardSuitBoots.PrimalItemArmor_HazardSuitBoots"
)

Saddles=(
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_ArgentavisSaddle.PrimalItemArmor_ArgentavisSaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_RexSaddle.PrimalItemArmor_RexSaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_YutySaddle.PrimalItemArmor_YutySaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_TherizinosaurusSaddle.PrimalItemArmor_TherizinosaurusSaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_MaewingSaddle.PrimalItemArmor_MaewingSaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_DoedSaddle.PrimalItemArmor_DoedSaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_AnkyloSaddle.PrimalItemArmor_AnkyloSaddle"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Saddles/PrimalItemArmor_PteroSaddle.PrimalItemArmor_PteroSaddle"
)

Chibis=(
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_SnowOwl.PrimalItemSkin_ChibiDino_SnowOwl"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_TophatKairuku.PrimalItemSkin_ChibiDino_TophatKairuku"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Raptor_Bone.PrimalItemSkin_ChibiDino_Raptor_Bone"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Trike.PrimalItemSkin_ChibiDino_Trike"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Raptor.PrimalItemSkin_ChibiDino_Raptor"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Pulmonoscorpius.PrimalItemSkin_ChibiDino_Pulmonoscorpius"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Pteranodon.PrimalItemSkin_ChibiDino_Pteranodon"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Phiomia.PrimalItemSkin_ChibiDino_Phiomia"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Sheep.PrimalItemSkin_ChibiDino_Sheep"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Otter.PrimalItemSkin_ChibiDino_Otter"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Mantis.PrimalItemSkin_ChibiDino_Mantis"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Lymantria.PrimalItemSkin_ChibiDino_Lymantria"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Mesopithecus.PrimalItemSkin_ChibiDino_Mesopithecus"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Dodo.PrimalItemSkin_ChibiDino_Dodo"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Anglerfish.PrimalItemSkin_ChibiDino_Anglerfish"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Ammonite.PrimalItemSkin_ChibiDino_Ammonite"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Achatina.PrimalItemSkin_ChibiDino_Achatina"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Reindeer.PrimalItemSkin_ChibiDino_Reindeer"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_WoollyRhino.PrimalItemSkin_ChibiDino_WoollyRhino"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_RockGolem.PrimalItemSkin_ChibiDino_RockGolem"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Saber.PrimalItemSkin_ChibiDino_Saber"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Sarco.PrimalItemSkin_ChibiDino_Sarco"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Stego.PrimalItemSkin_ChibiDino_Stego"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Tapejara.PrimalItemSkin_ChibiDino_Tapejara"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Megalodon.PrimalItemSkin_ChibiDino_Megalodon"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Megatherium.PrimalItemSkin_ChibiDino_Megatherium"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Onyc.PrimalItemSkin_ChibiDino_Onyc"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Paraceratherium.PrimalItemSkin_ChibiDino_Paraceratherium"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Procoptodon.PrimalItemSkin_ChibiDino_Procoptodon"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Ankylosaurus.PrimalItemSkin_ChibiDino_Ankylosaurus"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Araneo.PrimalItemSkin_ChibiDino_Araneo"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Argent.PrimalItemSkin_ChibiDino_Argent"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Baryonyx.PrimalItemSkin_ChibiDino_Baryonyx"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Basilisk.PrimalItemSkin_ChibiDino_Basilisk"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Bronto.PrimalItemSkin_ChibiDino_Bronto"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Carnotaurus.PrimalItemSkin_ChibiDino_Carnotaurus"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Castroides.PrimalItemSkin_ChibiDino_Castroides"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Daeodon.PrimalItemSkin_ChibiDino_Daeodon"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Diplodocus.PrimalItemSkin_ChibiDino_Diplodocus"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Direbear.PrimalItemSkin_ChibiDino_Direbear"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Direwolf.PrimalItemSkin_ChibiDino_Direwolf"
    "/Game/PrimalEarth/CoreBlueprints/Items/Armor/Skin/ChibiDinos/PrimalItemSkin_ChibiDino_Doedicurus.PrimalItemSkin_ChibiDino_Doedicurus"
)

Prize_RandomChibi() {
    PrizeName="A Random Chibi Selection!"
    PrizeCommands=()

    local Chibi

    Chibi="${Chibis[RANDOM % ${#Chibis[@]}]}"

    PrizeCommands+=(
        "GiveItemToEOSID {PLAYER} '$Chibi' 1 0 0"
    )
}

Prize_RandomSaddle() {
    PrizeName="A Random Saddle Selection!"
    PrizeCommands=()

    local Saddle
    local Quality

    Saddle="${Saddles[RANDOM % ${#Saddles[@]}]}"

    # 66.7% quality 5, 22.2% quality 10, 11.1% quality 20
    local Qualities=(5 5 5 5 5 5 10 10 20)
    Quality="${Qualities[RANDOM % ${#Qualities[@]}]}"

    PrizeCommands+=(
        "GiveItemToEOSID {PLAYER} '$Saddle' 1 $Quality 0"
    )
}

Prize_Armour2pc() {
    PrizeName="2 Piece Armour Pack"
    PrizeCommands=()

    local PieceCount=2

    local ArmourTypes=(Flak Ghillie Hazmat)

    SelectedType=${ArmourTypes[RANDOM % ${#ArmourTypes[@]}]}
    local -n Set="Armour_${SelectedType}"

    mapfile -t Picks < <(
        printf '%s\n' "${Set[@]}" |
            shuf |
            head -"${PieceCount}"
    )

    echo "Selected armour type: $SelectedType"

    local Qualities=(5 5 5 5 5 5 10 10 20)
    local Quality="${Qualities[RANDOM % ${#Qualities[@]}]}"

    for item in "${Picks[@]}"; do
        echo "  $item"
    done

    for item in "${Picks[@]}"; do
        PrizeCommands+=(
            "GiveItemToEOSID {PLAYER} '$item' 1 $Quality 0"
        )
    done
}

Prize_ResourcePack() {
    PrizeName="Resource Prize Pack"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Element.PrimalItemResource_Element' 25 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Gunpowder.PrimalItemResource_Gunpowder' 500 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Polymer.PrimalItemResource_Polymer' 500 0 0"
        #"GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_BlackPearl.PrimalItemResource_BlackPearl' 100 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Electronics.PrimalItemResource_Electronics' 150 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_ChitinPaste.PrimalItemResource_ChitinPaste' 500 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Oil.PrimalItemResource_Oil' 500 0 0"
    )
}

Prize_Element() {
    PrizeName="25 Element"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Element.PrimalItemResource_Element' 25 0 0"
    )
}

Prize_ElementDust() {
    PrizeName="5000 Element Dust"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/Extinction/CoreBlueprints/Resources/PrimalItemResource_ElementDust.PrimalItemResource_ElementDust' 5000 0 0"
    )
}

Prize_ElementShards() {
    PrizeName="500 Element Shards"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_ElementShard.PrimalItemResource_ElementShard' 500 0 0"

    )
}

Prize_MetalArmorset() {
    PrizeName="[Ascendant] Complete Flak Armor Set"
    PrizeCommands=(
        "GiveArmorSet {PLAYER} Metal Asc"
    )
}

Prize_ShotgunPack() {
    PrizeName="[Ascendant] Shotgun Prize Pack"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Weapons/PrimalItem_WeaponMachinedShotgun.PrimalItem_WeaponMachinedShotgun' 1 10 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Weapons/PrimalItemAmmo_SimpleShotgunBullet.PrimalItemAmmo_SimpleShotgunBullet' 10 0 0"
    )
}

Prize_ChainsawPack() {
    PrizeName="[Ascendant] Chainsaw Prize Pack"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/ScorchedEarth/WeaponChainsaw/PrimalItem_ChainSaw.PrimalItem_ChainSaw' 1 10 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Gasoline.PrimalItemResource_Gasoline' 100 0 0"
    )
}

Prize_CryoPack_Small() {
    PrizeName="5 Cryopods"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/Extinction/CoreBlueprints/Weapons/PrimalItem_WeaponEmptyCryopod.PrimalItem_WeaponEmptyCryopod' 5 0 0"
    )
}

Prize_CryoPack_Large() {
    PrizeName="Cryogenic Prize Pack"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/Extinction/CoreBlueprints/Weapons/PrimalItem_WeaponEmptyCryopod.PrimalItem_WeaponEmptyCryopod' 10 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/Extinction/CoreBlueprints/Items/PrimalItemStructure_CryoFridge.PrimalItemStructure_CryoFridge' 1 0 0"
    )
}

Prize_Polymer500() {
    PrizeName="500 Polymer"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Polymer.PrimalItemResource_Polymer' 500 0 0"
    )
}

Prize_Elec250() {
    PrizeName="150 Electronics"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Electronics.PrimalItemResource_Electronics' 150 0 0"
    )
}

Prize_Shit_Large() {
    PrizeName="200 Individual Large Dino Shits"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_DinoPoopLarge.PrimalItemConsumable_DinoPoopLarge' 200 0 0"
    )
}

Prize_indforge() {
    PrizeName="An Industrial Forge"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Structures/Misc/PrimalItemStructure_IndustrialForge.PrimalItemStructure_IndustrialForge' 1 0 0"
    )
}

Prize_Cons_Mindwipe() {
    PrizeName="A Forgetty potion"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/BaseBPs/PrimalItemConsumableRespecSoup.PrimalItemConsumableRespecSoup' 1 0 0"
    )
}

Prize_Cons_Cake() {
    PrizeName="10 Sweet Vegetable Cakes"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_SweetVeggieCake.PrimalItemConsumable_SweetVeggieCake' 10 0 0"
    )
}

Prize_Cons_Narcotic() {
    PrizeName="250 Narcotic"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Narcotic.PrimalItemConsumable_Narcotic' 250 0 0"
    )
}

Prize_Cons_Honey() {
    PrizeName="100 Honey"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Honey.PrimalItemConsumable_Honey' 100 0 0"
    )
}

Prize_Cons_PrimeJerky() {
    PrizeName="20 Prime Meat Jerky"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_CookedPrimeMeat_Jerky.PrimalItemConsumable_CookedPrimeMeat_Jerky' 20 0 0"
    )
}

Prize_Cons_PrimeFish() {
    PrizeName="25 Prime Fish Meat"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_RawPrimeMeat_Fish.PrimalItemConsumable_RawPrimeMeat_Fish' 25 0 0"
    )
}

Prize_Points() {
    PrizeName="5 Shop Points!"
    PrizeCommands=(
        "AddPoints {PLAYER} 5"
    )
}

Prize_ConsumablesPack() {
    PrizeName="Consumables Pack - 5 of Each Soup"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_CalienSoup.PrimalItemConsumable_Soup_CalienSoup' 5 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_FriaCurry.PrimalItemConsumable_Soup_FriaCurry' 5 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_FocalChili.PrimalItemConsumable_Soup_FocalChili' 5 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_EnduroStew.PrimalItemConsumable_Soup_EnduroStew' 5 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_LazarusChowder.PrimalItemConsumable_Soup_LazarusChowder' 5 0 0"
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_ShadowSteak.PrimalItemConsumable_Soup_ShadowSteak' 5 0 0"
    )
}
Prize_Sap() {
    PrizeName="100 Sap"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_Sap.PrimalItemResource_Sap' 100 0 0"
    )
}

Prize_RareFlowers() {
    PrizeName="250 Rare Flowers"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Resources/PrimalItemResource_RareFlower.PrimalItemResource_RareFlower' 250 0 0"
    )
}

Prize_AdvancedRifleAmmo() {
    PrizeName="150 Advanced Rifle Bullets"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Weapons/PrimalItemAmmo_AdvancedRifleBullet.PrimalItemAmmo_AdvancedRifleBullet' 150 0 0"
    )
}

Prize_SniperAmmo() {
    PrizeName="100 Sniper Bullets"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Weapons/PrimalItemAmmo_SimpleRifleBullet.PrimalItemAmmo_SimpleRifleBullet' 100 0 0"
    )
}

Prize_ShotgunShells() {
    PrizeName="100 Shotgun Shells"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Weapons/PrimalItemAmmo_ShotgunShell.PrimalItemAmmo_ShotgunShell' 100 0 0"
    )
}

Prize_MedicalBrews() {
    PrizeName="50 Medical Brews"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_HealSoup.PrimalItemConsumable_HealSoup' 50 0 0"
    )
}

Prize_LazarusChowders() {
    PrizeName="5 Lazarus Chowders"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_LazarusChowder.PrimalItemConsumable_Soup_LazarusChowder' 5 0 0"
    )
}

Prize_FriaCurry() {
    PrizeName="5 Fria Curry"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Soup_FriaCurry.PrimalItemConsumable_Soup_FriaCurry' 5 0 0"
    )
}

Prize_CookedMutton() {
    PrizeName="500 Cooked Lamb Chops"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_CookedLambChop.PrimalItemConsumable_CookedLambChop' 500 0 0"
    )
}

Prize_ExtraordinaryKibble() {
    PrizeName="10 Extraordinary Kibble"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Kibble_Base_Special.PrimalItemConsumable_Kibble_Base_Special' 10 0 0"
    )
}

Prize_ExceptionalKibble() {
    PrizeName="10 Exceptional Kibble"
    PrizeCommands=(
        "GiveItemToEOSID {PLAYER} '/Game/PrimalEarth/CoreBlueprints/Items/Consumables/PrimalItemConsumable_Kibble_Base_XLarge.PrimalItemConsumable_Kibble_Base_XLarge' 10 0 0"
    )
}

#######################
# FUNCTIONS
#######################
timestamp() {
    local message="$1"
    local colour="${2:-}"
    local line

    line="$(date '+%F %T') - $message"
    echo -e "${colour}${line}${RESET}" >&2
}

send_rcon() {
    local server=$1
    local command=$2
    local result

    result=$(
        "$mcrcon" \
            -H "$serverIP" \
            -P "${SERVER_PORT[$server]}" \
            -p "${SERVER_PASS[$server]}" \
            "$command" 2>&1 |
            sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g'
    )

    #timestamp "[$server] $command -> $result"
    printf '%s\n' "$result"
}

check_player_num() {
    OnlinePlayerCount=0
    echo
    echo -e "${CLR_GOLD}  ▶ Checking total connected players${RESET}"
    echo

    for servername in "${!SERVER_PORT[@]}"; do
        # Calculate total online on cluster
        onlineNum=$(send_rcon "$servername" getonlinenum)
        OnlinePlayerCount=$((OnlinePlayerCount + onlineNum))
        echo "${servername}: $onlineNum Players"
    done
    echo
    echo -e "  ▶ Total Players Online: ${CLR_GOLD}${OnlinePlayerCount}${RESET}"
    echo
}

broadcast_all() {
    local message=$1

    for server in "${ACTIVE_SERVERS[@]}"; do
        send_rcon "$server" "Broadcast $message" >/dev/null
    done
}

chat_all() {
    local message=$1

    for server in "${ACTIVE_SERVERS[@]}"; do
        send_rcon "$server" "ServerChat $message" >/dev/null
    done
}

get_active_servers() {
    ACTIVE_SERVERS=()

    for server in "${SERVERS[@]}"; do
        local result
        result=$(send_rcon "$server" "ListPlayers")
        [[ $result == *"No Players Connected"* ]] && continue
        ACTIVE_SERVERS+=("$server")
    done
}

get_online_players() {
    PLAYERS=()

    declare -gA PLAYER_NAME=()
    declare -gA PLAYER_SERVER=()

    local -A seen=()

    for server in "${SERVERS[@]}"; do
        timestamp "Querying players on $server"

        local result
        result=$(send_rcon "$server" "ListPlayers")

        [[ $result == *"No Players Connected"* ]] && continue

        while IFS= read -r line; do
            # Remove ANSI escape sequences
            line=$(sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g' <<<"$line")

            # Trim trailing whitespace
            line="${line%"${line##*[![:space:]]}"}"

            [[ $line =~ ^[0-9]+\.[[:space:]]*([^,]+),[[:space:]]*([a-fA-F0-9]{32})$ ]] || continue

            local name="${BASH_REMATCH[1]}"
            local id="${BASH_REMATCH[2]}"

            name="${name#"${name%%[![:space:]]*}"}"
            name="${name%"${name##*[![:space:]]}"}"

            [[ ${EXCLUDED[$id]+x} ]] && {
                timestamp "Excluded player: $name [$id]" "$CLR_ORANGE"
                continue
            }

            [[ ${seen[$id]+x} ]] && continue

            seen[$id]=1

            PLAYERS+=("$id")
            PLAYER_NAME[$id]="$name"
            PLAYER_SERVER[$id]="$server"

        done <<<"$result"
    done
}

get_prize() {
    selected=$((RANDOM % ${#PrizePacks[@]}))
    "${PrizePacks[$selected]}"
}

award_prize() {
    local server="$1"
    local player="$2"
    local command

    prizeLog=""

    for command in "${PrizeCommands[@]}"; do
        command="${command//\{PLAYER\}/$player}"

        send_rcon "$server" "$command" >/dev/null

        prizeLog+="  $command"$'\n'
    done
}


colour_logs() {
    shopt -s nocasematch

    while IFS= read -r line; do
    line=$(sed -E 's|.*/ShopLog_([^/]+)_WP\.log:|\1: |' <<<"$line")
        case $line in
    # Raffle Logs
    *RAFFLE\ INITIATED*)    echo -e "${CLR_GOLD}$line${RESET}";;
    *Active\ Servers*)    echo -e "${CLR_AQUA}$line${RESET}";;
    *Excluded\ player*)    echo -e "${BOLD}${CLR_ORANGE}$line${RESET}";;
    *Querying\ players*)    echo -e "${CLR_AQUA}$line${RESET}";;
    *Entry:*)               echo -e "${CLR_AQUA}$line${RESET}";;
    *▶\ Winner*)               echo -e "${CLR_GREEN}$line${RESET}";;
    *▶*)               echo -e "${CLR_PURPLE}$line${RESET}";;
    *Eligible\ Players*)    echo -e "${BOLD}${CLR_AQUA}$line${RESET}";;
    *\ \-\ WINNER:*)    echo -e "${BOLD}${CLR_GREEN}$line${RESET}";;
    *sent\ to*)    echo -e "${BOLD}${CLR_GREEN}$line${RESET}";;
    *RAFFLE\ COMPLETE*)    echo -e "${CLR_GOLD}$line${RESET}";;

            *)          echo "$line" ;;
        esac
    done

    shopt -u nocasematch
}

#######################
# COMMAND FUNCTIONS
#######################

cmd_version() {
    echo -e "${CLR_TITLE}${BOLD}Scruffy Cluster Raffle v${version}${RESET}"
    exit 0
}

usage() {
    echo
    echo -e "${CLR_TITLE}${BOLD}Scruffy Cluster Raffle v${version}${RESET}"
    echo
    echo -e "${BOLD}Description:${RESET}${CLR_TEXT}"
    echo -e "  CLI for running and maintaining raffles within the Scruffy Cluster"
    echo -e "  Raffle will first check for 5 or more players, if met, a raffle is run"
    echo -e "  Flags to force a raffle regardless are available."
    echo
    echo -e "${RESET}${BOLD}Usage:${RESET}${CLR_TEXT}"
    echo -e "  raffle         ────  Run a regular raffle (Required: 5 Players) "
    echo -e "  raffle -f      ────  Force a raffle, regardless of how many online players "
    echo
    echo -e "  raffle -l      ────  View the last 150 lines of the Raffle log "
    echo -e "  raffle -w      ────  View the last 100 lines of the Winners Log "
    echo -e "  raffle -lw     ────  View the last entry in Winners Log "
    echo
    echo -e "  raffle -v      ────  Check Raffle CLI Version "
    echo

    exit 1
}

#
# Raffle logs
#
cmd_log() {
    echo
    echo "Loading Live Raffle Log..."
    echo -e "${CLR_AQUA}${BOLD}  CTRL + C to Exit${RESET}"
    sleep 1
    tail -n 75 -F "$logFile" | colour_logs
}

cmd_winners() {
    echo
    echo "Loading Raffle Winners Log..."
    sleep 1
    tail -n 72 "$winnerLog" | colour_logs
}

cmd_last_winner() {

    echo
    echo -e "${CLR_GOLD}Last raffle winner${RESET}"
    echo "───────────────────"
    tail -n 12  "$winnerLog" | colour_logs
    echo
}

####################
# MAIN CODE
###################
cmd_raffle() {

local mode="${1:-}"

# Log raffle output only
    exec > >(tee >(sed -E 's/\x1B\[[0-9;]*[[:alpha:]]//g' >> "$logFile")) 2>&1

    timestamp ""

    timestamp "───── RAFFLE INITIATED ─────"

    if [[ "$mode" != "-f" ]]; then
        check_player_num

        if [[ $OnlinePlayerCount -lt "${min_players}" ]]; then
            timestamp "Less than ${min_players} players online"
            timestamp "───── RAFFLE CANCELLED ─────" "$CLR_RED"
            exit 0
        fi
    fi

    get_active_servers

    if ((${#ACTIVE_SERVERS[@]} == 0)); then
        timestamp "No active servers online."
        timestamp "───── RAFFLE CANCELLED ─────"
        exit 0
    fi

    timestamp "Active Servers: $(
        IFS=', '
        echo "${ACTIVE_SERVERS[*]}"
    )"

    broadcast_all "SCRUFFY CLUSTER RAFFLE! - A winner will be drawn in 30 seconds!"
    timestamp "Broadcasting Raffle Draw in 30 Seconds -> Active Servers"

    timestamp "Waiting 30 seconds before draw..."
    sleep 20

    broadcast_all "10 seconds until the raffle draw!"
    timestamp "Broadcasting Raffle Draw in 10 Seconds -> Active Servers"
    sleep 7

    chat_all "Drawing raffle..."
    timestamp "Drawing raffle..."

    chat_all "3..."
    timestamp "3..."
    sleep 1

    chat_all "2..."
    timestamp "2..."
    sleep 1

    chat_all "1..."
    timestamp "1..."
    sleep 1

    chat_all "Drawing winner..."
    timestamp "Drawing winner..."

    get_online_players

    timestamp ""
    timestamp "Eligible Players: ${#PLAYERS[@]}" "$CLR_GREEN"

    if ((${#PLAYERS[@]} == 0)); then
        timestamp "No eligible players found."
        chat_all "No eligible players were online for the raffle."
        timestamp "───── RAFFLE CANCELLED ─────"
        exit 0
    fi

    for id in "${PLAYERS[@]}"; do
        timestamp "Entry: ${PLAYER_NAME[$id]} [$id] on ${PLAYER_SERVER[$id]}"
    done

    mapfile -t PLAYERS < <(printf '%s\n' "${PLAYERS[@]}" | shuf)

    echo
    echo -e "${CLR_PURPLE}  ▶ PLAYER COUNT: ${CLR_GOLD}${#PLAYERS[@]}${RESET}"
    echo

    echo -e "${CLR_PURPLE}Shuffled player list:${RESET}"
    for i in "${!PLAYERS[@]}"; do
        playerID="${PLAYERS[$i]}"
        echo "$i : ${PLAYER_NAME[$playerID]}"
    done

    echo

    index=$((RANDOM % ${#PLAYERS[@]}))
    echo -e "${CLR_PURPLE}  ▶ Random index chosen: ${CLR_GREEN}${index}${RESET}"

    winnerID="${PLAYERS[$index]}"

    echo
    echo "WinnerID: $winnerID"
    echo -e "  ▶ Winner: ${CLR_GREEN}${PLAYER_NAME[$winnerID]}${RESET}"
    echo
    winnerName="${PLAYER_NAME[$winnerID]}"
    winnerServer="${PLAYER_SERVER[$winnerID]}"

    [[ -z $winnerName ]] && {
        echo "Winner selection failed." >&2
        exit 1
    }

    timestamp ""
    timestamp "WINNER: $winnerName [$winnerID]" "$CLR_GREEN"
    timestamp ""

    get_prize

    broadcast_all "RAFFLE WINNER: $winnerName!"
    chat_all "RAFFLE WINNER: $winnerName!"

    timestamp "Waiting 10 seconds to congratulate..."
    sleep 10

    broadcast_all "Congratulations $winnerName! - You have won: $PrizeName"
    chat_all "Congratulations $winnerName! You have won: $PrizeName"
    timestamp "Congratulations $winnerName! You have won: $PrizeName" "$CLR_GOLD"

    award_prize "$winnerServer" "$winnerID" "$CLR_GOLD"
    timestamp "Prize $PrizeName sent to $winnerName" "$CLR_GREEN"

    # Log all raffle details
    winnerRecord=$(
        cat <<EOF

────────────────────────────────────────────────────────────
Date      : $(date '+%Y-%m-%d %H:%M:%S')
Winner    : $winnerName
Player ID : $winnerID
Map       : $winnerServer

Prize     : $PrizeName
Prize(s):
$prizeLog
───────────────────────────────────────────────────────

EOF
    )
    printf '%s\n' "$winnerRecord" >>"$winnerLog"

    timestamp "───── RAFFLE COMPLETE ─────"
}

####################
# COMMANDS
####################
case "${1:-}" in
    "")
        cmd_raffle
        ;;
    -f)
        cmd_raffle -f
        ;;
    -v)
        cmd_version
        ;;
    -w)
        cmd_winners
        ;;
    -lw)
        cmd_last_winner
        ;;
    -l)
        cmd_log
        ;;
    -h)
        usage
        ;;
    *)
        usage
        ;;
esac

ArkShop Manager CLI

Dependencies:
Requires jq.exe – for interaction with the Json arkshop config file.
Simply download the windows amd64 .exe, rename it to jq.exe, and put it somewhere already on the server’s PATH.
──────────────────────────
If you need help with adding a location to your server’s PATH,
I’ve created a super quick and simple guide here: Git Bash & Editing your Server’s PATH

Download jq.exe: https://github.com/jqlang/jq/releases/tag/jq-1.8.2
──────────────────────────────

Description:
For use with the ArkShop plugin
Allows one to easily visualise abstracted JSON data.
View/Search/Add/Remove shop items
And roll out changes from a single master config.json to 1 or all servers at once.

Usage:

 List:
   arkshop -ls                   – List all available Kits
   arkshop -ls <item>            – List all items within a kit

 Search:
   arkshop -s <item>             – Search for a Kit/Item

 Add:
   arkshop -a                    – Add a new Kit and Items within
   arkshop -a <kit>              – Add a new item to an existing Kit

 Delete:
   arkshop -d <kit>              – Delete an entire Kit
   arkshop -d <kit> <item-#>     – Delete an item from a kit
                                 (item-# attainable using arkshop -ls)

 Logs:
   arkshop -l <map>              – Display a map’s entire ArkShop Log
   arkshop -f <search>           – Search all maps Arkshop Logs for a search-term
   arkshop -m                    – List all maps you’ve defined in this CLI’s config

 Rollout:
   arkshop -r                    – Rollout the updated config.json to 1 or ALL servers

Examples:
 arkshop -ls
 arkshop -s shotgun
 arkshop -d StarterKit 2
 arkshop -f element


Download arkshop.sh

#!/bin/bash


####################
# CONFIGURATION
####################

# ARKSHOP MASTER CONFIG FILE
SHOP_FILE="/c/ark/arkshop/config.json"

# YOUR ARK SERVERS AND THEIR DIRECTORY
SERVERS=(
    "Island|/C/ARK/Island"
    "Ragnarok|/C/ARK/Ragnarok"
    "Aberration|/C/ARK/Aberration"
    "Extinction|/C/ARK/Extinction"
    "Astraeos|/C/ARK/Astraeos"
    "Genesis|/C/ARK/Genesis"
)

####################
# END CONFIGURATION
####################

# GLOBAL VARIABLES
VERSION="Odi's ArkShop CLI 2.3.2"

####################
# COLOUR VARIABLES
####################

# Terminal Formatting
RESET="\033[0m"
BOLD="\033[1m"

# UI Colours
CLR_RED="\033[38;5;196m"  # Bright Red
CLR_GREEN="\033[38;5;46m" # Bright Lime Green

CLR_GOLD="\033[38;5;226m"         # Gold
CLR_PURPLE="\033[38;5;141m"       # Light Purple / Lavender
CLR_BLUE="\033[38;5;69m"          # Bright Sky Blue
CLR_DGREEN="\033[38;5;2m"         # Dark Forest Green
CLR_ORANGE="\033[38;5;208m"       # Orange
CLR_AQUA="\033[38;5;51m"          # Aqua
CLR_TEXT="\033[38;5;248m"         # Light Grey
CLR_FRAME="${BOLD}\033[38;5;250m" # Darker Grey
CLR_TITLE="\033[38;5;147m"        # Light Grey (optional)

set -e

#######################
# Unicode box Chars
########################
H="─"
V="${CLR_FRAME}│${RESET}"
V2="${CLR_ORANGE}│${RESET}"
VB="│"

TL="┌"
TR="┐"
BL="└"
BR="┘"

LT="├"
RT="┤"
TT="┬"
BT="┴"
CR="┼"

TH_FRAME="${BOLD}${CLR_ORANGE}${TL}──────────────────────────────────────────${TR}${RESET}"
#MD_FRAME="${CLR_FRAME}${LT}────${CR}────────────${CR}───────${RT}${RESET}"
BH_FRAME="${BOLD}${CLR_ORANGE}${BL}──────────────────────────────────────────${BR}${RESET}"

ROW_PREFIX="${CLR_FRAME}${V}${RESET}"

##########
# HELPERS
###########
error() {
    echo
    echo -e "  ${CLR_RED}Error: ${RESET}$1"
    echo
}

#######
# VALIDATION
######

is_int() {
    [[ $1 =~ ^[0-9]+$ ]]
}

is_number() {
    [[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
}
####################################
# JSON API
####################################

shop_exists() {

    local id="$1"

    jq -e --arg id "$id" \
        '.ShopItems[$id]' \
        "$SHOP_FILE" >/dev/null
}

shop_get() {

    local id="$1"
    local field="$2"

    if [[ -z "$field" ]]; then
        jq --arg id "$id" \
            '.ShopItems[$id]' \
            "$SHOP_FILE"
    else
        jq -r \
            --arg id "$id" \
            --arg field "$field" \
            '.ShopItems[$id][$field]' \
            "$SHOP_FILE"
    fi
}

shop_list() {

    jq -r '
        .ShopItems
        | to_entries[]
        | [
            .key,
            (.value.Price // "-"),
            (.value.Type // "-"),
            (.value.Description // "-")
        ]
        | @tsv
    ' "$SHOP_FILE"
}

shop_add() {

    local id="$1"
    local tmp
    local new_item

    if [[ "$type" == "dino" ]]; then

        new_item=$(
            jq -n \
                --arg desc "$description" \
                --arg type "$type" \
                --argjson price "$price" \
                --arg bp "$blueprint" \
                --argjson level "$level" \
                --argjson cryo "$prevent_cryo" '
            {
                Type: $type,
                Description: $desc,
                Level: $level,
                Price: $price,
                Blueprint: $bp,
                PreventCryo: $cryo
            }'
        )

    else

        local json_items="[]"

        json_items=$(printf '%s
' "${items[@]}" | jq -s .)

        new_item=$(
            jq -n \
                --arg desc "$description" \
                --arg type "$type" \
                --argjson price "$price" \
                --argjson items "$json_items" '
            {
                Type: $type,
                Description: $desc,
                Price: $price,
                Items: $items
            }'
        )

    fi

    tmp=$(mktemp)

    jq \
        --arg id "$id" \
        --argjson item "$new_item" \
        '.ShopItems[$id] = $item' \
        "$SHOP_FILE" >"$tmp"

    mv "$tmp" "$SHOP_FILE"
}

shop_add_item() {

    local id="$1"
    local item="$2"

    local tmp
    tmp=$(mktemp)

    jq \
        --arg id "$id" \
        --argjson item "$item" \
        '
        .ShopItems[$id].Items += [$item]
        ' \
        "$SHOP_FILE" >"$tmp" || {
        rm -f "$tmp"
        return 1
    }

    mv "$tmp" "$SHOP_FILE"
}

####################################
# COMMANDS
####################################

usage() {
    echo
    echo -e "${CLR_TITLE}${BOLD}${VERSION}${RESET}"
    echo
    echo -e "${BOLD}Description:${RESET}"
    echo -e "${CLR_TEXT} For use with the ArkShop plugin - allows one to easily visualize abstracted JSON data"
    echo -e "${CLR_TEXT}  add/remove entries, and roll out changes from a single master config.json to 1 or all servers.${RESET}"
    echo
    echo -e "${BOLD}Usage:${RESET}"
    echo
    echo -e "  ${BOLD}List:${RESET}"
    echo -e "${CLR_TEXT}    arkshop -ls                   - List all available Kits"
    echo -e "${CLR_TEXT}    arkshop -ls <item>            - List all items within a kit${RESET}"
    echo
    echo -e "  ${BOLD}Search:${RESET}"
    echo -e "${CLR_TEXT}    arkshop -s <item>             - Search for a Kit/Item${RESET}"
    echo
    echo -e "  ${BOLD}Add:${RESET}"
    echo -e "${CLR_TEXT}    arkshop -a                    - Add a new Kit and Items within"
    echo -e "    arkshop -a <kit>              - Add a new item to an existing Kit${RESET}"
    echo
    echo -e "  ${BOLD}Delete:${RESET}"
    echo -e "${CLR_TEXT}    arkshop -d <kit>              - Delete an entire Kit"
    echo -e "    arkshop -d <kit> <item-#>     - Delete an item from a kit"
    echo -e "                                  ${RESET}(item-# attainable using ${CLR_BLUE}arkshop -ls)${RESET}"
    echo
    echo -e "  ${BOLD}Logs:${RESET}"
    echo -e "${CLR_TEXT}    arkshop -l <map>              - Display a map's entire ArkShop Log"
    echo -e "    arkshop -f <search>           - Search all maps Arkshop Logs for a search-term"
    echo -e "    arkshop -m                    - List all maps you've defined in this CLI's config${RESET}"
    echo
    echo -e "  ${BOLD}Rollout:${RESET}"
    echo -e "${CLR_TEXT}    arkshop -r                    - Rollout the updated config.json to 1 or ALL servers${RESET}"
    echo
    echo -e "${BOLD}Examples:${RESET}"
    echo -e "${CLR_TEXT}  arkshop -ls"
    echo -e "  arkshop -s shotgun"
    echo -e "  arkshop -d StarterKit 2"
    echo -e "  arkshop -f element${RESET}"
    echo
}

cmd_list() {
    #
    # List the entire shop
    #

    if [[ -z "$2" ]]; then

        echo
        echo -e " ${CLR_FRAME}────────────────────── ${BOLD}Shop Contents${RESET} ──────────────────────────${RESET}"

        #
        # SHOP CONTENTS
        #
        shop_list |
            while IFS=$'\t' read -r id price type description; do
                #echo
                echo -e "  Name:${CLR_ORANGE}   ${BOLD}$id${RESET}"
                echo -e "  Desc:${RESET}   $description"
                echo -e "  ${CLR_GREEN}Price:  ${BOLD}$price${RESET}"
                #echo
                echo -e " ${CLR_FRAME}────────────────────────────────────────────────${RESET}"
            done

        echo
        return

    fi

    #
    # List a Specific Kit
    #

    local ITEM="$2"

    if ! shop_exists "$ITEM"; then
        echo
        echo -e "${CLR_RED}${BOLD}Error:${RESET} Item '$ITEM' not found."
        echo
        return 1
    fi

    local item
    item=$(shop_get "$ITEM")

    #
    # KIT CONTENTS
    #
    echo
    echo -e "${CLR_FRAME}───────────────────── Kit ───────────────────────────${RESET}"
    echo -e "${BOLD} Name:        ${CLR_ORANGE}$ITEM${RESET}"
    echo -e "${BOLD} Description:${BOLD} $(jq -r '.Description' <<<"$item")${RESET}"
    echo -e "${BOLD} Type:${BOLD}        $(jq -r '.Type' <<<"$item")${RESET}"
    echo -e "${BOLD} Price:${BOLD}       ${CLR_GREEN}$(jq -r '.Price' <<<"$item")${RESET}"
    echo
    echo -e "${CLR_FRAME}─────────────────── Contents ────────────────────────${RESET}"

    if jq -e '.Items' >/dev/null <<<"$item"; then

        items=$(jq -c ".ShopItems[\"$ITEM\"].Items[]?" "$SHOP_FILE")

        if [[ -z "$items" ]]; then
            echo -e "     ${CLR_FRAME}${TL}─────────────────────────────${TR}${RESET}"
            echo -e "     ${V}                             ${V}"
            echo -e "     ${V}    No items in this kit.    ${V}"
            echo -e "     ${V}                             ${V}"
            echo -e "     ${CLR_FRAME}${BL}─────────────────────────────${BR}${RESET}"
            echo
            return
        fi

        local item_no=1
        jq -c '.Items[]' <<<"$item" |
            while read -r entry; do
                path=$(jq -r '.Blueprint' <<<"$entry")
                amount=$(jq -r '.Amount' <<<"$entry")
                quality=$(jq -r '.Quality // empty' <<<"$entry")
                damage=$(jq -r '.Damage // empty' <<<"$entry")
                armor=$(jq -r '.Armor // empty' <<<"$entry")
                durability=$(jq -r '.Durability // empty' <<<"$entry")
                blueprint=$(jq -r '.ForceBlueprint' <<<"$entry")

                title=$(
                    basename "${path#*PrimalItem_}" |
                        cut -d. -f1 |
                        sed -E \
                            -e 's/^PrimalItemAmmo_//' \
                            -e 's/^PrimalItemConsumable_//' \
                            -e 's/^PrimalItemResource_//' \
                            -e 's/^PrimalItemStructure_//' \
                            -e 's/^PrimalItemSkin_//' \
                            -e 's/^PrimalItemDye_//' \
                            -e 's/^Weapon//' \
                            -e 's/^Armor//' \
                            -e 's/^Structure//' \
                            -e 's/^Consumable//' \
                            -e 's/^Resource//' \
                            -e 's/^Ammo//' \
                            -e 's/([a-z0-9])([A-Z])/\1 \2/g'
                )

                #START BOX
                echo -e "  ${BOLD}[ITEM ${CLR_GOLD}${item_no}${RESET}${BOLD}] - ${CLR_AQUA}$title${RESET}"

                echo -e "    Amount:      ${BOLD}${CLR_GREEN}$amount${RESET}"

                [[ -n "$quality" ]] && echo -e "    Quality:     ${BOLD}${CLR_BLUE}$quality${RESET}"
                [[ -n "$damage" ]] && echo -e "    Damage:      ${BOLD}${CLR_ORANGE}${damage}%${RESET}"
                [[ -n "$armor" ]] && echo -e "    Armor:       ${BOLD}${CLR_ORANGE}$armor${RESET}"
                [[ -n "$durability" ]] && echo -e "    Durability:  ${BOLD}${CLR_ORANGE}$durability${RESET}"

                if [[ "$blueprint" == "true" ]]; then
                    echo -e "    Blueprint:   ${CLR_GREEN}true${RESET}"
                else
                    echo -e "    Blueprint:   ${CLR_RED}false${RESET}"
                fi

                echo -e " ${BOLD} Path:${RESET}"
                echo -e "  ${CLR_TEXT}$path${RESET}"
                #echo -e " ${V}"
                #END BOX
                echo -e "${CLR_FRAME}─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────${RESET}"
                ((item_no++))
            done

    else

        blueprint=$(jq -r '.Blueprint' <<<"$item")
        level=$(jq -r '.Level // empty' <<<"$item")
        cryo=$(jq -r '.PreventCryo // empty' <<<"$item")

        dino_title=$(sed -E 's|.*/Dinos/([^/]+)/.*|\1|' <<<"$blueprint")

        echo -e "  ${BOLD}[DINO] - ${CLR_AQUA}$dino_title${RESET}"
        [[ -n "$level" ]] && echo -e "    Level:       ${BOLD}${CLR_BLUE}$level${RESET}"
        [[ -n "$cryo" ]] && echo -e "    Cryopod:     ${BOLD}$cryo${RESET}"
        echo -e "    Path:"
        echo -e "  ${CLR_TEXT}${blueprint}${RESET}"
        echo -e "${CLR_FRAME}─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────${RESET}"

    fi

    echo
}

cmd_search() {
    local term="${1,,}"
    echo
    echo -e "  Keyword: ${BOLD}${term}${REEST}"
    echo -e "${CLR_FRAME}──────────────────────── ${RESET}${BOLD}Search Results${RESET}${CLR_FRAME} ───────────────────────${RESET}"
    echo

    jq -r --arg term "$term" '
        .ShopItems
        | to_entries[]
        | select(
            (.key | ascii_downcase | contains($term))
        or
            (.value.Description | ascii_downcase | contains($term))
        or
        any(
            (.value.Items // [])[];
            (.Blueprint | ascii_downcase | contains($term))
        )
        )
        | [
            .key,
            .value.Description,
            .value.Price
          ] | @tsv
    ' "$SHOP_FILE" |
        while IFS=$'\t' read -r id desc price; do

            echo -e "  Name:${CLR_ORANGE}   ${BOLD}$id${RESET}"
            echo -e "  Desc:${RESET}   $desc"
            echo -e "  ${CLR_GREEN}Price:  ${BOLD}$price${RESET}"
            #echo
            echo -e "${CLR_FRAME}───────────────────────────────────────────────${RESET}"
        done

    echo
}

prompt_item() {

    #
    # Blueprint
    #
    while true; do
        read -rp "Blueprint Path: " blueprint

        [[ -n "$blueprint" ]] && break

        error "Blueprint Path cannot be blank."
    done

    #
    # Amount
    #
    while true; do
        read -rp "Amount [1]: " amount
        amount=${amount:-1}

        if is_int "$amount" && ((amount > 0)); then
            break
        fi

        error "Amount must be a whole number greater than zero."
    done

    #
    # Quality
    #
    while true; do
        read -rp "Quality (blank = omit): " quality

        [[ -z "$quality" ]] && break
        is_number "$quality" && break

        error "Quality must be a number."
    done

    #
    # Damage
    #
    while true; do
        read -rp "Damage (blank = omit): " damage

        [[ -z "$damage" ]] && break
        is_number "$damage" && break

        error "Damage must be a number."
    done

    #
    # Armor
    #
    while true; do
        read -rp "Armor (blank = omit): " armor

        [[ -z "$armor" ]] && break
        is_number "$armor" && break

        error "Armor must be a number."
    done

    #
    # Durability
    #
    while true; do
        read -rp "Durability (blank = omit): " durability

        [[ -z "$durability" ]] && break
        is_number "$durability" && break

        error "Durability must be a number."
    done

    #
    # Blueprint?
    #
    read -rp "Give as Blueprint? (y/N): " ans
    [[ "${ans,,}" =~ ^y ]] && forcebp=true || forcebp=false

    echo
    echo "──────────────────────────────"
    echo -e "${BOLD} Blueprint :${RESET} $blueprint"
    echo -e "${BOLD} Amount    :${RESET} $amount"
    [[ -n "$quality" ]] && echo -e "${BOLD} Quality   :${RESET} $quality"
    [[ -n "$damage" ]] && echo -e "${BOLD} Damage    :${RESET} $damage"
    [[ -n "$armor" ]] && echo -e "${BOLD} Armor     :${RESET} $armor"
    [[ -n "$durability" ]] && echo -e "${BOLD} Durability:${RESET} $durability"
    echo -e "${BOLD} Blueprint :${RESET} $forcebp"
    echo "──────────────────────────────"

    item_json=$(
        jq -n \
            --arg bp "$blueprint" \
            --argjson amount "$amount" \
            --argjson force "$forcebp" \
            '{
            Amount: $amount,
            ForceBlueprint: $force,
            Blueprint: $bp
        }'
    )

    if [[ -n "$quality" ]]; then
        item_json=$(jq --argjson q "$quality" '. + {Quality:$q}' <<<"$item_json")
    fi

    if [[ -n "$damage" ]]; then
        item_json=$(jq --argjson d "$damage" '. + {Damage:$d}' <<<"$item_json")
    fi

    if [[ -n "$armor" ]]; then
        item_json=$(jq --argjson a "$armor" '. + {Armor:$a}' <<<"$item_json")
    fi

    if [[ -n "$durability" ]]; then
        item_json=$(jq --argjson d "$durability" '. + {Durability:$d}' <<<"$item_json")
    fi
}

cmd_add() {

    echo
    echo -e "${BOLD}${RESET}"
    echo -e "${BOLD}         Add New Shop Item${RESET}"
    echo

    echo -e "${CLR_FRAME}─────────── ${RESET}${BOLD}Kit Details${RESET}${CLR_FRAME} ───────────${RESET}"
    local id="$2"

    if [[ -z "$id" ]]; then
        read -rp "Name: " id
    fi

    if shop_exists "$id"; then
        type=$(shop_get "$id" "Type")

        if [[ "$type" != "item" ]]; then
            error "'$id' is a dino entry."
            error "Items cannot be added."
            return
        fi

        echo
        echo "Adding item to '$id'"
        echo

        prompt_item

        shop_add_item "$id" "$item_json"

        echo
        echo -e "──────────────────────────────"
        echo -e " ${CLR_GREEN}Added item to '$id'.${RESET}"
        echo -e "──────────────────────────────"
        echo
        return
    fi

    #
    # Description
    #
    while true; do
        read -rp "Description: " description

        [[ -n "$description" ]] && break

        error "Description cannot be blank."
    done

    #
    # Type
    #
    while true; do
        read -rp "Type (item/dino): " type
        type="${type,,}"

        case "$type" in
            item | dino)
                break
                ;;
            *)
                error "Type must be 'item' or 'dino'."
                ;;
        esac
    done

    #
    # Price
    #
    while true; do
        read -rp "Price: " price

        if is_int "$price" && ((price > 0)); then
            break
        fi

        error "Price must be a whole number greater than zero."
    done

    if [[ "$type" == "dino" ]]; then

        #
        # Level
        #
        while true; do
            read -rp "Level: " level

            if is_int "$level" && ((level > 0)); then
                break
            fi

            error "Level must be a whole number greater than zero."
        done

        #
        # Prevent Cryopod
        #
        read -rp "Prevent Cryopod? (y/N): " ans
        [[ "${ans,,}" =~ ^y ]] && prevent_cryo=true || prevent_cryo=false

        #
        # Blueprint
        #
        while true; do
            read -rp "Blueprint: " blueprint

            [[ -n "$blueprint" ]] && break

            error "Blueprint cannot be blank."
        done

    else

        echo
        echo -e "${CLR_FRAME}─────────── ${RESET}${BOLD}Adding Kit Items${RESET}${CLR_FRAME} ───────────${RESET}"
        #echo -e "${BOLD}Adding kit contents...${RESET}"

        items=()

        while true; do

            echo

            prompt_item

            items+=("$item_json")

            echo
            read -rp "Add another item? (y/N): " again

            [[ "${again,,}" =~ ^y ]] || break

        done

    fi

    shop_add "$id"

    echo
    echo -e "──────────────────────────────"
    echo -e "${CLR_GREEN}  Created shop entry '$id'.${RESET}"
    echo -e "──────────────────────────────"

    echo
}

shop_delete() {
    local id="$1"
    local idx="$2"
    local tmp
    tmp=$(mktemp)
    if [[ -z "$idx" ]]; then
        jq --arg id "$id" '.ShopItems |= del(.[$id])' "$SHOP_FILE" >"$tmp"
    else
        jq --arg id "$id" --argjson i "$idx" '.ShopItems[$id].Items |= del(.[ $i ])' "$SHOP_FILE" >"$tmp"
    fi
    mv "$tmp" "$SHOP_FILE"
}

cmd_delete() {
    local id="$2"
    local num="$3"
    [[ -z "$id" ]] && {
        usage
        return 1
    }
    if ! shop_exists "$id"; then
        error "Kit '$id' not found."
        return 1
    fi
    if [[ -z "$num" ]]; then
        echo
        echo -en "  Delete ${CLR_RED}Entire Kit${RESET}: ${BOLD}${CLR_GOLD}$id${RESET}? (y/N) "
        read -r ans
        echo
        [[ ${ans,,} != y ]] && return
        shop_delete "$id"
        echo -e "──────────────────────────────"
        echo -e "${CLR_RED}  Kit Deleted: $id${RESET}"
        echo -e "──────────────────────────────"
        echo
    else
        if ! jq -e --arg id "$id" '.ShopItems[$id].Items' "$SHOP_FILE" >/dev/null; then
            error "'$id' does not contain an Items array."
            return 1
        fi
        count=$(jq --arg id "$id" '.ShopItems[$id].Items|length' "$SHOP_FILE")
        [[ "$num" =~ ^[0-9]+$ ]] || {
            error "Invalid item number."
            return 1
        }
        ((num >= 1 && num <= count)) || {
            error "Item $num does not exist."
            return 1
        }
        echo
        echo -en "  Delete ${CLR_GOLD}Item $num${RESET} from ${CLR_GOLD}$id${RESET}? (y/N) "
        read -r ans
        echo

        [[ ${ans,,} != y ]] && return
        shop_delete "$id" "$((num - 1))"
        echo -e "────────────────────────────────"
        echo -e "${CLR_GREEN} Item $num removed from $id.${RESET}"
        echo -e "────────────────────────────────"
        echo
    fi
}

cmd_rollout() {

    echo
    echo -e "${BOLD}         Rollout Updated Config.json${RESET}"
    echo

    echo -e "${CLR_FRAME}───────────── ${RESET}Select Servers to Rollout Changes to${CLR_FRAME} ─────────────${RESET}"
    echo

    number=1

    for server in "${SERVERS[@]}"; do
        map="${server%%|*}"
        echo "$number) $map"
        ((number++))
    done

    echo
    echo "$number) All Servers"
    echo

    read -rp "Selection: " ans

    if ((ans == number)); then

        for server in "${SERVERS[@]}"; do
            path="${server#*|}"

            cp -f "$SHOP_FILE" \
                "$path/ShooterGame/Binaries/Win64/ArkApi/Plugins/ArkShop/config.json"
        done

        echo
        echo "──────────────────────────────"
        echo -e "${CLR_GREEN}config.json deployed to all servers.${RESET}"
        echo "──────────────────────────────"
        echo
        return
    fi

    if ((ans >= 1 && ans < number)); then

        server="${SERVERS[$((ans - 1))]}"
        map="${server%%|*}"
        path="${server#*|}"

        cp -f "$SHOP_FILE" \
            "$path/ShooterGame/Binaries/Win64/ArkApi/Plugins/ArkShop/config.json"

        echo
        echo "──────────────────────────────"
        echo -e "${CLR_GREEN}$map config.json updated.${RESET}"
        echo "──────────────────────────────"
        echo
        return
    fi

    echo -e "${CLR_RED}Invalid selection.${RESET}"
}

#
# Shop Logs
#
cmd_shop_logs() {

    if [[ -z "$1" ]]; then
        echo
        echo -e "${CLR_RED}Error:${RESET} No Map Specified!"
        echo "Usage: arkshop -l <map>"
        echo
        exit 1
    fi

    if [[ ! "${SERVERS[@],,}" =~ "${1,,}" ]]; then
        echo
        echo -e "${CLR_RED}Error:${RESET} ${1} Not found"
        echo " Use 'arkshop -m' to see which map you have configured in this CLI"
        echo
        exit 1
    fi

    echo
    echo -e " ▶ ${CLR_GOLD}Displaying ArkShop logs for $1 ${RESET}"
    echo
    sleep 2

    for server in "${SERVERS[@]}"; do
        server_name="${server%%|*}" # pass array content to server_name so we can convert it to lowercase
        if [[ "${1,,}" == "${server_name,,}" ]]; then
            path="${server#*|}"
            cat "${path}"/ShooterGame/Binaries/Win64/ArkApi/Plugins/ArkShop/*.log | colour_shop_logs
        fi
    done

    # ALL shop logs at once (too much)
    #for server in "${SERVERS[@]}"; do
    #   path="${server#*|}"
    #  cat "${path}"/ShooterGame/Binaries/Win64/ArkApi/Plugins/ArkShop/*.log | colour_shop_logs
    #done
}

cmd_shop_find() {

    if [[ -z "$1" ]]; then
        echo
        echo -e "${CLR_RED}Error:${RESET} No Search-term Specified!"
        echo "Usage: arkshop -f <Search>"
        echo
        exit 1
    fi

    echo
    echo -e " ▶ ${CLR_GOLD}Searching existing ArkShop logs for:${CLR_AQUA} '${1}' ${RESET}"
    echo
    sleep 1

    for server in "${SERVERS[@]}"; do
        server_path="${server#*|}"
        log_path="${server_path}/ShooterGame/Binaries/Win64/ArkApi/Plugins/ArkShop"
        grep -H -i --color=never "$1" "${log_path}"/*.log | colour_shop_logs
    done
}

colour_shop_logs() {
    while IFS= read -r line; do
        line=$(sed -E 's|.*/ShopLog_([^/]+)_WP\.log:|\1: |' <<<"$line")
        echo "$line"
    done
}

cmd_list_maps() {
    echo
    echo -e "${CLR_PURPLE}Servers you've defined in this CLI's Config:${RESET}"
    echo "───────────────────────────────────────────"
    for server in "${SERVERS[@]}"; do
        map="${server%%|*}"
        echo " - $map"
    done
}

####################################
# MAIN
####################################

case "${1:-}" in
    -ls)
        cmd_list "$@"
        ;;
    -s | --search)
        cmd_search "$2"
        ;;
    -a | --add)
        cmd_add "$@"
        ;;
    -d | --delete)
        cmd_delete "$@"
        ;;
    -r | --rollout)
        cmd_rollout "$@"
        ;;
    -l | --log)
        shift
        cmd_shop_logs "$@"
        ;;
    -f | --find)
        shift
        cmd_shop_find "$@"
        ;;
    -m | --maps)
        cmd_list_maps
        ;;
    *)
        usage
        ;;
esac