#!/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:pce|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 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 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 true 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 "$@"