#writelog "INFO" "${FUNCNAME[0]} - Last set in file has ID '$OLDSET', so continuing with '$OLDSET'"
# Get our APi on https://www.steamgriddb.com/profile/preferences/api/
SGDBAPIKEY="."
function checkSGDbApi {
if[-z"$SGDBAPIKEY"]||["$SGDBAPIKEY"=="$NON"];then
return 1
else
#writelog "INFO" "${FUNCNAME[0]} - Creating new $SCPATH"
printf'\x00%s\x00'"shortcuts">"$SCPATH"
NEWSET=0
return 0
fi
}
#writelog "INFO" "${FUNCNAME[0]} - Adding new set '$NEWSET'"
## How Non-Steam AppIDs work, because it took me almost a year to figure this out
## ----------------------
## Steam stores shortcuts in a binary 'shortcuts.vdf', at SROOT/userdata/<id>/config
##
## Non-Steam AppIDs are 32bit little-endian (reverse byte order) signed integers, stored as hexidecimal
## This is probably generated using a crc32 generated from AppName + Exe, but it can actually be anything
## Steam likely does this to ensure "uniqueness" among entries, tools like Steam-ROM-Manager do the same thing likely for similar reasons
##
## For simplicity we generate a random 32bit signed integer using an md5, which we'll then convert to hex to store in the AppID file
## Though we can write any AppID we want, Steam will reject invalid ones (i.e. big endian hex) it will overwrite our AppID
## We can also convert this to an unsigned 32bit integer to get the AppID used for grids and other things, the unsigned int is just what Steam stores
##
## We can later re-use these functions to do several things:
## - Check for and remove stray STL configs for no longer stored Non-Steam Game AppIDs (if we had Non-Steam Games we previously used with STL that we no longer use, we can remove these configs in case there is a conflict in future)
{
printf'\x00%s\x00'"$NEWSET"
printf'\x02%s\x00%b'"appid""$NOSTAIDHX"
printf'\x01%s\x00%s\x00'"appname""$NOSTAPPNAME"
printf'\x01%s\x00%s\x00'"Exe""$NOSTEXEPATH"
printf'\x01%s\x00%s\x00'"StartDir""$NOSTSTDIR"
### BEGIN MAGIC APPID FUNCTIONS
## ----------
# Generate random signed 32bit integer which can be converted into hex, using the first argument (AppName and Exe fields) as seed (in an attempt to reduce the chances of the same AppID being generated twice)
function generateShortcutVDFAppId {
seed="$(echo-n"$1" | md5sum | cut-c1-8)"
echo"-$((16#${seed} % 1000000000 ))"
}
if[-n"$NOSTICONPATH"];then
printf'\x01%s\x00%s\x00'"icon""$NOSTICONPATH"
else
printf'\x01%s\x00\x00'"icon"
fi
function dec2hex {
printf'%x\n'"$1"|cut-c 9-# cut removes the 'ffffffff' from the string (represents the sign) and starts from the 9th character
}
printf'\x01%s\x00\x00'"ShortcutPath"
# Takes big-endian ("normal") hexidecimal number and converts to little-endian
# Takes an signed 32bit integer and converts it to an unsigned 32bit integer
function generateShortcutGridAppId {
echo$(($1&0xFFFFFFFF ))
}
## ----------
### END MAGIC APPID FUNCTIONS
if["$NOSTAO"-eq 1 ];then
printf'\x02%s\x00\x01\x00\x00\x00'"AllowOverlay"
else
printf'\x02%s\x00\x00\x00\x00\x00'"AllowOverlay"
fi
NOSTAIDVDF="$(generateShortcutVDFAppId "${NOSTAPPNAME}${NOSTEXEPATH}")"# signed integer AppID, stored in the VDF as hexidecimal - ex: -598031679
NOSTAIDVDFHEX="$( generateShortcutVDFHexAppId "$NOSTAIDVDF")"# 4byte little-endian hexidecimal of above 32bit signed integer, which we write out to the binary VDF - ex: c1c25adc
NOSTAIDVDFHEXFMT="\x$(awk'{$1=$1}1'FPAT='.{2}'OFS="\\\x"<<<"$NOSTAIDVDFHEX")"# binary-formatted string hex of the above which we actually write out - ex: \xc1\xc2\x5a\xdc
NOSTAIDGRID="$( generateShortcutGridAppId "$NOSTAIDVDF")"# unsigned 32bit ingeger version of "$NOSTAIDVDF", which is used as the AppID for Steam artwork ("grids"), as well as for our shortcuts
if["$NOSTVR"-eq 1 ];then
printf'\x02%s\x00\x01\x00\x00\x00'"openvr"
else
printf'\x02%s\x00\x00\x00\x00\x00'"openvr"
# Set artwork for Steam game by copying/linking/moving passed artwork to steam grid folder
function setGameArt {
function applyGameArt {
GAMEARTAPPID="$1"
GAMEARTSOURCE="$2"# e.g. /home/gaben/GamesArt/cs2_hero.png
GAMEARTSUFFIX="$3"# e.g. "_hero" etc
GAMEARTCMD="$4"
GAMEARTBASE="$(basename"$GAMEARTSOURCE")"
GAMEARTDEST="${SGGRIDDIR}/${GAMEARTAPPID}${GAMEARTSUFFIX}.${GAMEARTBASE#*.}"# path to filename in grid e.g. turns "/home/gaben/GamesArt/cs2_hero.png" into "~/.local/share/Steam/userdata/1234567/config/grid/4440654_hero.png"
if[-n "$GAMEARTSOURCE"];then
if[-f "$GAMEARTDEST"];then
rm"$GAMEARTDEST"
fi
if[-f "$GAMEARTSOURCE"];then
$GAMEARTCMD"$GAMEARTSOURCE""$GAMEARTDEST"
fi
fi
}
GAME_APPID="$1"# We don't validate AppID as it would drastically slow down the process for large libraries
SETARTCMD="cp"# Default command will copy art
for i in"$@";do
case$iin
-hr=*|--hero=*)
SGHERO="${i#*=}"# <appid>_hero.png -- Banner used on game screen, logo goes on top of this
shift;;
-lg=*|--logo=*)
SGLOGO="${i#*=}"# <appid>_logo.png -- Logo used e.g. on game screen
shift;;
-ba=*|--boxart=*)
SGBOXART="${i#*=}"# <appid>p.png -- Used in library
shift;;
-tf=*|--tenfoot=*)
SGTENFOOT="${i#*=}"# <appid>.png -- Used as small boxart for e.g. most recently played banner
shift;;
--copy)
SETARTCMD="cp"# Copy file to grid folder -- Default
# This is formatted as a flag because we can pass "$SGACOPYMETHOD" as an argument to setGameArt, and it will be interpreted as --copy
SGACOPYMETHOD="${SGACOPYMETHOD:---copy}"
## Generic function to fetch some artwork from SteamGridDB based on an endpoint
## TODO: Steam only officially supports PNGs, test to see if WebP works when manually copied, and if it doesn't, we should try to only download PNG files
## TODO: Add max filesize option? Some artworks are really big, we should skip ones that are too large (though this may mean many animated APNG artworks will get skipped, because APNG can be huge)
# Used to get either Steam or Non-Steam artwork depending on a flag -- Used internally and for commandline usage
function commandlineGetSteamGridDBArtwork {
GSGDBA_HASFILE="$SGDBHASFILE" # Optional override for how to handle existinf file (downloadArtFromSteamGridDB defaults to '$SGDBHASFILE')
GSGDBA_APPLYARTWORK="$SGDBDLTOSTEAM"
GSGDBA_SEARCHNAME=""
GSGDBA_FOUNDGAMEID="" # ID found from SteamGridDB endpoint using GSGDBA_SEARCHNAME
for i in "${@}"; do
case $i in
--search-name=*)
GSGDBA_SEARCHNAME="${i#*=}" # Optional SteamGridDB Game Name -- Will use this to try and find matching SteamGridDB Game Art
shift ;;
--nonsteam)
SGDBENDPOINTTYPE="game"
shift ;;
--filename-appid=*)
GSGDBA_FILENAME="${i#*=}" # AppID to use in filename (Non-Steam Games need a different AppID)
shift ;;
## Override Global Menu setting for how to handle existing artwork
## in case user wants to replace all existing artwork, default STL setting is 'skip' and will only copy files over to grid dir if they don't exist, so user can easily fill in missing artwork only)
--replace-existing)
GSGDBA_HASFILE="replace"
shift ;;
--backup-existing)
GSGDBA_HASFILE="backup"
shift ;;
## Flag to force downloading to SteamGridDB folder (used for addNonSteamGame internally)
--apply)
GSGDBA_APPLYARTWORK="1"
shift ;;
esac
done
# If we pass a name to search on and we get a Game ID back from SteamGridDB, set this as the ID to search for artwork on
if [ -n "$GSGDBA_SEARCHNAME" ]; then
if [ -n "$GSGDBA_FILENAME" ]; then
#writelog "INFO" "${FUNCNAME[0]} - Searching SteamGridDB for game name matching '$GSGDBA_SEARCHNAME'"
#writelog "INFO" "${FUNCNAME[0]} - Found game name matching '$GSGDBA_SEARCHNAME' with Game ID '$GSGDBA_FOUNDGAMEID' -- Using this Game ID to search for SteamGridDB Game Art"
GSGDBA_APPID="$GSGDBA_FOUNDGAMEID"
#writelog "INFO" "${FUNCNAME[0]} - Forcing endpoint type as --nonsteam since we're searching with a found SteamGridDB Game ID"
SGDBENDPOINTTYPE="game"
fi
else
echo "You must provide a filename AppID when searching with SteamGridDB Game Name"
SGDBSEARCHENDPOINT_BOXART="${BASESTEAMGRIDDBAPI}/grids/${SGDBENDPOINTTYPE}" # Grid endpoint is used for Boxart and Tenfoot, which SteamGridDB counts as vertical/horizontal grids respectively
# Download Hero, Logo, Boxart, Tenfoot from SteamGridDB from given endpoint using given AppID
# On SteamGridDB tenfoot called horizontal Steam grid, so fetch it by passing specific dimensions matching this -- Users can override this, but default is what SteamGridDB expects for the tenfoot sizes
# The entered search name is prioritised over actual game EXE name, only one will be used and we will always prefer custom name
# Ex: user names Non-Steam Game "The Elder Scrolls IV: Oblivion" but they enter a custom search name because they want artwork for "The Elder Scrolls IV: Oblivion Game of the Year Edition"
# In case art is not found for the custom name, users should enter either the Steam AppID or the SteamGridDB Game ID to use as a fallback (Steam AppID will always be preferred because it will always be exact)
#
# Therefore, the order of priority for artwork searching is:
# 1. Name search (only ONE of the below will be used)
# a. If the user enters a custom search name with --steamgriddb-game-name, search on that
# b. Otherwise, use the Non-Steam Game name
# 2. Fallback to ID search if no SteamGridDB ID is found on the name search
# a. If the user enters a Steam AppID with --steamgriddb-steam-appid, search on that
# b. Otherwise, fall back to searching on an entered SteamGridDB Game ID
# In short, search on ONE of the names, and if a Game ID is not found on either of these, fall back to searching on ONE of the passed IDs
# If no IDs are found after all of this, we can't get artwork. We will not fall back to EXE name if no ID is found on custom name, and we will not fall back to SteamGridDB Game ID if no art is found for Steam AppID
# If no values are provided we will simply search on Non-Steam Game name
NOSTSEARCHNAME="" # Name to search for SteamGridDB Game ID on (either custom name or app name)
NOSTSEARCHID="" # ID to search for the SteamGridDB artwork on (either Steam AppID or SteamGridDB Game ID)
NOSTSEARCHFLAG="--nonsteam" # Whether to search using a Steam AppID or SteamGridDB Game ID (will be set to --steam if we get an AppID)
# Only add NOSTAPPNAME as fallback if we don't have an ID to search on, because commandlineGetSteamGridDBArtwork will prefer name over ID, so if we have to fall back to Non-Steam Name (i.e. no entered custom name) then only do so if we don't have an ID given
if [ -n "$NOSTAPPNAME" ]; then
NOSTSEARCHNAME="$NOSTAPPNAME"
fi
# Store the ID we searched with, so getSteamGridDBNonSteamIcon doesn't have to hit the endpoint again and we save an API call