diff --git a/DamageCalculator/DamageCalculator/MainWindow.xaml.cs b/DamageCalculator/DamageCalculator/MainWindow.xaml.cs
index c667310..be89750 100644
--- a/DamageCalculator/DamageCalculator/MainWindow.xaml.cs
+++ b/DamageCalculator/DamageCalculator/MainWindow.xaml.cs
@@ -127,7 +127,7 @@ namespace Damage_Calculator
InitializeComponent();
Globals.LoadSettings();
- SteamShared.Globals.Settings.CsgoHelper.CsgoPath = SteamShared.Globals.Settings.SteamHelper.GetGamePathFromExactName("Counter-Strike: Global Offensive");
+ SteamShared.Globals.Settings.CsgoHelper.CsgoPath = SteamShared.Globals.Settings.SteamHelper.GetGamePathFromExactName("Counter-Strike: Global Offensive", true);
if (SteamShared.Globals.Settings.CsgoHelper.CsgoPath == null)
{
ShowMessage.Error("Make sure you have installed CS:GO and Steam correctly.");
@@ -1843,7 +1843,7 @@ namespace Damage_Calculator
// CS:GO is not opened, just ask if the user wants to start it
if (MessageBox.Show("Do you want to start CS:GO now?", "Start CS:GO", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
- SteamShared.Globals.Settings.SteamHelper.StartApp(SteamShared.CsgoHelper.GameID, additionalStartOptions);
+ SteamShared.Globals.Settings.SteamHelper.StartSteamApp(SteamShared.CsgoHelper.GameID, additionalStartOptions);
ShowMessage.Info("CS:GO is currently attempting to start. Retry to connect when you're in-game.\n\nRetrying while on a map will load the corresponding map.");
return;
}
@@ -1866,7 +1866,7 @@ namespace Damage_Calculator
if (csgo.HasExited)
{
- SteamShared.Globals.Settings.SteamHelper.StartApp(SteamShared.CsgoHelper.GameID, additionalStartOptions);
+ SteamShared.Globals.Settings.SteamHelper.StartSteamApp(SteamShared.CsgoHelper.GameID, additionalStartOptions);
ShowMessage.Info("CS:GO is currently attempting to restart. Retry to connect when you're in-game.\n\nRetrying while on a map will load the corresponding map.");
return;
diff --git a/SteamShared/SteamShared/SteamShared/Models/MapPoint.cs b/SteamShared/SteamShared/SteamShared/Models/MapPoint.cs
index 9ad2e2a..6cc364c 100644
--- a/SteamShared/SteamShared/SteamShared/Models/MapPoint.cs
+++ b/SteamShared/SteamShared/SteamShared/Models/MapPoint.cs
@@ -8,29 +8,50 @@ namespace SteamShared.Models
{
public class MapPoint
{
+ ///
+ /// The actual UI circle element of this point displayed on the map.
+ ///
public System.Windows.Shapes.Ellipse? Circle { get; set; }
+ ///
+ /// The percentage that this point is at on the map's X axis (0% is left, 100% is right).
+ ///
public double PercentageX { get; set; }
+ ///
+ /// The percentage that this point is at on the map's Y axis (0% is top, 100% is bottom).
+ ///
public double PercentageY { get; set; }
///
- /// The in-game X-coordinate.
+ /// The in-game X-coordinate.
///
public double X { get; set; }
///
- /// The in-game Y-coordinate.
+ /// The in-game Y-coordinate.
///
public double Y { get; set; }
///
- /// The in-game Z-coordinate, if any.
+ /// The in-game Z-coordinate, if any.
///
public double? Z { get; set; }
+ ///
+ /// The ID of the area that this point was put on.
+ ///
+ ///
+ /// If there is no area associated, it will be negative.
+ ///
public int AssociatedAreaID { get; set; } = -1;
+ ///
+ /// The percentage of how wide this circle is relative to the map's width or height.
+ ///
+ ///
+ /// Width or height doesn't matter as maps are square shaped.
+ ///
public double PercentageScale { get; set; }
}
}
diff --git a/SteamShared/SteamShared/SteamShared/Models/SteamGame.cs b/SteamShared/SteamShared/SteamShared/Models/SteamGame.cs
index 771ace9..1c69b5d 100644
--- a/SteamShared/SteamShared/SteamShared/Models/SteamGame.cs
+++ b/SteamShared/SteamShared/SteamShared/Models/SteamGame.cs
@@ -1,38 +1,123 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SteamShared.Models
{
+ ///
+ /// A game (or app) which contains mainly app manifest data.
+ ///
public class SteamGame
{
+ ///
+ /// The name of this game.
+ ///
public string? Name { get; set; }
+ ///
+ /// The name of the folder this game's files are installed in,
+ /// e.g. for CS:GO it would be "Counter-Strike Global Offensive".
+ ///
public string? InstallFolderName { get; set; }
+ ///
+ /// The absolute path of the steam library, which contains this game.
+ ///
+ public string? LinkedSteamLibraryPath { get; set; }
+
+ ///
+ /// The absolute installation path of this game, including the ,
+ /// or , if a puzzle piece of it is not specified.
+ ///
+ ///
+ /// The path might look like this on Windows:
+ ///
+ /// C:\My\Library\Path\steamapps\common\Super Cool Game
+ /// [ Library Path ] [ Xtra Folders ] [ Game Name ]
+ ///
+ public string? FullInstallPath
+ {
+ get
+ {
+ if (this.LinkedSteamLibraryPath is null || this.InstallFolderName is null)
+ return null;
+
+ return System.IO.Path.Combine(LinkedSteamLibraryPath, "steamapps", "common", InstallFolderName);
+ }
+ }
+
+ ///
+ /// Whether the game's directory () exists.
+ ///
+ ///
+ /// and need to be set.
+ ///
+ public bool GameFolderExists
+ {
+ get
+ {
+ return Directory.Exists(this.FullInstallPath);
+ }
+ }
+
+ ///
+ /// The ID of this Steam App, e.g. for CS:GO this would be 730.
+ ///
public int AppId { get; set; }
+ ///
+ /// The flags of this game, defined in .
+ /// Note, that these are *flags* and thus can have multiple values.
+ ///
public int GameState { get; set; }
+ ///
+ /// The time this game was last updated at.
+ ///
public DateTime LastUpdated { get; set; }
+ ///
+ /// The SteamID64 of the last owner user of this game.
+ ///
public long LastOwnerSteam64Id { get; set; }
+ ///
+ /// The amount of bytes that are left to download.
+ ///
public long BytesToDownload { get; set; }
+ ///
+ /// The amount of bytes that were already downloaded.
+ ///
public long BytesDownloaded { get; set; }
+ ///
+ /// The amount of bytes that are left to be staged (Not 100% sure, what staging does).
+ ///
public long BytesToStage { get; set; }
+ ///
+ /// The amount of bytes that were alread staged.
+ ///
public long BytesStaged { get; set; }
+ ///
+ /// Whether Steam should keep this game updated automatically.
+ ///
public bool KeepAutomaticallyUpdated { get; set; }
+ ///
+ /// Whether Steam is allowed to update other games and apps while this app is running.
+ ///
public bool AllowOtherUpdatesWhileRunning { get; set; }
}
+ ///
+ /// The GameState flags defined in the app manifests.
+ ///
[Flags]
enum GameState
{
diff --git a/SteamShared/SteamShared/SteamShared/SteamHelper.cs b/SteamShared/SteamShared/SteamShared/SteamHelper.cs
index 5721846..04ca33d 100644
--- a/SteamShared/SteamShared/SteamShared/SteamHelper.cs
+++ b/SteamShared/SteamShared/SteamShared/SteamHelper.cs
@@ -18,8 +18,8 @@ namespace SteamShared
public List? InstalledGames;
///
- /// Gets the absolute path to the Steam install directory.
- /// If it can't be fetched (i.e. Steam is not installed) null is returned.
+ /// The absolute path to the Steam install directory.
+ /// If it can't be fetched (i.e. Steam is not installed) null is returned.
///
public string? SteamPath
{
@@ -34,8 +34,8 @@ namespace SteamShared
}
///
- /// Gets a list of all Steam libraries, and whether they're existent or not.
- /// If it can't be fetched (i.e. Steam is not installed) null is returned.
+ /// Gets a list of all Steam libraries, and whether they're existent or not.
+ /// If it can't be fetched (i.e. Steam is not installed) null is returned.
///
public List? SteamLibraries
{
@@ -50,7 +50,7 @@ namespace SteamShared
}
///
- /// Forcefully tries to update the property with the current Steam path, even if it should be already set.
+ /// Forcefully tries to update the property with the current Steam path, even if it should be already set.
///
public void UpdateSteamPath()
{
@@ -58,9 +58,9 @@ namespace SteamShared
}
///
- /// Gets the path to the Steam install directory. (For external use is preferred.)
+ /// The path to the Steam install directory. (For external use is preferred.)
///
- /// The absolute path to the Steam install directory, or null if it can't be fetched.
+ /// the absolute path to the Steam install directory, or null if it can't be fetched.
public string? GetSteamPath()
{
var steamKey = Registry.CurrentUser.OpenSubKey("software\\valve\\steam");
@@ -98,7 +98,7 @@ namespace SteamShared
// Usually the config.vdf had "BaseInstallFolder_" entries,
// now it seems that these entries don't exist anymore with reinstalls, and maybe they're not up-to-date anyways?
- // Now we try reading the "libraryfolders.vdf", which now also contains the default library location
+ // Now we try reading the "libraryfolders.vdf", which now also contains the default library location (In the Steam folder, by default under C:)
#if NEWLIBRARYLOCATION
string configFilePath = Path.Combine(this.steamPath, "config", "libraryfolders.vdf");
if (!File.Exists(configFilePath))
@@ -144,6 +144,10 @@ namespace SteamShared
#endif
}
+ ///
+ /// Updates the list of installed steam games.
+ ///
+ /// Whether to fetch them again, even if they were fetched before.
public void UpdateInstalledGames(bool force = false)
{
if (!force && this.InstalledGames != null)
@@ -152,10 +156,25 @@ namespace SteamShared
this.InstalledGames = this.GetInstalledGames();
}
+ ///
+ /// Gets a list of fully installed Steam games, as seen by the manifest files.
+ ///
+ ///
+ /// Games are seen as fully installed, when their manifest file exists,
+ /// and the manifest states that the game is fully installed.
+ /// This means, that if the files are deleted manually, it might still be seen as installed,
+ /// because the manifest file might not change.
+ ///
+ ///
+ /// a list of installed Steam games, with some manifest data,
+ /// or , if no games could be fetched or found.
+ ///
public List? GetInstalledGames()
{
+ // Get all steam library paths
var steamLibraries = this.GetSteamLibraries();
+ // If the steam path couldn't be fetched or no libraries exist, we short-circuit
if (steamLibraries == null)
return null;
@@ -163,10 +182,11 @@ namespace SteamShared
foreach(var library in steamLibraries)
{
- if (!library.DoesExist)
+ if (!library.DoesExist || library.Path is null)
continue;
- List manifestFiles = Directory.GetFiles(Path.Combine(library.Path!, "steamapps")).ToList().Where(f => this.isAppManifestFile(f)).ToList();
+ List manifestFiles = Directory.GetFiles(Path.Combine(library.Path, "steamapps"))
+ .Where(f => this.isAppManifestFile(f)).ToList();
foreach (string manifestFile in manifestFiles)
{
@@ -178,9 +198,13 @@ namespace SteamShared
var root = manifestVDF["AppState"];
+ if (root == null)
+ // Parse error of manifest, skip it
+ continue;
+
var currGame = new SteamGame();
- this.populateGameInfo(currGame, root!);
+ this.populateGameInfo(currGame, root, library.Path);
if((currGame.GameState & (int)GameState.StateFullyInstalled) != 0)
{
@@ -193,69 +217,83 @@ namespace SteamShared
return allGames;
}
- public string? GetGamePathFromExactName(string gameName)
+ ///
+ /// Gets the absolute path of the game name (not folder) provided.
+ ///
+ /// The name of the game. The case, as well as leading and trailing whitespaces don't matter.
+ /// Whether to only return it, if it's marked as fully installed.
+ ///
+ /// the absolute path of the game, or if not found,
+ /// the game's folder doesn't exist, or it wasn't marked as fully installed, when required to be.
+ ///
+ public string? GetGamePathFromExactName(string gameName, bool shouldBeFullyInstalled = false)
{
- var steamLibraries = this.GetSteamLibraries();
+ // Will not update, if already updated once before
+ this.UpdateInstalledGames();
- if (steamLibraries == null)
+ if (this.InstalledGames is null)
+ // User is broke or something
return null;
- var allGames = new List();
+ gameName = gameName.Trim();
- foreach (var library in steamLibraries)
- {
- if (!library.DoesExist)
- continue;
+ var foundGame = this.InstalledGames.Where(game => game.Name is not null && game.Name.Trim().Equals(gameName, StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault();
- List manifestFiles = Directory.GetFiles(Path.Combine(library.Path!, "steamapps")).ToList().Where(f => this.isAppManifestFile(f)).ToList();
+ if (foundGame is null)
+ return null;
- foreach (string manifestFile in manifestFiles)
- {
- var manifestVDF = new VDFFile(manifestFile);
+ if (!foundGame.GameFolderExists)
+ return null;
- if (manifestVDF.RootElements.Count < 1)
- // App manifest might be still existent but the game might not be installed (happened during testing)
- continue;
+ if (shouldBeFullyInstalled
+ && (foundGame.GameState & (int)GameState.StateFullyInstalled) == 0)
+ return null;
- var root = manifestVDF["AppState"];
-
- if(root!["name"].Value!.Trim().ToLower() != gameName.Trim().ToLower())
- {
- // Not our wanted game, skip
- continue;
- }
-
- var currGame = new SteamGame();
-
- this.populateGameInfo(currGame, root);
-
- if ((currGame.GameState & (int)GameState.StateFullyInstalled) != 0)
- {
- // Game was fully installed according to steam
- return Path.Combine(library.Path!, "steamapps", "common", currGame.InstallFolderName!);
- }
- }
- }
-
- return null;
+ // Match the name while ignoring leading and trailing whitespaces, as well as upper/lower case.
+ return foundGame.FullInstallPath;
}
///
- /// Gets the most recently logged in Steam user, based on the "MostRecent" value.
+ /// Gets the most recently logged in Steam user, based on the "MostRecent" value.
///
///
- /// The most recent logged in Steam user, or , if none has been found or an error has occurred.
+ /// the most recent logged in Steam user, or , if none has been found or an error has occurred.
///
public SteamUser? GetMostRecentSteamUser()
{
- string steamPath = this.SteamPath;
+ List? steamUsers = this.GetSteamUsers();
- if (steamPath == null)
+ if (steamUsers == null)
return null;
+ // Gets the user that has logged in most recently.
+ // We do this instead of checking, which user has the "MostRecent" VDF property set to 1
+ SteamUser? mostRecentLoggedInSteamUser = steamUsers.OrderByDescending(user => user.LastLogin).FirstOrDefault();
+
+ return mostRecentLoggedInSteamUser;
+ }
+
+ ///
+ /// Gets all Steam users from the loginusers.vdf file.
+ ///
+ ///
+ /// a list of users, or , if no users exist or there was an error.
+ ///
+ public List? GetSteamUsers()
+ {
+ string? steamPath = this.SteamPath;
+
+ // SteamPath couldn't be fetched.
+ // This would break if Steam was installed to a different directory between fetching and using the steam path.
+ if (steamPath == null)
+ return null;
+
+ // The path that probably contains all users that have logged in since Steam was installed
string usersFilePath = Path.Combine(steamPath, "config", "loginusers.vdf");
if (!File.Exists(usersFilePath))
+ // Where file? 🦧
return null;
VDFFile vdf = new VDFFile(usersFilePath);
@@ -265,44 +303,51 @@ namespace SteamShared
if (users == null)
return null;
- SteamUser? mostRecentUser = null;
+ List? steamUsers = null;
+ // users may be empty here
foreach (var user in users)
{
- if (int.TryParse(user["MostRecent"]?.Value, out int mostRecent) && Convert.ToBoolean(mostRecent))
+ // Create the list if we have at least one *potential* user
+ if (steamUsers == null)
+ steamUsers = new List();
+
+ var steamUser = new SteamUser();
+
+ // This is not the user name, but the name of the VDF element, which in this case should be the steam ID 64
+ if (ulong.TryParse(user.Name, out ulong steamID64))
{
- // We found a "most recent" user
- mostRecentUser = new SteamUser();
-
- if(ulong.TryParse(user.Name, out ulong steamID64))
- {
- mostRecentUser.SteamID64 = steamID64;
- }
-
- mostRecentUser.AccountName = user["AccountName"].Value;
- mostRecentUser.PersonaName = user["PersonaName"].Value;
-
- if (ulong.TryParse(user["Timestamp"].Value, out ulong lastLoginUnixTime))
- {
- mostRecentUser.LastLogin = lastLoginUnixTime;
- }
-
- mostRecentUser.AbsoluteUserdataFolderPath = Path.Combine(steamPath, "userdata", mostRecentUser.AccountID.ToString());
+ steamUser.SteamID64 = steamID64;
}
+
+ steamUser.AccountName = user["AccountName"].Value;
+ steamUser.PersonaName = user["PersonaName"].Value;
+
+ // "MostRecent" can later be found by getting the largest Timestamp
+ if (ulong.TryParse(user["Timestamp"].Value, out ulong lastLoginUnixTime))
+ {
+ steamUser.LastLogin = lastLoginUnixTime;
+ }
+
+ // The needed AccountID (Last part of the SteamId3) is calculated automatically from the SteamId64
+ steamUser.AbsoluteUserdataFolderPath = Path.Combine(steamPath, "userdata", steamUser.AccountID.ToString());
+
+ steamUsers.Add(steamUser);
}
- return mostRecentUser;
+ return steamUsers;
}
///
- /// Starts the given steam game, with the given additional arguments, if possible.
+ /// Starts the given steam game, with the given additional arguments, if possible.
///
- /// The ID of the game.
- ///
- /// The arguments passed to that game.
- /// Note, that the default arguments set by the user in the UI are also passed to the app, these are just additional.
+ /// The ID of the game (e.g. 730 for CS:GO/CS2).
+ ///
+ /// The arguments passed to that game.
+ /// Note, that the default arguments set by the user in the UI are also passed to the app,
+ /// these are just additional to that.
///
- public void StartApp(int gameID, string arguments)
+ public void StartSteamApp(int gameID, string additionalArgs)
{
string? steamPath = this.SteamPath;
this.UpdateInstalledGames(); // Won't force update, if already set
@@ -316,28 +361,66 @@ namespace SteamShared
startInfo.UseShellExecute = false; // Make double sure
startInfo.CreateNoWindow = false;
startInfo.FileName = Path.Combine(steamPath, "steam.exe");
- startInfo.Arguments = $"-applaunch {gameID}" + (String.IsNullOrWhiteSpace(arguments) ? string.Empty : $" {arguments}");
+ string extraArgs = String.IsNullOrWhiteSpace(additionalArgs) ? string.Empty : $" {additionalArgs}";
+
+ // The "-applaunch" argument will automatically add the args stored by the user. We add our own ones to that, if required.
+ startInfo.Arguments = $"-applaunch {gameID}" + extraArgs;
+
+ // Fire and forget!
Process.Start(startInfo);
}
#region Private Methods
+ ///
+ /// Checks, if the file at the given absolute path is considered an appmanifest, by the looks of it.
+ ///
+ ///
+ /// App manifest have the format "appmanifest_GAMEID.acf"
+ ///
+ /// The absolute path of the app manifest (acf) file.
+ ///
+ /// whether the file name matches the app manifest description.
+ ///
private bool isAppManifestFile(string filePath)
{
- return System.Text.RegularExpressions.Regex.IsMatch(filePath.Split(new[] { '\\', '/' }).Last(), "appmanifest_\\d+.acf"); ;
+ string[] splitFilePath = filePath.Split(new[] { '\\', '/' });
+
+ if (splitFilePath.Length < 1)
+ // Doesn't seem to be a valid path
+ return false;
+
+ return System.Text.RegularExpressions.Regex.IsMatch(splitFilePath.Last(), "appmanifest_\\d+.acf"); ;
}
- private DateTime fromUnixFormat(long unixFormat)
+ ///
+ /// Converts a unix time in seconds to a using the specified .
+ ///
+ /// The unix seconds.
+ /// The type of time zone, UTC by default.
+ ///
+ /// the that was created from the given seconds.
+ ///
+ private DateTime fromUnixFormat(long unixSeconds, DateTimeKind dateTimeKind = DateTimeKind.Utc)
{
- DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Local);
- return dateTime.AddSeconds(unixFormat);
+ DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, dateTimeKind);
+ return dateTime.AddSeconds(unixSeconds);
}
- private void populateGameInfo(SteamGame game, Element appStateVdf)
+ ///
+ /// Takes a game object and populates it with the info from the app manifest file,
+ /// which is specified by the given VDF element.
+ ///
+ /// The game to populate with info.
+ /// The app state VDF element, which contains the required information.
+ /// The absolute path to the steam library containing this game.
+ private void populateGameInfo(SteamGame game, Element appStateVdf, string steamLibraryPath)
{
game.Name = appStateVdf["name"]?.Value;
+ // Setting these two properties enables the ability to fetch the FullInstallPath
game.InstallFolderName = appStateVdf["installdir"]?.Value;
+ game.LinkedSteamLibraryPath = steamLibraryPath;
if (int.TryParse(appStateVdf["appid"]?.Value, out int appId))
{
@@ -351,7 +434,8 @@ namespace SteamShared
if (long.TryParse(appStateVdf["LastUpdated"]?.Value, out long lastUpdated))
{
- game.LastUpdated = fromUnixFormat(lastUpdated);
+ // It's unix time, but the time is in the local time zone, and not in UTC.
+ game.LastUpdated = fromUnixFormat(lastUpdated, DateTimeKind.Local);
}
if (long.TryParse(appStateVdf["LastOwner"]?.Value, out long lastOwner))