#!/bin/bash #steam-compatdata-symlinker #by: Mouse #Current build: Feb 11, 2023 ################### #user config: ################### #the ln command with switches: #change this command if you want this script to change how it creates links: cmd="ln -sTf" #directory filters: #you can add your own filters here for the directories you want ignored. #filters must be exact #in other words, they're case sensitive and each game name or ID must be written here exactly as it appears in the directory name. #example 1: the filter "Fallout" will not filter out the game Fallout: New Vegas #example 2: the filter "Fallout: New Vegas" will not filter out any Fallout. Nor will it filter out the game it was intended to filter either, because the directory is named "Fallout New Vegas" and not "Fallout: New Vegas" #note: filter checks aren't applied to broken symlinks filter=("Steam Controller Configs" "0") #custom directory overrides: #put your custom directory overrides here. #don't end paths with a slash ("/") #use $HOME to reference your home directory #example: commondir="$HOME/Games/mycustomsteamapps/common" commondir="" compatdatadir="" shadercachedir="" ################### #end of user config ################### countlines(){ grep "" -c "$1" } readline(){ if [[ $1 && $2 ]]; then max="$(countlines "$1")" if (( $2 > $max )); then sed -n "${max}p" "$1" else sed -n "${2}p" "$1" fi fi } #this lets us look for a corresponding symlink to give us a hint as to what game this compatdata was originally for lnsearchsub(){ str="$2" linktarget="$(readlink -f "$1")" if [[ "$linktarget" == *"$str"* ]]; then echo "Found symlink for $(basename "$linktarget"). It's likely $(basename "$1")'s compatdata directory." fi } lnsearch(){ if (( $# >= 2 )); then export -f lnsearchsub find "$2" -maxdepth 1 -type l -exec bash -c "lnsearchsub \"{}\" \"$1\"" \; else echo "Usage: $0 " fi } isfiltered(){ y=false for x in "${filter[@]}"; do if [[ "$x" == "$1" ]]; then y=true break fi done echo "$y" } lnfn(){ counter=0 for i in "$thedir/"*".acf"; do id="${i/"$thedir/appmanifest_"/""}" id="${id/".acf"/""}" if [[ "$(isfiltered "$id")" == "false" ]]; then if [[ -d "$compatdatadir/$id" ]]; then #line for undoing when $compatdatadir/$id/$id" was created by ln. #(I had to use -f and -T to prevent this. I had no idea ln behaved like this.) # if [[ -d "$compatdatadir/$id/$id" ]]; then # gio remove "$compatdatadir/$id/$id" # fi name="$(readline "$i" "5")" name="${name/"\"name\""/""}" name="${name//" "/""}" name="${name//"\""/""}" if [[ "$name" =~ ^[0-9]+$ ]] && [[ -e "$compatdatadir/$name" ]] && ! [[ -L "$compatdatadir/$name" ]]; then echo echo "WARNING: $compatdatadir/$name already exists, is all numbers, and isn't a symlink. This seems like the rare situation where a game, who's name is all numbers, can conflict with compatdata directories and the symlinks this script creates." echo "As a precaution, this script will only report the conflict. It's on you to create a symlink, if you really want one." echo "To stop getting warnings about $name, add it to the filter list at the top of this script." echo "Skipping $name." echo else if [[ "$1" == "test" ]]; then echo "$cmd \"$compatdatadir/$id\" \"$compatdatadir/$name\"" fi if [[ "$1" == "run" ]]; then $cmd "$compatdatadir/$id" "$compatdatadir/$name" fi fi counter=$(( counter + 1 )) # else #I guess it's technically an error but steam downloads appmanifest files for all of a game's subdepo's, so I guess we don't need to report on every single one. Or really any of them. # echo "ERROR: Missing target directory for ($name): $compatdatadir/$id" # echo "Skipping." fi fi done } usage(){ echo "Usage: $0 [ \"steamapps directory\" ]" echo "This will automatically make symlinks for all directories in the supplied /steamapps/compatdata/ directory." echo "This makes it easier to find the right compatdata directory without needing to look up the game's Steam ID." echo "Run this script again to create symlinks for new game installs, and clean up old uninstalls." echo "This script will also check for unused compatdata, shadercache, and common directories, left behind from uninstalls and moved installs." echo "-h for this dialog." echo "-e for a longer explanation on these unused directories." } longexplain(){ echo "There are multiple reasons why unused directories would exist, such as the game was uninstalled or moved to a different drive, another program made changes, a user made changes, etc etc." echo "But the important part is, as far as any directories reported by this script goes, Steam would be unaware of their existence and wouldn't be doing anything with them." echo echo "If you would like to know what game a directory is tied to, type the full name of the folder (the number(which is the steamid)) into your Steam library. It will automatically show you the game." echo echo "Whether or not these directories should be deleted is left to your discretion. But be aware your user data could be inside of them, including save files. So I advise at least looking around in the directory for valuable data first before deleting it." echo echo "If it's a compatdata directory, then look in the program files and steamuser directories." echo "If it's a steamapps/common directory, then obviously just look in the game's install directory." echo "If it's a shadercache directory, then you can safely delete that directory, unless you plan on reinstalling that game, on that same steamapps drive, sometime in the near future. This could save Steam from recompiling the shader cache. But even then, Steam may choose to do it anyways, so it's left to your discretion." echo echo "NOTE: This script doesn't support non-Steam games. To find which compatdata directory holds the non-Steam game in question, run this:" echo "find ~/path/to/your/steamapps/compatdata/ -iname *game*name*here* 2>/dev/null" echo echo "NOTE: Every steamapps directory has its own common, compatdata, and shadercache directories. If you have games installed on multiple drives, even moving a game from one drive to another can leave behind some of these directories. So just because Steam says a game is installed doesn't mean it's installed to the specific steamapps directory you supplied, which would mean these directories are still quite likely not in use. Again, this is all left to your discretion." } if (( $# >= 1 )); then while getopts "he" OPTION; do case "${OPTION}" in h) usage exit ;; e) longexplain exit ;; \?) exit ;; esac done shift "$((OPTIND -1))" thedir="$(realpath "$1")" if [[ "$compatdatadir" == "" ]]; then compatdatadir="$thedir/compatdata" fi if [[ "$commondir" == "" ]]; then commondir="$thedir/common" fi if [[ "$shadercachedir" == "" ]]; then shadercachedir="$thedir/shadercache" fi if [[ -d "$compatdatadir" ]]; then echo "The following commands will run:" lnfn "test" echo echo "$counter symlinks will be created." echo "Continue? [ y / n ]" read -e input if [[ "${input,,}" == "y"* ]]; then lnfn "run" echo echo "Done!" echo "$counter symlinks created." echo else echo "Canceled!" echo fi #now let's find broken symlinks asdf="$(find "$compatdatadir/" -maxdepth 1 -xtype l)" if [[ "$asdf" == "" ]]; then echo "No broken symlinks found." else echo "--------------------------------" echo "Broken symlinks found:" echo "$asdf" echo "--------------------------------" echo fi unaccountedfound=0 #now let's look for unused compatdata directories found=0 for number in "$compatdatadir/"*; do if [[ -d "$number" ]] && ! [[ -L "$number" ]]; then numberbn="$(basename "$number")" if [[ "$(isfiltered "$numberbn")" == "false" ]]; then if ! [[ -e "$thedir/appmanifest_""$numberbn"".acf" ]]; then if [[ $found == 0 ]]; then found=1 unaccountedfound=1 echo "--------------------------------" echo "Unused compatdata directory(s) found:" fi echo "$number (link: https://steamdb.info/app/$numberbn)" lnsearch "$number" "$compatdatadir/" fi fi fi done if [[ $found == 1 ]]; then echo "--------------------------------" echo else echo "No unused compatdata directories found." fi #let's look for unused ../steamapps/common directories too while we're at it. if [[ -d "$commondir" ]]; then found=0 for d in "$commondir/"*; do if [[ -d "$d" ]] && ! [[ -L "$d" ]]; then asdf="$(basename "$d")" if [[ "$(isfiltered "$asdf")" == "false" ]]; then if ! [[ "$(grep -ie "$asdf" "$thedir/app"*)" == *"$asdf"* ]]; then if [[ $found == 0 ]]; then found=1 unaccountedfound=1 echo "--------------------------------" echo "Unused common directory(s) found:" fi echo "$commondir/$asdf ($asdf)" fi fi fi done if [[ $found == 1 ]]; then echo "--------------------------------" echo else echo "No unused common directories found." fi else echo "ERROR: An invalid common directory has been set:" echo "'$commondir': $commondir" echo "Skipping." echo fi #and, what the hell, let's look for unused shader directories too I guess. we might as well since we're already wiping all of these other butts. if [[ -d "$shadercachedir" ]]; then found=0 for number in "$shadercachedir/"*; do if [[ -d "$number" ]] && ! [[ -L "$number" ]]; then numberbn="$(basename "$number")" if [[ "$(isfiltered "$numberbn")" == "false" ]]; then if ! [[ -e "$thedir/appmanifest_""$numberbn"".acf" ]]; then if [[ $found == 0 ]]; then found=1 unaccountedfound=1 echo "--------------------------------" echo "Unused shadercache directory(s) found:" fi echo "$number (link: https://steamdb.info/app/$numberbn)" fi fi fi done if [[ $found == 1 ]]; then echo "--------------------------------" echo else echo "No unused shadercache directories found." fi else echo "ERROR: An invalid shadercache directory has been set:" echo "'$shadercachedir': $shadercachedir" echo "Skipping." echo fi if [[ $unaccountedfound == 1 ]]; then echo "These unused directories seem to have no configuration files and are likely not in use." echo "For a longer explanation, run:" echo "$0 -e" fi else echo "ERROR: compatdata directory not found." echo "Ensure you are supplying a valid steamapps directory." echo "Exiting." fi else usage fi