Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Steam Paths: Attempt to set userdata path using MostRecent loginuser UserID #1141

Merged
merged 5 commits into from
Jul 27, 2024

Conversation

sonic2kk
Copy link
Owner

@sonic2kk sonic2kk commented Jul 17, 2024

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's loginusers.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 in setSteamPaths. If the current row has "MostRecent" as "1" then we select that Short UserID row to build the path to the userdata 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" in loginusers.vdf. However if we cannot find or parse loginusers.vdf then we will fall back to using this find 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 the shortcuts.vdf file and the localconfig.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 is 12345, then the userdata 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 and ac. We don't need to care about these folders. Every UserID folder we care about will start with at least 1 and should always be at least two digits (i.e. there can't be a UserID of just 1).

Since there can be multiple users, SteamTinkerLaunch just runs find in the userdata 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 with find 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 the users 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's userdata folder by going to $SROOT/userdata/12345.

By using this loginusers.vdf field we can effectively tell SteamTinkerLaunch to build the path to the userdata 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 on find or relying on STEAMUSERID in the global config, we can dynamically choose the userdata 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 the find command and picking the first result from userdata which has only numbers is greater than 0.

Implementation

We have a new function called fillLoginUsersCSV, to parse through loginusers.vdf. Once we parse the information out, we write it to a file in $STLSHM called LoginUsersCSV.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 of loginusers.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 the ProtonCSV 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.

1234567890,12345,1
0987654321,54321,0
4236927692,32265,0

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:

  • Start with a tab character
    • We can assume for the sections we want that they will always start with one tab, because we know the structure of this file already
    • We check that it starts with one tab to differentiate from values in the VDF sections, i.e. the "MostRecent" has two tabs followed by a number in quotes, as does "timestamp".
  • Contain only numbers inside of quotes

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:

  1. If the CSV already exists and is not empty, don't re-create it
  2. If we cannot parse 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 the find 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 parse loginusers.vdf, we will still check the CSV to get the information we need in setSteamPaths, so that function only has to handle the paths of: if STEAMUSERID defined in global config, elif Steam user information in CSV file (doesn't care how the data got there, whether it's from loginusers.vdf or find command is irrelevant), else log warning that we couldn't get userdata folder.

Once we have this information in the CSV file, in setSteamPaths we try to parse this. If everything goes smoothly, the userdata path will be built dynamically based on the most recent user rather than always using the same result of the find 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 Name

One 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:

  • Further testing of fallback
  • Some code cleanup
  • Version bump

@sonic2kk
Copy link
Owner Author

sonic2kk commented Jul 23, 2024

I have local changes for this that I want to test some more, but once those are tested and pushed, this should be ready for merging pretty soon. I have been using the branch with those local changes as my default for the last few days and it hasn't caused any issues that I have noticed or seen from occasionally glancing at the logs.

@sonic2kk
Copy link
Owner Author

With the recent pushed changes, it's been a little while so I will re-check the code and then re-test, but this should be merged soon. I want to explicitly test further the edge case of the loginusers.vdf being unreadable to essentially ensure the fallback code path that pulls the UserID from the userdata folder works. This path is what we are doing currently on master so I want to make extra sure that we can always fall back to this should anything to wrong, to prevent this PR from suddenly making a ton of userdata directories undiscoverable.

@sonic2kk
Copy link
Owner Author

In testing this seems to work, and SteamTinkerLaunch can still find the Most Recent user with and without loginusers.vdf.

Although I did realise in testing the Add Non-Steam Game functionality (which most commonly makes use of this) that the grids folder in the userdata config folder is not guaranteed to exist if you have never set up any grids before. I'll need to make a PR to fix that too.

@sonic2kk sonic2kk force-pushed the use-mostrecent-loginuser-userdatadir branch from 1cb35e3 to 952e975 Compare July 27, 2024 17:38
sonic2kk added 2 commits July 27, 2024 18:43
we did test it, it works ;-)
@sonic2kk
Copy link
Owner Author

Re-reviewed the code and it all makes sense to me, future sonic2kk, so I think it's good to merge with a version bump!

@sonic2kk sonic2kk changed the title Steam Paths: Attempt to set userdata path for MostRecent loginuser instead of first found directory in Steam userdata folder Steam Paths: Attempt to set userdata path using MostRecent loginuser UserID Jul 27, 2024
@sonic2kk sonic2kk merged commit 8756dc8 into master Jul 27, 2024
2 checks passed
@sonic2kk sonic2kk deleted the use-mostrecent-loginuser-userdatadir branch July 27, 2024 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding Non-Steam Games Does Not Use The Right userdata Path with Multiple Accounts
1 participant