Files
hascheevos2/hascheevos/bin/hascheevos.sh.bak
T
2020-12-18 20:09:36 +00:00

1194 lines
37 KiB
Bash
Executable File

#!/bin/bash
# hascheevos.sh
###############
#
# A tool to check if your ROMs have cheevos (RetroAchievements.org).
# globals ####################################################################
readonly USAGE="
USAGE:
$(basename "$0") [OPTIONS] romfile1 [romfile2 ...]"
readonly GIT_REPO="https://github.com/meleu/hascheevos.git"
readonly SCRIPT_URL="https://raw.githubusercontent.com/meleu/hascheevos/master/bin/hascheevos.sh"
readonly SCRIPT_DIR="$(cd "$(dirname $0)" && pwd)"
readonly DATA_DIR="$SCRIPT_DIR/../data"
readonly GAMEID_REGEX='^[1-9][0-9]{0,9}$'
readonly HASH_REGEX='[A-Fa-f0-9]{32}'
readonly URL="https://retroachievements.org"
#readonly URL='http://localhost'
# flags
CHECK_FALSE_FLAG=0
COPY_ROMS_FLAG=0
CHECK_RA_SERVER_FLAG=0
TAB_FLAG=0
ARCADE_FLAG=0
RA_USER=
RA_PASSWORD=
RA_TOKEN=
FILES_TO_CHECK=()
COPY_ROMS_DIR=
TMP_DIR="/tmp/hascheevos-$$"
mkdir -p "$TMP_DIR"
GAME_CONSOLE_NAME="$(mktemp -p "$TMP_DIR")"
# these will be increased later, based on extensions supported by the systems
EXTENSIONS='zip|7z'
SUPPORTED_SYSTEMS=()
declare -A CONSOLE_IDS
# format: [shortname]='consoleId:extension1|extensionN:Long Name:alias'
declare -A SYSTEMS_INFO=(
[megadrive]='1:bin|gen|md|sg|smd:Sega Mega Drive:genesis'
[n64]='2:z64|n64|v64:Nintendo 64'
[snes]='3:fig|mgd|sfc|smc|swc:Super Nintendo Entertainment System'
[gb]='4:gb:GameBoy'
[gba]='5:gba:GameBoy Advance'
[gbc]='6:gbc:GameBoy Color'
[nes]='7:nes|fds:fds:Nintendo Entertainment System:fds'
[pcengine]='8:ccd|chd|cue|:PC Engine:pcenginecd'
[segacd]='9:bin|chd|cue|iso:Sega CD'
[sega32x]='10:32x|bin|md|smd:Sega 32X'
[mastersystem]='11:bin|sms:Sega Master System'
[psx]='12:cue|ccd|chd|exe|iso|m3u|pbp|toc:PlayStation'
[atarilynx]='13:lnx:Atari Lynx'
[ngp]='14:ngp|ngc:NeoGeo Pocket [Color]:ngpc'
[gamegear]='15:bin|gg|sms:Game Gear'
[atarijaguar]='17:j64|jag:Atari Jaguar'
[nds]='18:nds:Nintendo DS'
[pokemini]='24:min:Pokemon Mini'
[atari2600]='25:a26|bin|rom:Atari 2600'
[arcade]='27:fba:Arcade:fbneo|fba'
[virtualboy]='28:vb:VirtualBoy'
[sg-1000]='33:bin|sg:SG-1000'
[coleco]='44:col|rom:ColecoVision'
[atari7800]='51:a78|bin:Atari 7800'
[wonderswan]='53:ws|wsc:WonderSwan [Color]:wonderswancolor'
)
# RetroPie specific variables
readonly RP_ROMS_DIR="$HOME/RetroPie/roms"
GAMELIST=
GAMELIST_BAK=
ROMS_DIR=()
SCRAPE_FLAG=0
COLLECTIONS_FLAG=0
SINGLE_COLLECTION_FLAG=0
# functions ###################################################################
function safe_exit() {
rm -rf "$TMP_DIR"
if [[ -f "$GAMELIST" && -f "$GAMELIST_BAK" ]]; then
diff "$GAMELIST" "$GAMELIST_BAK" > /dev/null && rm -f "$GAMELIST_BAK"
fi
exit "$1"
}
function urlencode() {
local LC_ALL=C
local string="$*"
local length="${#string}"
local char
for (( i = 0; i < length; i++ )); do
char="${string:i:1}"
if [[ "$char" == [a-zA-Z0-9.~_-] ]]; then
printf "$char"
else
printf '%%%02X' "'$char"
fi
done
printf '\n' # opcional
}
function join_by() {
local IFS="$1"
echo "${*:2}"
}
function fill_data() {
local shortname
local entry
local temp_extensions
for shortname in "${!SYSTEMS_INFO[@]}"; do
entry="${SYSTEMS_INFO[$shortname]}"
SUPPORTED_SYSTEMS+=("$shortname")
CONSOLE_IDS[$shortname]="$(cut -d: -f1 <<< "$entry")"
temp_extensions="$(
join_by '|' "$temp_extensions" "$(cut -d: -f2 <<< "$entry" )"
)"
done
EXTENSIONS="$(join_by '|' "$EXTENSIONS" $(tr '|' '\n' <<< "$temp_extensions" | sort -u) )"
# this subshell must NOT be quoted! ---^
}
function get_console_shortname_by_id() {
local id="$1"
local shortname
for shortname in "${!CONSOLE_IDS[@]}"; do
if [[ "$id" == "${CONSOLE_IDS[$shortname]}" ]]; then
echo "$shortname"
return 0
fi
done
return 1
}
function help_message() {
echo "$USAGE"
echo
echo "Where [OPTIONS] are:"
echo
# getting the help message from the comments in this source code
sed -n 's/^#H //p' "$0"
safe_exit 0
}
function check_dependencies() {
local cmd
local answer
local deps=(jq curl unzip 7z)
for cmd in "${deps[@]}"; do
if ! which "$cmd" >/dev/null 2>&1; then
if ! which apt-get >/dev/null 2>&1; then
echo "ERROR: missing dependency: $cmd" >&2
echo "To use this tool you need to install \"$cmd\" package. Please, install it and try again."
safe_exit 1
fi
echo "To use this tool you need to install \"$cmd\"."
echo "Do you want to install \"$cmd\" now? (if you're sure, type \"yes\" and press ENTER)"
read -p 'Answer: ' answer
if ! [[ "$answer" =~ ^[Yy][Ee][Ss]$ ]]; then
echo "Aborting..."
safe_exit 1
fi
[[ "$cmd" == 7z ]] && cmd=p7zip-full
sudo apt-get install "$cmd"
fi
done
}
function is_retropie() {
if [[ -d "$RP_ROMS_DIR" ]]; then
mkdir -p "$HOME/.emulationstation/collections"
return 0
fi
return 1
}
function regex_safe() {
echo "$@" | sed -e 's/[]\/$*.^|[]/\\&/g'
}
function get_game_title_hascheevos() {
echo "$@" | sed 's/^[^:]\+:[^:]\+://'
}
# XXX: this function needs more intensive tests
function update() {
local err_flag=0
local dir="$SCRIPT_DIR/.."
if [[ -d "$dir/.git" ]]; then
pushd "$dir" > /dev/null
if ! git pull --rebase 2>/dev/null; then
git fetch && git reset --hard origin/master || err_flag=1
fi
popd > /dev/null
else
echo "ERROR: \"$dir/.git\": directory not found!" >&2
echo "Looks like this tool wasn't installed as instructed in repo's README." >&2
echo "Aborting..." >&2
err_flag=1
fi
if [[ "$err_flag" != 0 ]]; then
echo "UPDATE: Failed to update." >&2
safe_exit 1
fi
# after updating, silently check hascheevos-local.txt files
check_hascheevos_file >/dev/null 2>&1
rm "$DATA_DIR/*.bkp" 2> /dev/null
echo
echo "UPDATE: The files have been successfully updated."
safe_exit 0
}
# Getting the RetroAchievements token
# input: RA_USER, RA_PASSWORD
# updates: RA_TOKEN
# exit if fails
# TODO: cache the token in some file?
function get_cheevos_token() {
if [[ -z "$RA_USER" ]]; then
echo "WARNING: undefined RetroAchievements.org user (see \"--user\" option)." >&2
return 1
fi
[[ -n "$RA_TOKEN" ]] && return 0
if [[ -z "$RA_PASSWORD" ]]; then
echo "WARNING: undefined RetroAchievements.org password (see \"--password\" option)." >&2
return 1
fi
RA_TOKEN="$(curl -s "$URL/dorequest.php?r=login&u=${RA_USER}&p=${RA_PASSWORD}" | jq -e -r .Token)"
if [[ "$?" -ne 0 || "$RA_TOKEN" == null || -z "$RA_TOKEN" ]]; then
echo "ERROR: cheevos authentication failed. Aborting..."
safe_exit 1
fi
}
function is_supported_system() {
local sys
local match="$1"
for sys in "${SUPPORTED_SYSTEMS[@]}"; do
[[ "$sys" == "$match" ]] && return 0
done
return 1
}
# download hashlibrary for a specific console
# $1 is the system shortname
function download_hashlibrary() {
local system="$1"
local json_file="$DATA_DIR/${system}_hashlibrary.json"
echo "--- getting the console hash library for \"$system\"..." >&2
curl -s "$URL/dorequest.php?r=hashlibrary&c=${CONSOLE_IDS[$system]}" \
| jq '.' > "$json_file" 2> /dev/null \
|| echo "ERROR: failed to download hash library for \"$system\"!" >&2
[[ -s "$json_file" ]] || rm -f "$json_file"
}
# if a valid system is given in $1, the function tries to update only the
# hashlib for that system.
# Otherwise update hashlibraries older than 1 day.
function update_hashlib() {
local line
local given_system="$1"
local file
local sys
echo "Checking JSON hash libraries..." >&2
for sys in "${SUPPORTED_SYSTEMS[@]}"; do
[[ -f "$DATA_DIR/${sys}_hashlibrary.json" ]] || download_hashlibrary "$sys"
done
echo "Done!" >&2
if [[ -n "$given_system" ]]; then
file="$DATA_DIR/${given_system}_hashlibrary.json"
# check if the file exists and is older than 1 minute
if [[ -n "$(find "$file" -mmin +1 2>/dev/null)" ]]; then
echo "Updating \"$given_system\" hashlib..." >&2
download_hashlibrary "$given_system" && echo "Done!" >&2
return "$?"
else
if [[ -f "$file" ]]; then
echo "The \"$given_system\" hashlib is already up-to-date." >&2
return 0
else
echo "ERROR: invalid system: \"$given_system\""
return 1
fi
fi
else
# update hashlibs older than one day
while read -r line; do
system="$(basename "${line%_hashlibrary.json*}")"
download_hashlibrary "$system"
done < <(find "$DATA_DIR" -type f -name '*_hashlibrary.json' -mtime +1)
fi
}
# Print (echo) the game ID of a given rom file
# This function try to get the game id from local *_hashlibrary.json files, if
# these files don't exist the script will try to get them from RA server.
# input:
# $1 is a rom file (should be previously validated with validate_rom_file())
# also needs RA_TOKEN
function get_game_id() {
local rom="$1"
local line
local hash
local hash_i
local gameid
local console_id=0
local console_shortname
echo -n > "$GAME_CONSOLE_NAME"
hash="$(get_rom_hash "$rom")" || return 1
while read -r line; do
echo "--- $line" >&2
hash_i="$(echo "$line" | sed 's/^\(SNES\|NES\|Genesis\|Lynx\|plain MD5\): //')"
line="$(grep -i "\"$hash_i\"" "$DATA_DIR"/*_hashlibrary.json 2> /dev/null)"
echo -n "$(basename "${line%_hashlibrary.json*}")" > "$GAME_CONSOLE_NAME"
gameid="$(echo ${line##*: } | tr -d ' ,')"
[[ $gameid =~ $GAMEID_REGEX ]] && break
done <<< "$hash"
if [[ "$CHECK_RA_SERVER_FLAG" -eq 1 && ! $gameid =~ $GAMEID_REGEX ]]; then
echo "--- checking at RetroAchievements.org server..." >&2
for hash_i in $(echo "$hash" | sed 's/^\(SNES\|NES\|Genesis\|Lynx\|plain MD5\): //'); do
echo "--- hash: $hash_i" >&2
gameid="$(curl -s "$URL/dorequest.php?r=gameid&m=$hash_i" | jq .GameID)"
if [[ $gameid =~ $GAMEID_REGEX ]]; then
# if the logic reaches this point, mark this game's console to download the hashlibrary
console_id="$(
curl -s "$URL/dorequest.php?r=patch&u=${RA_USER}&g=${gameid}&f=3&l=1&t=${RA_TOKEN}" \
| jq '.PatchData.ConsoleID'
)"
console_shortname="$(get_console_shortname_by_id "$console_id")"
echo "$console_shortname" > "$GAME_CONSOLE_NAME"
break
fi
done
fi
if [[ "$gameid" == 0 ]]; then
echo "--- WARNING: this ROM file doesn't feature achievements." >&2
return 1
fi
if [[ ! $gameid =~ $GAMEID_REGEX ]]; then
echo "--- unable to get game ID." >&2
return 1
fi
# if the logic reaches this point, we have a valid game ID
[[ -n "$console_shortname" ]] && download_hashlibrary "$console_id"
echo "$gameid"
}
# Check if a game has cheevos.
# returns 0 if yes; 1 if not; 2 if an error occurred
function game_has_cheevos() {
local gameid="$1"
local hascheevos_file
local boolean
local game_title
local patch_json
local console_id
local console_shortname
local number_of_cheevos
if [[ ! $gameid =~ $GAMEID_REGEX ]]; then
echo "ERROR: \"$gameid\" invalid game ID." >&2
return 1
fi
echo "--- game ID: $gameid" >&2
# check if $DATA_DIR exist.
if [[ ! -d "$DATA_DIR" ]]; then
echo "ERROR: \"$DATA_DIR\": directory not found!" >&2
echo "Looks like this tool wasn't installed as instructed in repo's README." >&2
echo "Aborting..." >&2
safe_exit 1
fi
if [[ "$CHECK_RA_SERVER_FLAG" -ne 1 ]]; then
hascheevos_file="$(grep -l "^$gameid:" "$DATA_DIR"/*_hascheevos-local.txt 2> /dev/null)"
[[ -f "$hascheevos_file" ]] || hascheevos_file="$(grep -l "^$gameid:" "$DATA_DIR"/*_hascheevos.txt 2> /dev/null)"
if [[ -f "$hascheevos_file" ]]; then
boolean="$( grep "^$gameid:" "$hascheevos_file" | cut -d: -f2)"
game_title="$(get_game_title_hascheevos "$(grep "^$gameid:" "$hascheevos_file")" )"
[[ -n "$game_title" ]] && echo "--- Game Title: $game_title" >&2
[[ "$boolean" == true ]] && return 0
[[ "$boolean" == false && "$CHECK_FALSE_FLAG" -eq 0 ]] && return 1
fi
fi
if [[ -z "$RA_TOKEN" ]]; then
get_cheevos_token || return $?
fi
echo "--- checking at RetroAchievements.org server..." >&2
patch_json="$(curl -s "$URL/dorequest.php?r=patch&u=${RA_USER}&g=${gameid}&f=3&l=1&t=${RA_TOKEN}")"
console_id="$(echo "$patch_json" | jq -e '.PatchData.ConsoleID')"
if [[ "$?" -ne 0 || "$console_id" -lt 1 || -z "$console_id" ]]; then
echo "--- WARNING: unable to find the Console ID for Game #$gameid!" >&2
return 1
fi
console_shortname="$(get_console_shortname_by_id "$console_id")"
hascheevos_file="$DATA_DIR/${console_shortname}_hascheevos-local.txt"
game_title="$(echo "$patch_json" | jq -e '.PatchData.Title')" || game_title=
[[ -n "$game_title" ]] && echo "--- Game Title: $game_title" >&2
number_of_cheevos="$(echo "$patch_json" | jq '.PatchData.Achievements | length')"
# if the game has no cheevos...
if [[ -z "$number_of_cheevos" || "$number_of_cheevos" -lt 1 ]]; then
sed -i "s/^${gameid}:true/${gameid}:false/" "$hascheevos_file" 2> /dev/null
if ! grep -q "^${gameid}:false" "$hascheevos_file" 2> /dev/null; then
echo "${gameid}:false:${game_title}" >> "$hascheevos_file"
sort -un "$hascheevos_file" -o "$hascheevos_file"
fi
return 1
fi
# if the logic reaches this point, the game has cheevos.
sed -i "s/^${gameid}:false/${gameid}:true/" "$hascheevos_file" 2> /dev/null
if ! grep -q "^${gameid}:true" "$hascheevos_file" 2> /dev/null; then
echo "${gameid}:true:${game_title}" >> "$hascheevos_file"
sort -un "$hascheevos_file" -o "$hascheevos_file"
fi
sleep 1 # XXX: a small delay to not stress the server
return 0
}
# print the hash of a given rom file
function get_rom_hash() {
local rom="$1"
local hash
local uncompressed_rom
local ret=0
if [[ "$ARCADE_FLAG" == 1 ]]; then
rom="$(basename "$rom")"
hash="$(echo -n "${rom%.*}" | md5sum | grep -Eo "^$HASH_REGEX")"
[[ "$hash" =~ ^$HASH_REGEX$ ]] || return 1
echo "$hash"
return 0
fi
case "$rom" in
*.zip|*.ZIP)
uncompressed_rom="$TMP_DIR/$(unzip -Z1 "$rom" | head -1)"
unzip -o -d "$TMP_DIR" "$rom" >/dev/null
validate_rom_file "$uncompressed_rom" || ret=1
;;
*.7z|*.7Z)
uncompressed_rom="$TMP_DIR/$(7z l -slt "$rom" | sed -n 's/^Path = //p' | sed '2q;d')"
7z e -y -bd -o"$TMP_DIR" "$rom" >/dev/null
validate_rom_file "$uncompressed_rom" || ret=1
;;
esac
if [[ $ret -ne 0 ]]; then
rm -f "$uncompressed_rom"
return $ret
fi
if [[ -n "$uncompressed_rom" ]]; then
hash="$($SCRIPT_DIR/cheevoshash "$uncompressed_rom")"
rm -f "$uncompressed_rom"
else
hash="$($SCRIPT_DIR/cheevoshash "$rom")"
fi
[[ "$hash" =~ :\ [^\ ]{32} ]] || return 1
echo "$hash"
}
# check if the file exists and has a valid extension
function validate_rom_file() {
local rom="$1"
if [[ -z "$rom" ]]; then
echo "ERROR: missing ROM file name." >&2
echo "$USAGE" >&2
return 1
fi
if [[ ! -f "$rom" ]]; then
echo "ERROR: \"$rom\": file not found!" >&2
return 1
fi
if [[ ! "${rom##*.}" =~ ^($EXTENSIONS)$ ]]; then
echo "ERROR: \"$rom\": invalid file extension." >&2
return 1
fi
return 0
}
# Check if a game has cheevos.
# returns 0 if yes; 1 if not; 2 if an error occurred
function rom_has_cheevos() {
local rom="$1"
validate_rom_file "$rom" || return 1
echo "Checking \"$rom\"..." >&2
[[ "$TAB_FLAG" == 1 ]] && echo -en "${rom/ (*/}"
local gameid
gameid="$(get_game_id "$rom")"
if [[ -z "$gameid" ]]; then
echo -e "\t"
return 1
fi
[[ "$TAB_FLAG" == 1 ]] && echo -en "\t$gameid"
game_has_cheevos "$gameid"
}
# check if the local hascheevos repository is synchronized with the remote one.
# returns
# 0 if yes
# 1 if no
# 2 if unable to check
function is_updated() {
local version_local
local version_remote
version_local=$(git -C "$SCRIPT_DIR" log -1 --format="%H") || return 2
version_remote=$(git ls-remote "$GIT_REPO" | head -1 | cut -f1) || return 2
[[ "$version_local" == "$version_remote" ]]
}
function check_hascheevos_files() {
local file_local
local file_orig
local file_pr # file for Pull Request
local line_local
local line_orig
local gameid
local bool_local
local bool_orig
local title_local
local title_orig
local ret
local updated
local pr_files
local ret=0
local tmp_ret
is_updated
ret="$?"
case "$ret" in
0) updated=true
;;
1) update=false
echo "ERROR: your hascheevos files are outdated. Perform an '--update' and try again." >&2
return "$ret"
;;
2) updated=false
echo "WARNING: unable to compare your local files with remote ones from hascheevos repository." >&2
return "$ret"
;;
esac
while read -r file_local; do
tmp_ret=0
file_orig="${file_local/-local/}"
file_pr="${file_orig/.txt/-PR.txt}"
[[ "$updated" == true ]] && cat "$file_orig" > "$file_pr"
echo
echo "Checking \"$(basename "$file_orig")\"..."
while read -r line_local; do
gameid=$(echo "$line_local" | cut -d: -f1)
line_orig=$(grep "^$gameid:" "$file_orig")
bool_local=$(echo "$line_local" | cut -d: -f2)
bool_orig=$( echo "$line_orig" | cut -d: -f2)
title_local="$(get_game_title_hascheevos "$line_local")"
title_orig="$( get_game_title_hascheevos "$line_orig")"
if [[ -z "$line_orig" ]]; then
echo "* there's no Game ID #$gameid ($title_local) on your \"$(basename "$file_orig")\"."
ret=3
tmp_ret=1
elif [[ "$bool_local" == "$bool_orig" ]]; then
if [[ "$title_local" == "$title_orig" ]]; then
sed -i "/$(regex_safe "$line_local")/d" "$file_local"
[[ -s "$file_local" ]] || rm "$file_local"
else
echo "* Game ID #$gameid is named $title_local locally but it's $title_orig in the original file."
ret=3
tmp_ret=1
fi
else
echo "* Game ID #$gameid ($title_local) is marked as \"$bool_local\" locally but it's \"$bool_orig\" in the original file."
ret=3
tmp_ret=1
fi
if [[ "$updated" == true && "$ret" != 0 ]]; then
sed -i "/^$gameid/d" "$file_pr"
echo "$line_local" >> "$file_pr"
sort -o "$file_pr" -un "$file_pr"
fi
done < "$file_local"
if [[ "$updated" == true ]]; then
diff -q "$file_pr" "$file_orig" >/dev/null && rm "$file_pr"
fi
done < <(find "$DATA_DIR" -type f -name '*_hascheevos-local.txt')
while read -r file_pr; do
file_orig="${file_pr/-PR.txt/.txt}"
if diff -q "$file_pr" "$file_orig" >/dev/null; then
rm "$file_pr"
else
pr_files+=("$file_pr")
fi
done < <(find "$DATA_DIR" -maxdepth 1 -name '*-PR.txt')
if [[ -n "$pr_files" ]]; then
# XXX: yeah! I shouldn't hardcode this thing, but it helps to keep the repo updated! :)
if [[ "$updated" == true && "$RA_USER" == meleu ]]; then
update_repository || echo "WARNING: hascheevos repository was NOT updated!" >&2
else
echo -e "\n-----"
echo "Consider helping to keep the hascheevos files synchronized with RetroAchievements.org data."
echo "Please, copy the output's content above and paste it in a new issue at https://github.com/meleu/hascheevos/issues"
echo "Attaching the file(s) below to your issue would be really useful:"
echo "${pr_files[@]}"
fi
fi
return "$ret"
}
# this function exists only for me, sorry :)
# needs to be called from check_hascheevos_files() to access its variables
function update_repository() {
[[ "$RA_USER" != meleu ]] && return 1
local file_bkp
local commit_msg=()
commit_msg=(-m "updated *_hascheevos.txt files ($(date +'%d-%b-%Y %H:%M'))")
pushd "$dir" > /dev/null
for file_pr in "${pr_files[@]}"; do
file_orig="${file_pr/-PR.txt/.txt}"
file_bkp="${file_orig/.txt/.bkp}"
cat "$file_orig" > "$file_bkp"
cat "$file_pr" > "$file_orig"
git add "$file_orig"
commit_msg+=(-m "$(basename "$file_orig")" )
done
echo
git commit "${commit_msg[@]}"
git push origin master
# revert things if failed to push
if [[ "$?" != 0 ]]; then
for file_pr in "${pr_files[@]}"; do
file_orig="${file_pr/-PR.txt/.txt}"
file_bkp="${file_orig/.txt/.bkp}"
cat "$file_bkp" > "$file_orig"
done
git reset --soft HEAD^
fi
popd > /dev/null
}
# a trick for getting the system based on the folder where the rom is stored
function get_rom_system() {
echo "$1" | sed 's|\(.*/RetroPie/roms/[^/]*\).*|\1|' | xargs basename
}
# add the game to the system specific custom collection
# XXX: RetroPie specific
function set_cheevos_custom_collection() {
[[ -f "$1" ]] || return 1
local set="$2"
local system
local rom_full_path="$1"
local collection_cfg
if [[ "$SINGLE_COLLECTION_FLAG" == 1 ]]; then
collection_cfg="$HOME/.emulationstation/collections/custom-achievements.cfg"
else
system=$(get_rom_system "$rom_full_path")
collection_cfg="$HOME/.emulationstation/collections/custom-achievements ${system}.cfg"
fi
if [[ -z "$set" || "$set" == true ]]; then
echo "$rom_full_path" >> "$collection_cfg"
elif [[ "$set" == false ]]; then
sed -i "/$(regex_safe "$rom_full_path")/d" "$collection_cfg"
else
return 1
fi
sort -o "$collection_cfg" -u "$collection_cfg" \
&& echo "--- This game has been added to \"$collection_cfg\"." >&2
}
# update gamelist.xml info
# TODO: will it be useful? this feature will be useful only if the related PR gets merged on ES.
# XXX: RetroPie specific
function set_cheevos_gamelist_xml() {
local set="$2"
local system
local rom_full_path="$1"
local rom
local game_name
local new_entry_flag
local has_cheevos_xml_element
system=$(get_rom_system "$rom_full_path")
[[ -f "$rom_full_path" ]] || return 1
rom="$(basename "$rom_full_path")"
# From https://github.com/RetroPie/EmulationStation/blob/master/GAMELISTS.md
# ES will check three places for a gamelist.xml in the following order, using
# the first one it finds:
# - [SYSTEM_PATH]/gamelist.xml
# - ~/.emulationstation/gamelists/[SYSTEM_NAME]/gamelist.xml
# - /etc/emulationstation/gamelists/[SYSTEM_NAME]/gamelist.xml
for GAMELIST in \
"$RP_ROMS_DIR/$system/gamelist.xml" \
"$HOME/.emulationstation/gamelists/$system/gamelist.xml" \
"/etc/emulationstation/gamelists/$system/gamelist.xml"
do
[[ -f "$GAMELIST" ]] && break
GAMELIST=
done
[[ -f "$GAMELIST" ]] || return 1
GAMELIST_BAK="${GAMELIST}-$(date +'%Y%m%d').bak"
[[ -f "$GAMELIST_BAK" ]] || cp "$GAMELIST" "$GAMELIST_BAK"
# if set != true, just delete <achievements> element (it's considered false).
if [[ "$set" != true ]]; then
xmlstarlet ed -L -d "/gameList/game[contains(path,\"$rom\")]/achievements" "$GAMELIST"
return "$?"
fi
# 0 means new entry
new_entry_flag="$(xmlstarlet sel -t -v "count(/gameList/game[contains(path,\"$rom\")])" "$GAMELIST")"
# 0 means no <achievements> xml element
has_cheevos_xml_element="$(xmlstarlet sel -t -v "count(/gameList/game[contains(path,\"$rom\")]/achievements)" "$GAMELIST")"
# if it's a new entry in gamelist.xml...
if [[ "$new_entry_flag" -eq 0 ]]; then
game_name="${rom%.*}"
xmlstarlet ed -L -s "/gameList" -t elem -n "game" -v "" \
-s "/gameList/game[last()]" -t elem -n "name" -v "$game_name" \
-s "/gameList/game[last()]" -t elem -n "path" -v "$rom_full_path" \
-s "/gameList/game[last()]" -t elem -n "achievements" -v "true" \
"$GAMELIST" || return 1
elif [[ "$has_cheevos_xml_element" -gt 0 ]]; then
xmlstarlet ed -L \
-u "/gameList/game[contains(path,\"$rom\")]/achievements" -v "true" \
"$GAMELIST" || return 1
else
xmlstarlet ed -L \
-s "/gameList/game[contains(path,\"$rom\")]" -t elem -n achievements -v "true" \
"$GAMELIST" || return 1
fi
echo "--- This game has been defined as having cheevos in \"$GAMELIST\"." >&2
}
function process_files() {
local f
readonly local max=10
# avoiding to stress the server
if [[ "$CHECK_RA_SERVER_FLAG" == 1 && "$#" -gt "$max" ]]; then
echo >&2
echo "ABORTING!" >&2
echo "Using the --check-ra-server option to check more than $max files isn't allowed!" >&2
return 1
fi
for f in "$@"; do
if rom_has_cheevos "$f"; then
if [[ "$TAB_FLAG" == 1 ]]; then
echo -e "\tx"
else
[[ "$COLLECTIONS_FLAG" -eq 1 ]] && set_cheevos_custom_collection "$f" true
[[ "$SCRAPE_FLAG" -eq 1 ]] && set_cheevos_gamelist_xml "$f" true
echo -n "--- \"" >&2
echo -n "$f"
echo "\" HAS CHEEVOS!" >&2
echo
fi
if [[ "$COPY_ROMS_FLAG" -eq 1 ]]; then
console_name="$(cat "$GAME_CONSOLE_NAME")"
mkdir -p "$COPY_ROMS_DIR/$console_name"
cp -v "$f" "$COPY_ROMS_DIR/$console_name"
fi
else
if [[ "$TAB_FLAG" == 1 ]]; then
# echo -e "\tno"
echo
else
echo -e "\"$f\" has no cheevos. :(\n" >&2
fi
fi
done
}
# helping to deal with command line arguments
function check_argument() {
# limitation: the argument 2 can NOT start with '-'
if [[ -z "$2" || "$2" =~ ^- ]]; then
echo "$1: missing argument" >&2
return 1
fi
}
function parse_args() {
local i
local ret
local oldIFS
while [[ -n "$1" ]]; do
case "$1" in
#H -h|--help Print the help message and exit.
#H
-h|--help)
help_message
;;
#H --update Update hascheevos files and exit.
#H
--update)
update
;;
#H -u|--user USER USER is your RetroAchievements.org username.
#H
-u|--user)
check_argument "$1" "$2" || safe_exit 1
shift
RA_USER="$1"
;;
#H -p|--password PASSWORD PASSWORD is your RetroAchievements.org password.
#H
-p|--password)
check_argument "$1" "$2" || safe_exit 1
shift
RA_PASSWORD="$(urlencode "$1")"
;;
# TODO: is it really necessary?
##H --token TOKEN TOKEN is your RetroAchievements.org token.
##H
--token)
check_argument "$1" "$2" || safe_exit 1
shift
RA_TOKEN="$1"
get_cheevos_token
;;
#H -g|--game-id GAME_ID Check if there are cheevos for a given GAME_ID and
#H exit. Accept game IDs separated by commas, ex: 1,2,3
#H Note: this option should be the last argument.
#H
-g|--game-id)
check_argument "$1" "$2" || safe_exit 1
ret=0
IFS=, # XXX: not sure if it will impact other parts
for i in $2; do
if game_has_cheevos "$i"; then
echo "--- Game ID $i HAS CHEEVOS!" >&2
else
echo "--- Game ID $i has no cheevos. :(" >&2
ret=1
fi
done
safe_exit "$ret"
;;
#H --hash CHECKSUM Check if there are cheevos for a given CHECKSUM and exit.
#H Note: this option should be the last argument.
#H
--hash)
local line
local gameid
check_argument "$1" "$2" || safe_exit 1
ret=0
if [[ ! $2 =~ ^$HASH_REGEX$ ]]; then
echo "--- invalid checksum: $2" >&2
safe_exit 1
fi
line="$(grep -i "\"$2\"" "$DATA_DIR"/*_hashlibrary.json 2> /dev/null)"
echo -n "$(basename "${line%_hashlibrary.json*}")" > "$GAME_CONSOLE_NAME"
gameid="$(echo ${line##*: } | tr -d ' ,')"
if [[ ! $gameid =~ $GAMEID_REGEX ]]; then
echo "--- unable to get game ID." >&2
safe_exit 1
fi
if game_has_cheevos "$gameid"; then
echo "--- Game ID $gameid HAS CHEEVOS!" >&2
else
echo "--- Game ID $gameid has no cheevos. :(" >&2
ret=1
fi
safe_exit "$ret"
;;
#H --get-hashlib SYSTEM Download JSON hash library for a given SYSTEM (console)
#H and exit.
#H
--get-hashlib)
check_argument "$1" "$2" || safe_exit 1
shift
update_hashlib "$1"
safe_exit "$?"
;;
#H -f|--check-false Check at RetroAchievements.org server even if the
#H game ID is marked as "has no cheevos" (false) in
#H the local *_hascheevos.txt files.
#H
-f|--check-false)
CHECK_FALSE_FLAG=1
;;
#H -a|--arcade Arcade hashes are calculated agains the ROM filename
#H and then needs a different treatment. Use -a to check
#H arcade ROM files.
#H
-a|--arcade)
ARCADE_FLAG=1
;;
#H -t|--tab-output Instead of the normal output, the -t option makes
#H it be as in this example:
#H Game With Cheevos yes
#H Game With No Cheevos no
#H
-t|--tab-output)
TAB_FLAG=1
;;
# XXX: is it a good idea to let users use the script this way? can stress the server
# answer: it's useful to check if a game doesn't have cheevos anymore.
#H -r|--check-ra-server Force checking info at RetroAchievements.org server
#H ignoring some info you may have locally.
#H Note: do NOT use this option to check many files at once.
#H
-r|--check-ra-server)
CHECK_RA_SERVER_FLAG=1
;;
#H -d|--copy-roms-to DIR Create a copy of the ROMs that has cheevos and put
#H them at "DIR/CONSOLE_NAME/". There's no need to
#H specify the console name, the script detects it.
#H
-d|--copy-roms-to)
check_argument "$1" "$2" || safe_exit 1
shift
COPY_ROMS_FLAG=1
COPY_ROMS_DIR="$1"
;;
#H -c|--check-hascheevos Check if your local data is synchronized with the
#H repository, print a report and exit.
#H
-c|--check-hascheevos)
if check_hascheevos_files; then
echo "Your hascheevos files are up-to-date."
safe_exit "0"
fi
safe_exit "1"
;;
# TODO: is it really necessary?
##H --print-token Print the user's RetroAchievements.org token and exit.
##H
--print-token)
get_cheevos_token
echo "$RA_TOKEN"
safe_exit 0
;;
# TODO: will it be useful? this feature will be useful only if the related PR will be merged on ES.
##H --scrape [RETROPIE ONLY] Updates the gamelist.xml file with
##H <achievements>true</achievements> if the ROM has
##H cheevos.
##H
--scrape)
if ! is_retropie; then
echo "ERROR: not a RetroPie system." >&2
echo "The \"$1\" option is available only for RetroPie systems." >&2
safe_exit 1
fi
SCRAPE_FLAG=1
;;
#H --collection [RETROPIE ONLY] Creates a custom collection file
#H to use on RetroPie's EmulationStation. The resulting
#H files will be named as
#H "~/.emuationstation/collections/custom-SYSTEM achievements.cfg"
#H and filled with full paths for ROMs that have cheevos.
#H
--collection)
if ! is_retropie; then
echo "ERROR: not a RetroPie system." >&2
echo "The \"$1\" option is available only for RetroPie systems." >&2
safe_exit 1
fi
COLLECTIONS_FLAG=1
;;
#H --single-collection [RETROPIE ONLY] Creates one big custom collection file
#H to use on RetroPie's EmulationStation. The resulting
#H file will be named
#H "~/.emuationstation/collections/custom-achievements.cfg"
#H and filled with full paths to ALL ROMs that have cheevos.
#H
--single-collection)
if ! is_retropie; then
echo "ERROR: not a RetroPie system." >&2
echo "The \"$1\" option is available only for RetroPie systems." >&2
safe_exit 1
fi
COLLECTIONS_FLAG=1
SINGLE_COLLECTION_FLAG=1
;;
#H -s|--system SYSTEM [RETROPIE ONLY] Check if each ROM in the respective
#H "~/RetroPie/roms/SYSTEM" directory has cheevos. You
#H can specifie multiple systems separeted by commas or
#H use "all" to check all supported systems' directory.
#H
-s|--system)
local directories=()
if ! is_retropie; then
echo "ERROR: not a RetroPie system." >&2
echo "The \"$1\" option is available only for RetroPie systems." >&2
safe_exit 1
fi
check_argument "$1" "$2" || safe_exit 1
shift
if [[ "$1" == all ]]; then
directories=("${SUPPORTED_SYSTEMS[@]}")
else
oldIFS="$IFS"
IFS=, # XXX: not sure if it will impact other parts
for i in $1; do
directories+=("$i")
done
IFS="$oldIFS"
fi
for i in "${directories[@]}"; do
if [[ -d "$RP_ROMS_DIR/$i" ]]; then
ROMS_DIR+=("$RP_ROMS_DIR/$i")
continue
fi
echo "WARNING: ignoring \"$(basename "$i")\": not found." >&2
done
;;
*) break
;;
esac
shift
done
FILES_TO_CHECK=("$@")
}
# START HERE ##################################################################
function main() {
trap safe_exit SIGHUP SIGINT SIGQUIT SIGKILL SIGTERM
if [[ "$(id -u)" == 0 ]]; then
echo "ERROR: You can't use this script as super user." >&2
echo " Please, try again as a regular user." >&2
safe_exit 1
fi
check_dependencies
[[ -z "$1" ]] && help_message
fill_data
update_hashlib
parse_args "$@"
if is_retropie && [[ -n "$ROMS_DIR" ]]; then
local line
while read -r line; do
FILES_TO_CHECK+=("$line")
done < <(find "${ROMS_DIR[@]}" -type f -regextype egrep -iregex ".*\.($EXTENSIONS)$")
fi
process_files "${FILES_TO_CHECK[@]}"
safe_exit "$?"
}
main "$@"