using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using DynamicData; using Microsoft.Win32; namespace Wabbajack.Common { [DebuggerDisplay("{" + nameof(Name) + "}")] public class SteamGame { public int AppId; public string Name; public string InstallDir; public Game? Game; public HashSet WorkshopItems; /// /// Combined size of all installed workshop items on disk in bytes /// public int WorkshopItemsSize; } public class SteamWorkshopItem { public SteamGame Game; public int ItemID; /// /// Size is in bytes /// public int Size; } /// /// Class for all Steam operations /// public class SteamHandler { private static readonly Lazy instance = new Lazy( () => new SteamHandler(true), isThreadSafe: true); public static SteamHandler Instance => instance.Value; private const string SteamRegKey = @"Software\Valve\Steam"; /// /// Path to the Steam folder /// public string SteamPath { get; internal set; } /// /// HashSet of all known Steam Libraries /// public HashSet InstallFolders { get; internal set; } /// /// HashSet of all known SteamGames /// public HashSet Games { get; internal set; } private string SteamConfig => Path.Combine(SteamPath, "config", "config.vdf"); public SteamHandler(bool init) { var steamKey = Registry.CurrentUser.OpenSubKey(SteamRegKey); SteamPath = steamKey?.GetValue("SteamPath").ToString(); if(!init) return; LoadInstallFolders(); LoadAllSteamGames(); } /// /// Finds the installation path of a Steam game by ID /// /// ID of the Steam game /// public string GetGamePathById(int id) { return Games.FirstOrDefault(f => f.AppId == id)?.InstallDir; } /// /// Reads the config file and adds all found installation folders to the HashSet /// public void LoadInstallFolders() { var paths = new HashSet(); File.ReadLines(SteamConfig, Encoding.UTF8).Do(l => { if (!l.Contains("BaseInstallFolder_")) return; var s = GetVdfValue(l); s = Path.Combine(s, "steamapps"); if(Directory.Exists(s)) paths.Add(s); }); // Default path in the Steam folder isn't in the configs if(Directory.Exists(Path.Combine(SteamPath, "steamapps"))) paths.Add(Path.Combine(SteamPath, "steamapps")); InstallFolders = paths; } /// /// Enumerates through all Steam Libraries to find and read all .afc files, adding the found game /// to the HashSet /// public void LoadAllSteamGames() { var games = new HashSet(); InstallFolders.Do(p => { Directory.EnumerateFiles(p, "*.acf", SearchOption.TopDirectoryOnly).Where(File.Exists).Do(f => { var steamGame = new SteamGame(); var valid = false; File.ReadAllLines(f, Encoding.UTF8).Do(l => { if(l.Contains("\"appid\"")) if (!int.TryParse(GetVdfValue(l), out steamGame.AppId)) return; if(l.Contains("\"name\"")) steamGame.Name = GetVdfValue(l); if (l.Contains("\"installdir\"")) { var path = Path.Combine(p, "common", GetVdfValue(l)); steamGame.InstallDir = Directory.Exists(path) ? path : null; } if (steamGame.AppId != 0 && !string.IsNullOrWhiteSpace(steamGame.Name) && !string.IsNullOrWhiteSpace(steamGame.InstallDir)) valid = true; }); if (!valid) return; steamGame.Game = GameRegistry.Games.Values .FirstOrDefault(g => g.SteamIDs.Contains(steamGame.AppId) && g.RequiredFiles.TrueForAll(s => File.Exists(Path.Combine(steamGame.InstallDir, s))) )?.Game; games.Add(steamGame); }); }); Games = games; } public void LoadWorkshopItems(SteamGame game) { if(game.WorkshopItems == null) game.WorkshopItems = new HashSet(); InstallFolders.Do(p => { var workshop = Path.Combine(p, "workshop"); if(!Directory.Exists(workshop)) return; Directory.EnumerateFiles(workshop, "*.acf", SearchOption.TopDirectoryOnly).Where(File.Exists).Do(f => { if (Path.GetFileName(f) != $"appworkshop_{game.AppId}.acf") return; var foundAppID = false; var workshopItemsInstalled = 0; var workshopItemDetails = 0; var currentItem = new SteamWorkshopItem(); var bracketStart = 0; var bracketEnd = 0; var lines = File.ReadAllLines(f, Encoding.UTF8); var end = false; lines.Do(l => { if (end) return; if(currentItem == null) currentItem = new SteamWorkshopItem(); var currentLine = lines.IndexOf(l); if (l.Contains("\"appid\"") && !foundAppID) { if (!int.TryParse(GetVdfValue(l), out var appID)) return; foundAppID = true; if (appID != game.AppId) return; } if (!foundAppID) return; if (l.Contains("\"SizeOnDisk\"")) { if (!int.TryParse(GetVdfValue(l), out var sizeOnDisk)) return; game.WorkshopItemsSize = sizeOnDisk; } if (l.Contains("\"WorkshopItemsInstalled\"")) workshopItemsInstalled = currentLine; if (l.Contains("\"WorkshopItemDetails\"")) workshopItemDetails = currentLine; if (workshopItemsInstalled == 0) return; if (currentLine <= workshopItemsInstalled + 1 && currentLine >= workshopItemDetails - 1) return; if (currentItem.ItemID == 0) if (!int.TryParse(GetSingleVdfValue(l), out currentItem.ItemID)) return; if (currentItem.ItemID == 0) return; if (bracketStart == 0 && l.Contains("{")) bracketStart = currentLine; if (bracketEnd == 0 && l.Contains("}")) bracketEnd = currentLine; if (bracketStart == 0) return; if (currentLine == bracketStart + 1) if (!int.TryParse(GetVdfValue(l), out currentItem.Size)) return; if (bracketStart == 0 || bracketEnd == 0 || currentItem.ItemID == 0 || currentItem.Size == 0) return; bracketStart = 0; bracketEnd = 0; currentItem.Game = game; game.WorkshopItems.Add(currentItem); currentItem = null; end = true; }); }); }); } private static string GetVdfValue(string line) { var trim = line.Trim('\t').Replace("\t", ""); string[] s = trim.Split('\"'); return s[3].Replace("\\\\", "\\"); } private static string GetSingleVdfValue(string line) { var trim = line.Trim('\t').Replace("\t", ""); return trim.Split('\"')[1]; } } }