Steam Paths: Attempt to set userdata path using MostRecent loginuser UserID #1141
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes #1140.
Overview
This PR builds the path to the
userdata
folder and selects the current Steam UserID based on the most recent user in Steam'sloginusers.vdf
. This VDF file has blocks for each Steam account currently logged into the Steam Client, and the one that was last logged into has its"MostRecent"
field set to"1"
. We store some information about this file into a CSV file and then read that insetSteamPaths
. If the current row has"MostRecent"
as"1"
then we select that Short UserID row to build the path to theuserdata
folder.This means instead of our current solution of picking the first result of the
find
command that matches our regex, we will try to use the Steam Account marked with"MostRecent"
as"1"
inloginusers.vdf
. However if we cannot find or parseloginusers.vdf
then we will fall back to using thisfind
command as a last resort. This value is written into the CSV as the only user, meaning that in cases where we can find a Steam User, we will always write it into the CSV, so it should always have at least one row.Alternatively, the
STEAMUSERID
variable can still be set in the global config to enforce a specific Steam Account, and all of this logic is ignored. This is not new logic, but is not something I knew before looking into this.If we cannot parse
loginusers.vdf
to find the Most Recent user, then we log a warning and move on.Background
Steam has a
userdata
folder in its install location. This stores some user-specific Steam information, such as theshortcuts.vdf
file and thelocalconfig.vdf
file. We do use it for other things, but this the crux of #1140.The
userdata
folder identifies users based on their UserID, specifically the Short UserID. So if a Short UserID is12345
, then theuserdata
folder is$SROOT/userdata/12345
(where$SROOT
is the Steam install location).Since the Steam Client can have multiple users, there may be multiple folders in this directory. However there are two we can ignore:
0
andac
. We don't need to care about these folders. Every UserID folder we care about will start with at least1
and should always be at least two digits (i.e. there can't be a UserID of just1
).Since there can be multiple users, SteamTinkerLaunch just runs
find
in theuserdata
folder and picks the first result (probably the one with the smallest UserID). This works fine for most users because there is usually only one user account logged into the Steam Client on a PC at any given time... Or at least this has historically been true!Problem
Since we just pick the first userdata folder, if there are multiple accounts, this means that without setting
STEAMUSERID=
in the global config file, SteamTinkerLaunch may not use the right user account! If you have two accounts, Main and Alt, if Main's userdata folder is returned before Alt's, then by default SteamTinkerLaunch will always use Main's userdata folder.This becomes a problem for a few reasons but one example is for adding Non-Steam Games. If you want to add a shortcut for Main and Alt, by default the first result of
find
will always be used. So without modifying the global config, shortcuts will always be added to whatever the first match withfind
is.Solution
In order to determine which userdata folder we should use, we can check for the most recently logged in user in Steam's internal file that tracks some information about accounts currently logged into the Steam Client. This file is at
$SROOT/config/loginusers.vdf
. Each block in this file under theusers
section has a Long UserID, say"1234567890"
. Inside this block is a field for each account called"MostRecent"
which tracks the account which was last logged in to.These Long UserIDs can be converted down into short UserIDs using
LongUserID & 0xFFFFFFFF
, identical to how we convert Non-Steam Game AppIDs. Converting this down from the Long UserID to the Short UserID can then give us the foldername. For example if"1234567890"
converts down to"12345"
, then we can get that user'suserdata
folder by going to$SROOT/userdata/12345
.By using this
loginusers.vdf
field we can effectively tell SteamTinkerLaunch to build the path to theuserdata
folder based on the currently logged in / last logged in user (last logged in if the user has the option to choose which account to sign into enabled at startup). So instead of hardcoding a default based onfind
or relying onSTEAMUSERID
in the global config, we can dynamically choose theuserdata
folder.In the event where we cannot get the
"MostRecent"
value (i.e.loginusers.vdf
doesn't exist, or the VDF format changes, etc) then we fall back to using thefind
command and picking the first result fromuserdata
which has only numbers is greater than0
.Implementation
We have a new function called
fillLoginUsersCSV
, to parse throughloginusers.vdf
. Once we parse the information out, we write it to a file in$STLSHM
calledLoginUsersCSV.txt
(as is convention). The reason we store it in a file is because this is not very likely to change, and it would be time consuming to fetch on each execution. So the trade-off is to cache the state ofloginusers.vdf
as it was when the file is generated for the first time. If the user logs into a different account, this file won't update. We do the same thing for some other files we parse including theProtonCSV
which is just as, if not moreso, I/O-bound.The CSV file stores the Long UserID (where possible), Short UserID, and the value of
"MostRecent"
. The file might look something like this with multiple accounts.The first row is the Most Recent account, and its userdata path would be
$SROOT/userdata/12345
.This function works by getting the entire
"users"
block and then parsing out all the VDF sections that match the following:"MostRecent"
has two tabs followed by a number in quotes, as does"timestamp"
.This allows us to match the start of VDF blocks inside of the file, which we can retrieve and parse out
"MostRecent"
from.The function has a couple of other paths:
loginusers.vdf
(i.e. it doesn't exist, or our functions cannot parse it for some reason like if the format ever changed), then we fall back to using thefind
function and picking the first one that gets returned.a This is still written into the CSV, and will be the only row written into the CSV. So if we fallback to
find
because we can't parseloginusers.vdf
, we will still check the CSV to get the information we need insetSteamPaths
, so that function only has to handle the paths of: ifSTEAMUSERID
defined in global config, elif Steam user information in CSV file (doesn't care how the data got there, whether it's fromloginusers.vdf
orfind
command is irrelevant), else log warning that we couldn't getuserdata
folder.Once we have this information in the CSV file, in
setSteamPaths
we try to parse this. If everything goes smoothly, theuserdata
path will be built dynamically based on the most recent user rather than always using the same result of thefind
command, which may pick the wrong result if multiple other Steam accounts are added.This is all significantly more complex than what we had before, but should be more correct.
Future Work
Since we now store all known Steam Users, we could in future parse out more information if known (such as display name). This would be useful for exposing on the Add Non-Steam Game GUI, where we could add a combobox entry to select which Steam User the
shortcuts.vdf
should be added for, defaulting to the current Steam User we selected. The dropdown should contain either the Account Name or Display NameOne problem with this is that we may not have an account name if we could not parse
loginusers.vdf
, so we would have to say "default" or find the account name in some other way.We should be able to store the Display Name and Account Name, and on the Add Non-Steam Game GUI we can default to one of these name fields, but we should be able to accept the Long UserID, Short UserID, Account Name, and Display Name as possible arguments. Then when it gets passed to the commandline either by the user running it manually or from the GUI (i.e. if a user decides to enter their Account Name instead of Display Name) we should be able to search the CSV file and check if the entered value for the user matches any column in the current row.
If we cannot find the account mentioned, we should default to the Most Recent account.
That would mean, on top of this PR which would be a good default of selecting the current Steam User, a user can optionally define an account which the shortcuts can be added for. If they have a few different accounts this could be handy, or it could allow them to run the command multiple times to add the shortcut for multiple accounts.
This PR is mostly ready, it just needs a bit more testing. In particular I want to make extra sure that the fallback works, as there was one report of VDF parsing not working for a user with
config.vdf
. So in cases where the parsing fails I want to make sure we can default so that we shouldn't end up with a case where we cannot find a Steam userdata directory if a valid one exists, and a valid one should always exist if at least one account is logged into Steam.TODO: