diff --git a/FModel/Enums.cs b/FModel/Enums.cs index 58dca396..3bdf53f2 100644 --- a/FModel/Enums.cs +++ b/FModel/Enums.cs @@ -90,7 +90,11 @@ namespace FModel [Description("Core")] Platform, [Description("Days Gone")] - BendGame + BendGame, + [Description("PLAYERUNKNOWN'S BATTLEGROUNDS")] + TslGame, + [Description("Splitgate")] + PortalWars } public enum ELoadingMode @@ -138,4 +142,4 @@ namespace FModel // [Description("Community")] // CommunityMade } -} \ No newline at end of file +} diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 984dcead..4d524066 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -36,7 +36,7 @@ namespace FModel.Settings { if (File.Exists(FilePath)) File.Delete(FilePath); } - + private bool _showChangelog = true; public bool ShowChangelog { @@ -228,7 +228,9 @@ namespace FModel.Settings {FGame.RogueCompany, Constants._NO_PRESET_TRIGGER}, {FGame.SwGame, Constants._NO_PRESET_TRIGGER}, {FGame.Platform, Constants._NO_PRESET_TRIGGER}, - {FGame.BendGame, Constants._NO_PRESET_TRIGGER} + {FGame.BendGame, Constants._NO_PRESET_TRIGGER}, + {FGame.TslGame, Constants._NO_PRESET_TRIGGER}, + {FGame.PortalWars, Constants._NO_PRESET_TRIGGER} }; public IDictionary Presets { @@ -252,7 +254,9 @@ namespace FModel.Settings {FGame.RogueCompany, EGame.GAME_RogueCompany}, {FGame.SwGame, EGame.GAME_UE4_LATEST}, {FGame.Platform, EGame.GAME_UE4_25}, - {FGame.BendGame, EGame.GAME_UE4_11} + {FGame.BendGame, EGame.GAME_UE4_11}, + {FGame.TslGame, EGame.GAME_PlayerUnknownsBattlegrounds}, + {FGame.PortalWars, EGame.GAME_UE4_LATEST}, }; public IDictionary OverridedGame { @@ -276,14 +280,16 @@ namespace FModel.Settings {FGame.RogueCompany, UE4Version.VER_UE4_DETERMINE_BY_GAME}, {FGame.SwGame, UE4Version.VER_UE4_DETERMINE_BY_GAME}, {FGame.Platform, UE4Version.VER_UE4_DETERMINE_BY_GAME}, - {FGame.BendGame, UE4Version.VER_UE4_DETERMINE_BY_GAME} + {FGame.BendGame, UE4Version.VER_UE4_DETERMINE_BY_GAME}, + {FGame.TslGame, UE4Version.VER_UE4_DETERMINE_BY_GAME}, + {FGame.PortalWars, UE4Version.VER_UE4_DETERMINE_BY_GAME} }; public IDictionary OverridedUEVersion { get => _overridedUEVersion; set => SetProperty(ref _overridedUEVersion, value); } - + private IDictionary> _overridedCustomVersions = new Dictionary> { {FGame.Unknown, null}, @@ -300,14 +306,16 @@ namespace FModel.Settings {FGame.RogueCompany, null}, {FGame.SwGame, null}, {FGame.Platform, null}, - {FGame.BendGame, null} + {FGame.BendGame, null}, + {FGame.TslGame, null}, + {FGame.PortalWars, null} }; public IDictionary> OverridedCustomVersions { get => _overridedCustomVersions; set => SetProperty(ref _overridedCustomVersions, value); } - + private IDictionary> _overridedOptions = new Dictionary> { {FGame.Unknown, null}, @@ -324,7 +332,9 @@ namespace FModel.Settings {FGame.RogueCompany, null}, {FGame.SwGame, null}, {FGame.Platform, null}, - {FGame.BendGame, null} + {FGame.BendGame, null}, + {FGame.TslGame, null}, + {FGame.PortalWars, null} }; public IDictionary> OverridedOptions { @@ -387,7 +397,9 @@ namespace FModel.Settings {FGame.RogueCompany, new List()}, {FGame.SwGame, new List()}, {FGame.Platform, new List()}, - {FGame.BendGame, new List()} + {FGame.BendGame, new List()}, + {FGame.TslGame, new List()}, + {FGame.PortalWars, new List()} }; public IDictionary> CustomDirectories { @@ -534,14 +546,14 @@ namespace FModel.Settings get => _meshExportFormat; set => SetProperty(ref _meshExportFormat, value); } - + private ELodFormat _lodExportFormat = ELodFormat.FirstLod; public ELodFormat LodExportFormat { get => _lodExportFormat; set => SetProperty(ref _lodExportFormat, value); } - + private ETextureFormat _textureExportFormat = ETextureFormat.Png; public ETextureFormat TextureExportFormat { @@ -549,4 +561,4 @@ namespace FModel.Settings set => SetProperty(ref _textureExportFormat, value); } } -} \ No newline at end of file +} diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 1a982dc5..0a4980c1 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -1,4 +1,4 @@ -using FModel.Extensions; +using FModel.Extensions; using FModel.Framework; using Newtonsoft.Json; using Serilog; @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Win32; namespace FModel.ViewModels { @@ -43,14 +45,14 @@ namespace FModel.ViewModels public void AddUnknownGame(string gameDirectory) { - _autoDetectedGames.Add(new DetectedGame {GameName = gameDirectory.SubstringAfterLast('\\'), GameDirectory = gameDirectory}); + _autoDetectedGames.Add(new DetectedGame { GameName = gameDirectory.SubstringAfterLast('\\'), GameDirectory = gameDirectory }); SelectedDetectedGame = AutoDetectedGames.Last(); } private IEnumerable EnumerateDetectedGames() { yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks"); - yield return new DetectedGame {GameName = "Fortnite [LIVE]", GameDirectory = Constants._FN_LIVE_TRIGGER}; + yield return new DetectedGame { GameName = "Fortnite [LIVE]", GameDirectory = Constants._FN_LIVE_TRIGGER }; yield return GetUnrealEngineGame("Pewee", "\\RogueCompany\\Content\\Paks"); yield return GetUnrealEngineGame("Rosemallow", "\\Indiana\\Content\\Paks"); yield return GetUnrealEngineGame("Catnip", "\\OakGame\\Content\\Paks"); @@ -62,20 +64,22 @@ namespace FModel.ViewModels yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508", "\\Core\\Platform\\Content\\Paks"); yield return GetUnrealEngineGame("Nebula", "\\BendGame\\Content"); yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks"); - yield return new DetectedGame {GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER}; + yield return new DetectedGame { GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER }; yield return GetMojangGame("MinecraftDungeons", "\\dungeons\\dungeons\\Dungeons\\Content\\Paks"); + yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks"); // PUBG + yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks"); // Splitgate } private LauncherInstalled _launcherInstalled; private DetectedGame GetUnrealEngineGame(string gameName, string pakDirectory) { - _launcherInstalled ??= GetDrivedLauncherInstalls("ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat"); + _launcherInstalled ??= GetDriveLauncherInstalls("ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat"); if (_launcherInstalled?.InstallationList != null) { foreach (var installationList in _launcherInstalled.InstallationList) { if (installationList.AppName.Equals(gameName, StringComparison.OrdinalIgnoreCase)) - return new DetectedGame {GameName = installationList.AppName, GameDirectory = $"{installationList.InstallLocation}{pakDirectory}"}; + return new DetectedGame { GameName = installationList.AppName, GameDirectory = $"{installationList.InstallLocation}{pakDirectory}" }; } } @@ -86,8 +90,8 @@ namespace FModel.ViewModels private RiotClientInstalls _riotClientInstalls; private DetectedGame GetRiotGame(string gameName, string pakDirectory) { - _riotClientInstalls ??= GetDrivedLauncherInstalls("ProgramData\\Riot Games\\RiotClientInstalls.json"); - if (_riotClientInstalls is {AssociatedClient: { }}) + _riotClientInstalls ??= GetDriveLauncherInstalls("ProgramData\\Riot Games\\RiotClientInstalls.json"); + if (_riotClientInstalls is { AssociatedClient: { } }) { foreach (var (key, _) in _riotClientInstalls.AssociatedClient) { @@ -101,24 +105,33 @@ namespace FModel.ViewModels } private LauncherSettings _launcherSettings; - private DetectedGame GetMojangGame(string gameName, string pakDirectory) { _launcherSettings ??= GetDataLauncherInstalls("\\.minecraft\\launcher_settings.json"); - if (_launcherSettings is {ProductLibraryDir: { }}) - return new DetectedGame {GameName = gameName, GameDirectory = $"{_launcherSettings.ProductLibraryDir}{pakDirectory}"}; + if (_launcherSettings is { ProductLibraryDir: { } }) + return new DetectedGame { GameName = gameName, GameDirectory = $"{_launcherSettings.ProductLibraryDir}{pakDirectory}" }; Log.Warning("Could not find {GameName} in launcher_settings.json", gameName); return null; } - private T GetDrivedLauncherInstalls(string jsonFile) + private DetectedGame GetSteamGame(int id, string pakDirectory) + { + var steamInfo = SteamDetection.GetSteamGameById(id); + if (steamInfo is not null) + return new DetectedGame { GameName = steamInfo.Name, GameDirectory = $"{steamInfo.GameRoot}{pakDirectory}" }; + + Log.Warning("Could not find {GameId} in steam manifests", id); + return null; + } + + private T GetDriveLauncherInstalls(string jsonFile) { foreach (var drive in DriveInfo.GetDrives()) { var launcher = $"{drive.Name}{jsonFile}"; if (!File.Exists(launcher)) continue; - + Log.Information("\"{Launcher}\" found in drive \"{DriveName}\"", launcher, drive.Name); return JsonConvert.DeserializeObject(File.ReadAllText(launcher)); } @@ -158,10 +171,13 @@ namespace FModel.ViewModels { [JsonProperty("associated_client", NullValueHandling = NullValueHandling.Ignore)] public Dictionary AssociatedClient; + [JsonProperty("patchlines", NullValueHandling = NullValueHandling.Ignore)] public Dictionary Patchlines; + [JsonProperty("rc_default", NullValueHandling = NullValueHandling.Ignore)] public string RcDefault; + [JsonProperty("rc_live", NullValueHandling = NullValueHandling.Ignore)] public string RcLive; } @@ -170,17 +186,112 @@ namespace FModel.ViewModels { [JsonProperty("channel", NullValueHandling = NullValueHandling.Ignore)] public string Channel; + [JsonProperty("customChannels", NullValueHandling = NullValueHandling.Ignore)] public object[] CustomChannels; + [JsonProperty("deviceId", NullValueHandling = NullValueHandling.Ignore)] public string DeviceId; + [JsonProperty("formatVersion", NullValueHandling = NullValueHandling.Ignore)] public int FormatVersion; + [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public string Locale; + [JsonProperty("productLibraryDir", NullValueHandling = NullValueHandling.Ignore)] public string ProductLibraryDir; } #pragma warning restore 649 + + // https://stackoverflow.com/questions/54767662/finding-game-launcher-executables-in-directory-c-sharp/67679123#67679123 + public static class SteamDetection + { + private static readonly List _steamApps; + + static SteamDetection() + { + _steamApps = GetSteamApps(GetSteamLibs()); + } + + public static AppInfo GetSteamGameById(int id) => _steamApps.FirstOrDefault(app => app.Id == id.ToString()); + + private static List GetSteamApps(IEnumerable steamLibs) + { + var apps = new List(); + foreach (var files in steamLibs.Select(lib => Path.Combine(lib, "SteamApps")).Select(appMetaDataPath => Directory.GetFiles(appMetaDataPath, "*.acf"))) + { + apps.AddRange(files.Select(GetAppInfo).Where(appInfo => appInfo != null)); + } + + return apps; + } + + private static AppInfo GetAppInfo(string appMetaFile) + { + var fileDataLines = File.ReadAllLines(appMetaFile); + var dic = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var line in fileDataLines) + { + var match = Regex.Match(line, @"\s*""(?\w+)""\s+""(?.*)"""); + if (!match.Success) continue; + var key = match.Groups["key"].Value; + var val = match.Groups["val"].Value; + dic[key] = val; + } + + if (dic.Keys.Count <= 0) return null; + AppInfo appInfo = new(); + var appId = dic["appid"]; + var name = dic["name"]; + var installDir = dic["installDir"]; + + var path = Path.GetDirectoryName(appMetaFile); + var libGameRoot = Path.Combine(path, "common", installDir); + + if (!Directory.Exists(libGameRoot)) return null; + + appInfo.Id = appId; + appInfo.Name = name; + appInfo.GameRoot = libGameRoot; + + return appInfo; + } + + private static List GetSteamLibs() + { + var steamPath = GetSteamPath(); + var libraries = new List { steamPath }; + + var listFile = Path.Combine(steamPath, @"steamapps\libraryfolders.vdf"); + var lines = File.ReadAllLines(listFile); + foreach (var line in lines) + { + var match = Regex.Match(line, @"""(?\w:\\\\.*)"""); + if (!match.Success) continue; + var path = match.Groups["path"].Value.Replace(@"\\", @"\"); + if (Directory.Exists(path)) + { + libraries.Add(path); + } + } + + return libraries; + } + + private static string GetSteamPath() => (string) Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Valve\Steam", "InstallPath", ""); // Win64, we don't support Win32 + + public class AppInfo + { + public string Id { get; internal set; } + public string Name { get; internal set; } + public string GameRoot { get; internal set; } + + public override string ToString() + { + return $"{Name} ({Id})"; + } + } + } } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Converters/StringToGameConverter.cs b/FModel/Views/Resources/Converters/StringToGameConverter.cs index 60322668..6a93d827 100644 --- a/FModel/Views/Resources/Converters/StringToGameConverter.cs +++ b/FModel/Views/Resources/Converters/StringToGameConverter.cs @@ -25,6 +25,8 @@ namespace FModel.Views.Resources.Converters "MinecraftDungeons" => "Minecraft Dungeons", "shoebill" => "Star Wars: Jedi Fallen Order", "a99769d95d8f400baad1f67ab5dfe508" => "Core", + 578080 => "PLAYERUNKNOWN'S BATTLEGROUNDS", + 677620 => "Splitgate", _ => value, }; } @@ -34,4 +36,4 @@ namespace FModel.Views.Resources.Converters throw new NotImplementedException(); } } -} \ No newline at end of file +}