diff --git a/.gitignore b/.gitignore index 1a57dee..7cf360d 100644 --- a/.gitignore +++ b/.gitignore @@ -542,3 +542,4 @@ MigrationBackup/ # End of https://www.toptal.com/developers/gitignore/api/intellij,rider,visualstudio,dotnetcore,windows /GoldbergGUI.Core/Utils/Secrets.cs +/README.bbcode diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bd149f0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "SteamStorefrontAPI"] + path = SteamStorefrontAPI + url = https://git.jeddunk.xyz/jeddunk/SteamStorefrontAPI.git diff --git a/GoldbergGUI.Core/GoldbergGUI.Core.csproj b/GoldbergGUI.Core/GoldbergGUI.Core.csproj index 79fab83..72f32a9 100644 --- a/GoldbergGUI.Core/GoldbergGUI.Core.csproj +++ b/GoldbergGUI.Core/GoldbergGUI.Core.csproj @@ -1,27 +1,28 @@  - - netcoreapp3.1 - 0.1.0 - 0.1.0 - Jeddunk - + + net8.0 + 0.3.0 + Jeddunk + AnyCPU;x86;x64 + - - - - - - - + + + + + + + + - - - ..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_32\PresentationCore\v4.0_4.0.0.0__31bf3856ad364e35\PresentationCore.dll - - - ..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework\v4.0_4.0.0.0__31bf3856ad364e35\PresentationFramework.dll - - + + + ..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_32\PresentationCore\v4.0_4.0.0.0__31bf3856ad364e35\PresentationCore.dll + + + ..\..\..\..\..\..\Windows\Microsoft.NET\assembly\GAC_MSIL\PresentationFramework\v4.0_4.0.0.0__31bf3856ad364e35\PresentationFramework.dll + + diff --git a/GoldbergGUI.Core/Models/GoldbergModel.cs b/GoldbergGUI.Core/Models/GoldbergModel.cs index 724ce27..d6a40fe 100644 --- a/GoldbergGUI.Core/Models/GoldbergModel.cs +++ b/GoldbergGUI.Core/Models/GoldbergModel.cs @@ -1,13 +1,280 @@ +using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global namespace GoldbergGUI.Core.Models { + public class GoldbergGlobalConfiguration + { + /// + /// Name of the user + /// + public string AccountName { get; set; } + /// + /// Steam64ID of the user + /// + public long UserSteamId { get; set; } + /// + /// language to be used + /// + public string Language { get; set; } + /// + /// Custom broadcast addresses (IPv4 or domain addresses) + /// + public List CustomBroadcastIps { get; set; } + } public class GoldbergConfiguration { + /// + /// App ID of the game + /// public int AppId { get; set; } - public List DlcList { get; set; } + /// + /// List of DLC + /// + public List DlcList { get; set; } + + public List Depots { get; set; } + + public List SubscribedGroups { get; set; } + + //public List AppPaths { get; set; } + + public List Achievements { get; set; } + + public List Items { get; set; } + + public List Leaderboards { get; set; } + + public List Stats { get; set; } + + // Add controller setting here! + /// + /// Set offline mode. + /// public bool Offline { get; set; } + /// + /// Disable networking (game is set to online, however all outgoing network connectivity will be disabled). + /// public bool DisableNetworking { get; set; } + /// + /// Disable overlay (experimental only). + /// public bool DisableOverlay { get; set; } + + public GoldbergGlobalConfiguration OverwrittenGlobalConfiguration { get; set; } + } + + public class DlcApp : SteamApp + { + public DlcApp() { } + + public DlcApp(SteamApp steamApp) + { + AppId = steamApp.AppId; + Name = steamApp.Name; + ComparableName = steamApp.ComparableName; + AppType = steamApp.AppType; + LastModified = steamApp.LastModified; + PriceChangeNumber = steamApp.PriceChangeNumber; + } + + /// + /// Path to DLC (relative to Steam API DLL) (optional) + /// + public string AppPath { get; set; } + } + + public class Group + { + /// + /// ID of group (https://steamcommunity.com/gid/103582791433980119/memberslistxml/?xml=1). + /// + public int GroupId { get; set; } + /// + /// Name of group. + /// + public string GroupName { get; set; } + /// + /// App ID of game associated with group (https://steamcommunity.com/games/218620/memberslistxml/?xml=1). + /// + public int AppId { get; set; } + } + + public class Achievement + { + /// + /// Achievement description. + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// Human readable name, as shown on webpage, game library, overlay, etc. + /// + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } + + /// + /// Is achievement hidden? 0 = false, else true. + /// + [JsonPropertyName("hidden")] + public int Hidden { get; set; } + + /// + /// Path to icon when unlocked (colored). + /// + [JsonPropertyName("icon")] + public string Icon { get; set; } + + /// + /// Path to icon when locked (grayed out). + /// + // ReSharper disable once StringLiteralTypo + [JsonPropertyName("icongray")] + public string IconGray { get; set; } + + /// + /// Internal name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public class Item + { + [JsonPropertyName("Timestamp")] + public DateTimeOffset Timestamp { get; set; } + + [JsonPropertyName("modified")] + public string Modified { get; set; } + + [JsonPropertyName("date_created")] + public string DateCreated { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("display_type")] + public string DisplayType { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("bundle")] + public string Bundle { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("background_color")] + public string BackgroundColor { get; set; } + + [JsonPropertyName("icon_url")] + public Uri IconUrl { get; set; } + + [JsonPropertyName("icon_url_large")] + public Uri IconUrlLarge { get; set; } + + [JsonPropertyName("name_color")] + public string NameColor { get; set; } + + [JsonPropertyName("tradable")] + // [JsonConverter(typeof(PurpleParseStringConverter))] + public bool Tradable { get; set; } + + [JsonPropertyName("marketable")] + // [JsonConverter(typeof(PurpleParseStringConverter))] + public bool Marketable { get; set; } + + [JsonPropertyName("commodity")] + // [JsonConverter(typeof(PurpleParseStringConverter))] + public bool Commodity { get; set; } + + [JsonPropertyName("drop_interval")] + // [JsonConverter(typeof(FluffyParseStringConverter))] + public long DropInterval { get; set; } + + [JsonPropertyName("drop_max_per_window")] + // [JsonConverter(typeof(FluffyParseStringConverter))] + public long DropMaxPerWindow { get; set; } + + // ReSharper disable once StringLiteralTypo + [JsonPropertyName("workshopid")] + // [JsonConverter(typeof(FluffyParseStringConverter))] + public long WorkshopId { get; set; } + + [JsonPropertyName("tw_unique_to_own")] + // [JsonConverter(typeof(PurpleParseStringConverter))] + public bool TwUniqueToOwn { get; set; } + + [JsonPropertyName("item_quality")] + // [JsonConverter(typeof(FluffyParseStringConverter))] + public long ItemQuality { get; set; } + + [JsonPropertyName("tw_price")] + public string TwPrice { get; set; } + + [JsonPropertyName("tw_type")] + public string TwType { get; set; } + + [JsonPropertyName("tw_client_visible")] + // [JsonConverter(typeof(FluffyParseStringConverter))] + public long TwClientVisible { get; set; } + + [JsonPropertyName("tw_icon_small")] + public string TwIconSmall { get; set; } + + [JsonPropertyName("tw_icon_large")] + public string TwIconLarge { get; set; } + + [JsonPropertyName("tw_description")] + public string TwDescription { get; set; } + + [JsonPropertyName("tw_client_name")] + public string TwClientName { get; set; } + + [JsonPropertyName("tw_client_type")] + public string TwClientType { get; set; } + + [JsonPropertyName("tw_rarity")] + public string TwRarity { get; set; } + } + + public class Leaderboard + { + public string Name { get; set; } + public SortMethod SortMethodSetting { get; set; } + public DisplayType DisplayTypeSetting { get; set; } + + public enum SortMethod + { + None, + Ascending, + Descending + } + public enum DisplayType + { + None, + Numeric, + TimeSeconds, + TimeMilliseconds + } + } + + public class Stat + { + public string Name { get; set; } + public StatType StatTypeSetting { get; set; } + public string Value { get; set; } + + public enum StatType + { + Int, + Float, + AvgRate + } } } \ No newline at end of file diff --git a/GoldbergGUI.Core/Models/SteamAppModel.cs b/GoldbergGUI.Core/Models/SteamAppModel.cs index 0b5f79e..4329b75 100644 --- a/GoldbergGUI.Core/Models/SteamAppModel.cs +++ b/GoldbergGUI.Core/Models/SteamAppModel.cs @@ -1,7 +1,6 @@ +using SQLite; using System.Collections.Generic; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using GoldbergGUI.Core.Utils; // ReSharper disable UnusedMember.Global // ReSharper disable ClassNeverInstantiated.Global @@ -11,35 +10,41 @@ using GoldbergGUI.Core.Utils; // ReSharper disable InconsistentNaming namespace GoldbergGUI.Core.Models { + [Table("steamapp")] public class SteamApp { - private string _name; - private string _comparableName; - [JsonPropertyName("appid")] public int AppId { get; set; } + [JsonPropertyName("appid")] + [Column("appid")] + [PrimaryKey] + public int AppId { get; set; } + /// + /// Name of Steam app + /// [JsonPropertyName("name")] - public string Name - { - get => _name; - set - { - _name = value; - _comparableName = Regex.Replace(value, Misc.SpecialCharsRegex, "").ToLower(); - } - } + [Column("name")] + public string Name { get; set; } - public bool CompareName(string value) => _comparableName.Equals(value); + [Column("comparable_name")] + public string ComparableName { get; set; } - public AppType type { get; set; } + /// + /// App type (Game, DLC, ...) + /// + [Column("type")] + public string AppType { get; set; } public override string ToString() { return $"{AppId}={Name}"; } - [JsonPropertyName("last_modified")] public long LastModified { get; set; } + [JsonPropertyName("last_modified")] + [Ignore] + public long LastModified { get; set; } [JsonPropertyName("price_change_number")] + [Ignore] public long PriceChangeNumber { get; set; } } @@ -67,19 +72,4 @@ namespace GoldbergGUI.Core.Models { [JsonPropertyName("response")] public override AppList AppList { get; set; } } - - public class AppType - { - private AppType(string value) => Value = value; - - public string Value { get; } - - public static AppType Game { get; } = new AppType("game"); - public static AppType DLC { get; } = new AppType("dlc"); - public static AppType Music { get; } = new AppType("music"); - public static AppType Demo { get; } = new AppType("demo"); - public static AppType Ad { get; } = new AppType("advertising"); - public static AppType Mod { get; } = new AppType("mod"); - public static AppType Video { get; } = new AppType("video"); - } } \ No newline at end of file diff --git a/GoldbergGUI.Core/Services/GoldbergService.cs b/GoldbergGUI.Core/Services/GoldbergService.cs index 83f32b5..6b7bcfa 100644 --- a/GoldbergGUI.Core/Services/GoldbergService.cs +++ b/GoldbergGUI.Core/Services/GoldbergService.cs @@ -1,3 +1,6 @@ +using GoldbergGUI.Core.Models; +using GoldbergGUI.Core.Utils; +using MvvmCross.Logging; using System; using System.Collections.Generic; using System.IO; @@ -5,10 +8,8 @@ using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; -using GoldbergGUI.Core.Models; -using MvvmCross.Logging; +using System.Windows; namespace GoldbergGUI.Core.Services { @@ -17,24 +18,25 @@ namespace GoldbergGUI.Core.Services // does file copy stuff public interface IGoldbergService { - public Task<(string accountName, long userSteamId, string language)> Initialize(IMvxLog log); + public Task Initialize(IMvxLog log); public Task Read(string path); public Task Save(string path, GoldbergConfiguration configuration); - public Task<(string accountName, long steamId, string language)> GetGlobalSettings(); - public Task SetGlobalSettings(string accountName, long userSteamId, string language); + public Task GetGlobalSettings(); + public Task SetGlobalSettings(GoldbergGlobalConfiguration configuration); public bool GoldbergApplied(string path); - public Task Download(); - public Task Extract(string archivePath); public Task GenerateInterfacesFile(string filePath); public List Languages(); } // ReSharper disable once UnusedType.Global + // ReSharper disable once ClassNeverInstantiated.Global public class GoldbergService : IGoldbergService { private IMvxLog _log; - private const string GoldbergUrl = "https://mr_goldberg.gitlab.io/goldberg_emulator/"; + private const string DefaultAccountName = "Mr_Goldberg"; + private const long DefaultSteamId = 76561197960287930; private const string DefaultLanguage = "english"; + private const string GoldbergUrl = "https://mr_goldberg.gitlab.io/goldberg_emulator/"; private readonly string _goldbergZipPath = Path.Combine(Directory.GetCurrentDirectory(), "goldberg.zip"); private readonly string _goldbergPath = Path.Combine(Directory.GetCurrentDirectory(), "goldberg"); @@ -46,6 +48,10 @@ namespace GoldbergGUI.Core.Services private readonly string _userSteamIdPath = Path.Combine(GlobalSettingsPath, "settings/user_steam_id.txt"); private readonly string _languagePath = Path.Combine(GlobalSettingsPath, "settings/language.txt"); + private readonly string _customBroadcastIpsPath = + Path.Combine(GlobalSettingsPath, "settings/custom_broadcasts.txt"); + + // ReSharper disable StringLiteralTypo private readonly List _interfaceNames = new List { "SteamClient", @@ -76,68 +82,124 @@ namespace GoldbergGUI.Core.Services // Call Download // Get global settings - public async Task<(string accountName, long userSteamId, string language)> Initialize(IMvxLog log) + public async Task Initialize(IMvxLog log) { _log = log; var download = await Download().ConfigureAwait(false); - if (download) await Extract(_goldbergZipPath).ConfigureAwait(false); + if (download) + { + await Extract(_goldbergZipPath).ConfigureAwait(false); + } + return await GetGlobalSettings().ConfigureAwait(false); } - public async Task<(string accountName, long steamId, string language)> GetGlobalSettings() + public async Task GetGlobalSettings() { _log.Info("Getting global settings..."); - var accountName = "Account name..."; - long steamId = -1; + var accountName = DefaultAccountName; + var steamId = DefaultSteamId; var language = DefaultLanguage; + var customBroadcastIps = new List(); + if (!File.Exists(GlobalSettingsPath)) Directory.CreateDirectory(Path.Join(GlobalSettingsPath, "settings")); await Task.Run(() => { if (File.Exists(_accountNamePath)) accountName = File.ReadLines(_accountNamePath).First().Trim(); if (File.Exists(_userSteamIdPath) && !long.TryParse(File.ReadLines(_userSteamIdPath).First().Trim(), out steamId) && steamId < 76561197960265729 && steamId > 76561202255233023) - _log.Error("Invalid User Steam ID!"); + { + _log.Error("Invalid User Steam ID! Using default Steam ID..."); + steamId = DefaultSteamId; + } + if (File.Exists(_languagePath)) language = File.ReadLines(_languagePath).First().Trim(); + if (File.Exists(_customBroadcastIpsPath)) + customBroadcastIps.AddRange( + File.ReadLines(_customBroadcastIpsPath).Select(line => line.Trim())); }).ConfigureAwait(false); - return (accountName, steamId, language); + _log.Info("Got global settings."); + return new GoldbergGlobalConfiguration + { + AccountName = accountName, + UserSteamId = steamId, + Language = language, + CustomBroadcastIps = customBroadcastIps + }; } - public async Task SetGlobalSettings(string accountName, long userSteamId, string language) + public async Task SetGlobalSettings(GoldbergGlobalConfiguration c) { + var accountName = c.AccountName; + var userSteamId = c.UserSteamId; + var language = c.Language; + var customBroadcastIps = c.CustomBroadcastIps; _log.Info("Setting global settings..."); - if (accountName != null && accountName != "Account name...") + // Account Name + if (!string.IsNullOrEmpty(accountName)) { _log.Info("Setting account name..."); + if (!File.Exists(_accountNamePath)) + await File.Create(_accountNamePath).DisposeAsync().ConfigureAwait(false); await File.WriteAllTextAsync(_accountNamePath, accountName).ConfigureAwait(false); } else { _log.Info("Invalid account name! Skipping..."); - await File.WriteAllTextAsync(_accountNamePath, "Goldberg").ConfigureAwait(false); + if (!File.Exists(_accountNamePath)) + await File.Create(_accountNamePath).DisposeAsync().ConfigureAwait(false); + await File.WriteAllTextAsync(_accountNamePath, DefaultAccountName).ConfigureAwait(false); } + // User SteamID if (userSteamId >= 76561197960265729 && userSteamId <= 76561202255233023) { _log.Info("Setting user Steam ID..."); + if (!File.Exists(_userSteamIdPath)) + await File.Create(_userSteamIdPath).DisposeAsync().ConfigureAwait(false); await File.WriteAllTextAsync(_userSteamIdPath, userSteamId.ToString()).ConfigureAwait(false); } else { _log.Info("Invalid user Steam ID! Skipping..."); - await Task.Run(() => File.Delete(_userSteamIdPath)).ConfigureAwait(false); + if (!File.Exists(_userSteamIdPath)) + await File.Create(_userSteamIdPath).DisposeAsync().ConfigureAwait(false); + await File.WriteAllTextAsync(_userSteamIdPath, DefaultSteamId.ToString()).ConfigureAwait(false); } - if (language != null) + // Language + if (!string.IsNullOrEmpty(language)) { _log.Info("Setting language..."); + if (!File.Exists(_languagePath)) + await File.Create(_languagePath).DisposeAsync().ConfigureAwait(false); await File.WriteAllTextAsync(_languagePath, language).ConfigureAwait(false); } else { _log.Info("Invalid language! Skipping..."); + if (!File.Exists(_languagePath)) + await File.Create(_languagePath).DisposeAsync().ConfigureAwait(false); await File.WriteAllTextAsync(_languagePath, DefaultLanguage).ConfigureAwait(false); } + + // Custom Broadcast IPs + if (customBroadcastIps != null && customBroadcastIps.Count > 0) + { + _log.Info("Setting custom broadcast IPs..."); + var result = + customBroadcastIps.Aggregate("", (current, address) => $"{current}{address}\n"); + if (!File.Exists(_customBroadcastIpsPath)) + await File.Create(_customBroadcastIpsPath).DisposeAsync().ConfigureAwait(false); + await File.WriteAllTextAsync(_customBroadcastIpsPath, result).ConfigureAwait(false); + } + else + { + _log.Info("Empty list of custom broadcast IPs! Skipping..."); + await Task.Run(() => File.Delete(_customBroadcastIpsPath)).ConfigureAwait(false); + } + _log.Info("Setting global configuration finished."); } // If first time, call GenerateInterfaces @@ -146,7 +208,8 @@ namespace GoldbergGUI.Core.Services { _log.Info("Reading configuration..."); var appId = -1; - var dlcList = new List(); + var achievementList = new List(); + var dlcList = new List(); var steamAppidTxt = Path.Combine(path, "steam_appid.txt"); if (File.Exists(steamAppidTxt)) { @@ -159,7 +222,21 @@ namespace GoldbergGUI.Core.Services _log.Info(@"""steam_appid.txt"" missing! Skipping..."); } + var achievementJson = Path.Combine(path, "steam_settings", "achievements.json"); + if (File.Exists(achievementJson)) + { + _log.Info("Getting achievements..."); + var json = await File.ReadAllTextAsync(achievementJson) + .ConfigureAwait(false); + achievementList = System.Text.Json.JsonSerializer.Deserialize>(json); + } + else + { + _log.Info(@"""steam_settings/achievements.json"" missing! Skipping..."); + } + var dlcTxt = Path.Combine(path, "steam_settings", "DLC.txt"); + var appPathTxt = Path.Combine(path, "steam_settings", "app_paths.txt"); if (File.Exists(dlcTxt)) { _log.Info("Getting DLCs..."); @@ -170,12 +247,27 @@ namespace GoldbergGUI.Core.Services { var match = expression.Match(line); if (match.Success) - dlcList.Add(new SteamApp + dlcList.Add(new DlcApp() { AppId = Convert.ToInt32(match.Groups["id"].Value), Name = match.Groups["name"].Value }); } + + // ReSharper disable once InvertIf + if (File.Exists(appPathTxt)) + { + var appPathAllLinesAsync = await File.ReadAllLinesAsync(appPathTxt).ConfigureAwait(false); + var appPathExpression = new Regex(@"(?.*) *= *(?.*)"); + foreach (var line in appPathAllLinesAsync) + { + var match = appPathExpression.Match(line); + if (!match.Success) continue; + var i = dlcList.FindIndex(x => + x.AppId.Equals(Convert.ToInt32(match.Groups["id"].Value))); + dlcList[i].AppPath = match.Groups["appPath"].Value; + } + } } else { @@ -185,6 +277,7 @@ namespace GoldbergGUI.Core.Services return new GoldbergConfiguration { AppId = appId, + Achievements = achievementList, DlcList = dlcList, Offline = File.Exists(Path.Combine(path, "steam_settings", "offline.txt")), DisableNetworking = File.Exists(Path.Combine(path, "steam_settings", "disable_networking.txt")), @@ -212,6 +305,7 @@ namespace GoldbergGUI.Core.Services { CopyDllFiles(path, x64Name); } + _log.Info("DLL setup finished!"); // Create steam_settings folder if missing _log.Info("Saving settings..."); @@ -221,52 +315,138 @@ namespace GoldbergGUI.Core.Services } // create steam_appid.txt - await File.WriteAllTextAsync(Path.Combine(path, "steam_appid.txt"), c.AppId.ToString()).ConfigureAwait(false); + await File.WriteAllTextAsync(Path.Combine(path, "steam_appid.txt"), c.AppId.ToString()) + .ConfigureAwait(false); - // DLC - if (c.DlcList.Count > 0) + // Achievements + Images + if (c.Achievements.Count > 0) { - var dlcString = ""; - c.DlcList.ForEach(x => dlcString += $"{x}\n"); - await File.WriteAllTextAsync(Path.Combine(path, "steam_settings", "DLC.txt"), dlcString) + _log.Info("Downloading images..."); + var imagePath = Path.Combine(path, "steam_settings", "images"); + Directory.CreateDirectory(imagePath); + + foreach (var achievement in c.Achievements) + { + await DownloadImageAsync(imagePath, achievement.Icon); + await DownloadImageAsync(imagePath, achievement.IconGray); + + // Update achievement list to point to local images instead + achievement.Icon = $"images/{Path.GetFileName(achievement.Icon)}"; + achievement.IconGray = $"images/{Path.GetFileName(achievement.IconGray)}"; + } + + _log.Info("Saving achievements..."); + + var achievementJson = System.Text.Json.JsonSerializer.Serialize( + c.Achievements, + new System.Text.Json.JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true + }); + await File.WriteAllTextAsync(Path.Combine(path, "steam_settings", "achievements.json"), achievementJson) .ConfigureAwait(false); + + _log.Info("Finished saving achievements."); } else { + _log.Info("No achievements set! Removing achievement files..."); + var imagePath = Path.Combine(path, "steam_settings", "images"); + if (Directory.Exists(imagePath)) + { + Directory.Delete(imagePath); + } + var achievementPath = Path.Combine(path, "steam_settings", "achievements"); + if (File.Exists(achievementPath)) + { + File.Delete(achievementPath); + } + _log.Info("Removed achievement files."); + } + + // DLC + App path + if (c.DlcList.Count > 0) + { + _log.Info("Saving DLC settings..."); + var dlcContent = ""; + //var depotContent = ""; + var appPathContent = ""; + c.DlcList.ForEach(x => + { + dlcContent += $"{x}\n"; + //depotContent += $"{x.DepotId}\n"; + if (!string.IsNullOrEmpty(x.AppPath)) + appPathContent += $"{x.AppId}={x.AppPath}\n"; + }); + await File.WriteAllTextAsync(Path.Combine(path, "steam_settings", "DLC.txt"), dlcContent) + .ConfigureAwait(false); + + /*if (!string.IsNullOrEmpty(depotContent)) + { + await File.WriteAllTextAsync(Path.Combine(path, "steam_settings", "depots.txt"), depotContent) + .ConfigureAwait(false); + }*/ + + + if (!string.IsNullOrEmpty(appPathContent)) + { + await File.WriteAllTextAsync(Path.Combine(path, "steam_settings", "app_paths.txt"), appPathContent) + .ConfigureAwait(false); + } + else + { + if (File.Exists(Path.Combine(path, "steam_settings", "app_paths.txt"))) + File.Delete(Path.Combine(path, "steam_settings", "app_paths.txt")); + } + _log.Info("Saved DLC settings."); + } + else + { + _log.Info("No DLC set! Removing DLC configuration files..."); if (File.Exists(Path.Combine(path, "steam_settings", "DLC.txt"))) File.Delete(Path.Combine(path, "steam_settings", "DLC.txt")); + if (File.Exists(Path.Combine(path, "steam_settings", "app_paths.txt"))) + File.Delete(Path.Combine(path, "steam_settings", "app_paths.txt")); + _log.Info("Removed DLC configuration files."); } // Offline if (c.Offline) { + _log.Info("Create offline.txt"); await File.Create(Path.Combine(path, "steam_settings", "offline.txt")).DisposeAsync() .ConfigureAwait(false); } else { + _log.Info("Delete offline.txt if it exists"); File.Delete(Path.Combine(path, "steam_settings", "offline.txt")); } // Disable Networking if (c.DisableNetworking) { + _log.Info("Create disable_networking.txt"); await File.Create(Path.Combine(path, "steam_settings", "disable_networking.txt")).DisposeAsync() .ConfigureAwait(false); } else { + _log.Info("Delete disable_networking.txt if it exists"); File.Delete(Path.Combine(path, "steam_settings", "disable_networking.txt")); } // Disable Overlay if (c.DisableOverlay) { + _log.Info("Create disable_overlay.txt"); await File.Create(Path.Combine(path, "steam_settings", "disable_overlay.txt")).DisposeAsync() .ConfigureAwait(false); } else { + _log.Info("Delete disable_overlay.txt if it exists"); File.Delete(Path.Combine(path, "steam_settings", "disable_overlay.txt")); } } @@ -275,17 +455,21 @@ namespace GoldbergGUI.Core.Services { var steamApiDll = Path.Combine(path, $"{name}.dll"); var originalDll = Path.Combine(path, $"{name}_o.dll"); - var guiBackup = Path.Combine(path, $"{name}.dll.GOLDBERGGUIBACKUP"); + var guiBackup = Path.Combine(path, $".{name}.dll.GOLDBERGGUIBACKUP"); var goldbergDll = Path.Combine(_goldbergPath, $"{name}.dll"); if (!File.Exists(originalDll)) + { + _log.Info("Back up original Steam API DLL..."); File.Move(steamApiDll, originalDll); + } else { File.Move(steamApiDll, guiBackup, true); File.SetAttributes(guiBackup, FileAttributes.Hidden); } + _log.Info("Copy Goldberg DLL to target path..."); File.Copy(goldbergDll, steamApiDll); } @@ -297,11 +481,11 @@ namespace GoldbergGUI.Core.Services return steamSettingsDirExists && steamAppIdTxtExists; } - // Get webpage - // Get job id, compare with local if exists, save it if false or missing - // Get latest archive if mismatch, call Extract - public async Task Download() + private async Task Download() { + // Get webpage + // Get job id, compare with local if exists, save it if false or missing + // Get latest archive if mismatch, call Extract _log.Info("Initializing download..."); if (!Directory.Exists(_goldbergPath)) Directory.CreateDirectory(_goldbergPath); var client = new HttpClient(); @@ -313,55 +497,118 @@ namespace GoldbergGUI.Core.Services var match = regex.Match(body); if (File.Exists(jobIdPath)) { - _log.Info("Check if update is needed..."); - var jobIdLocal = Convert.ToInt32(File.ReadLines(jobIdPath).First().Trim()); - var jobIdRemote = Convert.ToInt32(match.Groups["jobid"].Value); - _log.Debug($"job_id: local {jobIdLocal}; remote {jobIdRemote}"); - if (jobIdLocal.Equals(jobIdRemote)) + try { - _log.Info("Latest Goldberg emulator is already available! Skipping..."); - return false; + _log.Info("Check if update is needed..."); + var jobIdLocal = Convert.ToInt32(File.ReadLines(jobIdPath).First().Trim()); + var jobIdRemote = Convert.ToInt32(match.Groups["jobid"].Value); + _log.Debug($"job_id: local {jobIdLocal}; remote {jobIdRemote}"); + if (jobIdLocal.Equals(jobIdRemote)) + { + _log.Info("Latest Goldberg emulator is already available! Skipping..."); + return false; + } + } + catch (Exception) + { + _log.Error("An error occured, local Goldberg setup might be broken!"); } } + _log.Info("Starting download..."); - await StartDownload(client, match.Value).ConfigureAwait(false); + await StartDownload(match.Value).ConfigureAwait(false); return true; } - private async Task StartDownload(HttpClient client, string downloadUrl) + private async Task StartDownload(string downloadUrl) { - _log.Debug(downloadUrl); - await using var fileStream = File.OpenWrite(_goldbergZipPath); - //client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead) - var task = GetFileAsync(client, downloadUrl, fileStream).ConfigureAwait(false); - await task; - if (task.GetAwaiter().IsCompleted) + try { - _log.Info("Download finished!"); + var client = new HttpClient(); + _log.Debug(downloadUrl); + await using var fileStream = File.OpenWrite(_goldbergZipPath); + //client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead) + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, downloadUrl); + var headResponse = await client.SendAsync(httpRequestMessage).ConfigureAwait(false); + var contentLength = headResponse.Content.Headers.ContentLength; + await client.GetFileAsync(downloadUrl, fileStream).ContinueWith(async t => + { + // ReSharper disable once AccessToDisposedClosure + await fileStream.DisposeAsync().ConfigureAwait(false); + var fileLength = new FileInfo(_goldbergZipPath).Length; + // Environment.Exit(128); + if (contentLength == fileLength) + { + _log.Info("Download finished!"); + } + else + { + throw new Exception("File size does not match!"); + } + }).ConfigureAwait(false); + } + catch (Exception e) + { + ShowErrorMessage(); + _log.Error(e.ToString); + Environment.Exit(1); } - } - - private static async Task GetFileAsync(HttpClient client, string requestUri, Stream destination, - CancellationToken cancelToken = default) - { - var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancelToken) - .ConfigureAwait(false); - await using var download = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - await download.CopyToAsync(destination, cancelToken).ConfigureAwait(false); - if (destination.CanSeek) destination.Position = 0; } // Empty subfolder ./goldberg/ // Extract all from archive to subfolder ./goldberg/ - public async Task Extract(string archivePath) + private async Task Extract(string archivePath) { + var errorOccured = false; _log.Debug("Start extraction..."); - await Task.Run(() => + Directory.Delete(_goldbergPath, true); + Directory.CreateDirectory(_goldbergPath); + using (var archive = await Task.Run(() => ZipFile.OpenRead(archivePath)).ConfigureAwait(false)) + { + foreach (var entry in archive.Entries) + { + await Task.Run(() => + { + try + { + var fullPath = Path.Combine(_goldbergPath, entry.FullName); + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(fullPath); + } + else + { + entry.ExtractToFile(fullPath, true); + } + } + catch (Exception e) + { + errorOccured = true; + _log.Error($"Error while trying to extract {entry.FullName}"); + _log.Error(e.ToString); + } + }).ConfigureAwait(false); + } + } + + if (errorOccured) + { + ShowErrorMessage(); + _log.Warn("Error occured while extraction! Please setup Goldberg manually"); + } + _log.Info("Extraction was successful!"); + } + + private void ShowErrorMessage() + { + if (Directory.Exists(_goldbergPath)) { Directory.Delete(_goldbergPath, true); - ZipFile.ExtractToDirectory(archivePath, _goldbergPath); - }).ConfigureAwait(false); - _log.Debug("Extract done!"); + } + + Directory.CreateDirectory(_goldbergPath); + MessageBox.Show("Could not setup Goldberg Emulator!\n" + + "Please download it manually and extract its content into the \"goldberg\" subfolder!"); } // https://gitlab.com/Mr_Goldberg/goldberg_emulator/-/blob/master/generate_interfaces_file.cpp @@ -436,5 +683,22 @@ namespace GoldbergGUI.Core.Services return success; } + + private async Task DownloadImageAsync(string imageFolder, string imageUrl) + { + var fileName = Path.GetFileName(imageUrl); + var targetPath = Path.Combine(imageFolder, fileName); + if (File.Exists(targetPath)) + { + return; + } + else if (imageUrl.StartsWith("images/")) + { + _log.Warn($"Previously downloaded image '{imageUrl}' is now missing!"); + } + + var wc = new System.Net.WebClient(); + await wc.DownloadFileTaskAsync(new Uri(imageUrl, UriKind.Absolute), targetPath); + } } } \ No newline at end of file diff --git a/GoldbergGUI.Core/Services/SteamService.cs b/GoldbergGUI.Core/Services/SteamService.cs index 53d00fe..74c369b 100644 --- a/GoldbergGUI.Core/Services/SteamService.cs +++ b/GoldbergGUI.Core/Services/SteamService.cs @@ -1,19 +1,18 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading.Tasks; using AngleSharp.Dom; using AngleSharp.Html.Parser; using GoldbergGUI.Core.Models; using GoldbergGUI.Core.Utils; using MvvmCross.Logging; using NinjaNye.SearchExtensions; +using SQLite; using SteamStorefrontAPI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace GoldbergGUI.Core.Services { @@ -21,23 +20,21 @@ namespace GoldbergGUI.Core.Services public interface ISteamService { public Task Initialize(IMvxLog log); - public IEnumerable GetListOfAppsByName(string name); - public SteamApp GetAppByName(string name); - public SteamApp GetAppById(int appid); - public Task> GetListOfDlc(SteamApp steamApp, bool useSteamDb); + public Task> GetListOfAppsByName(string name); + public Task GetAppByName(string name); + public Task GetAppById(int appid); + public Task> GetListOfAchievements(SteamApp steamApp); + public Task> GetListOfDlc(SteamApp steamApp, bool useSteamDb); } class SteamCache { - public string Filename { get; } public string SteamUri { get; } public Type ApiVersion { get; } - public AppType SteamAppType { get; } - public HashSet Cache { get; set; } = new HashSet(); + public string SteamAppType { get; } - public SteamCache(string filename, string uri, Type apiVersion, AppType steamAppType) + public SteamCache(string uri, Type apiVersion, string steamAppType) { - Filename = filename; SteamUri = uri; ApiVersion = apiVersion; SteamAppType = steamAppType; @@ -49,32 +46,30 @@ namespace GoldbergGUI.Core.Services public class SteamService : ISteamService { // ReSharper disable StringLiteralTypo - private readonly Dictionary _caches = - new Dictionary + private readonly Dictionary _caches = + new Dictionary { { - AppType.Game, + AppTypeGame, new SteamCache( - "steamapps_games.json", "https://api.steampowered.com/IStoreService/GetAppList/v1/" + "?max_results=50000" + "&include_games=1" + "&key=" + Secrets.SteamWebApiKey(), typeof(SteamAppsV1), - AppType.Game + AppTypeGame ) }, { - AppType.DLC, + AppTypeDlc, new SteamCache( - "steamapps_dlc.json", "https://api.steampowered.com/IStoreService/GetAppList/v1/" + "?max_results=50000" + "&include_games=0" + "&include_dlc=1" + "&key=" + Secrets.SteamWebApiKey(), typeof(SteamAppsV1), - AppType.DLC + AppTypeDlc ) } }; @@ -84,147 +79,160 @@ namespace GoldbergGUI.Core.Services private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/87.0.4280.88 Safari/537.36"; + private const string AppTypeGame = "game"; + private const string AppTypeDlc = "dlc"; + private const string Database = "steamapps.cache"; + private const string GameSchemaUrl = "https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/"; private IMvxLog _log; + private SQLiteAsyncConnection _db; + public async Task Initialize(IMvxLog log) { - //var (path, uri, jsonType, appType) = _caches[0]; static SteamApps DeserializeSteamApps(Type type, string cacheString) { - if (type == typeof(SteamAppsV1)) - return JsonSerializer.Deserialize(cacheString); - else if (type == typeof(SteamAppsV2)) - return JsonSerializer.Deserialize(cacheString); - return null; + return type == typeof(SteamAppsV2) + ? (SteamApps)JsonSerializer.Deserialize(cacheString) + : JsonSerializer.Deserialize(cacheString); } - foreach (var (k, c) in _caches) - { - _log = log; - _log.Info($"Updating cache ({k.Value})..."); - var updateNeeded = - DateTime.Now.Subtract(File.GetLastWriteTimeUtc(c.Filename)).TotalDays >= 1; - SteamApps steamApps; - try - { - var temp = await GetCache(updateNeeded, c.SteamUri, c.Filename) - .ConfigureAwait(false); - steamApps = DeserializeSteamApps(c.ApiVersion, temp); - } - catch (JsonException) - { - _log.Error("Local cache broken, forcing update..."); - var temp = await GetCache(true, c.SteamUri, c.Filename).ConfigureAwait(false); - steamApps = DeserializeSteamApps(c.ApiVersion, temp); - } + _log = log; + _db = new SQLiteAsyncConnection(Database); + //_db.CreateTable(); + await _db.CreateTableAsync() + //.ContinueWith(x => _log.Debug("Table success!")) + .ConfigureAwait(false); - try + var countAsync = await _db.Table().CountAsync().ConfigureAwait(false); + if (DateTime.Now.Subtract(File.GetLastWriteTimeUtc(Database)).TotalDays >= 1 || countAsync == 0) + { + foreach (var (appType, steamCache) in _caches) { - var cacheRaw = new HashSet(steamApps.AppList.Apps); + _log.Info($"Updating cache ({appType})..."); + bool haveMoreResults; + long lastAppId = 0; + var client = new HttpClient(); + var cacheRaw = new HashSet(); + do + { + var response = lastAppId > 0 + ? await client.GetAsync($"{steamCache.SteamUri}&last_appid={lastAppId}") + .ConfigureAwait(false) + : await client.GetAsync(steamCache.SteamUri).ConfigureAwait(false); + var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var steamApps = DeserializeSteamApps(steamCache.ApiVersion, responseBody); + foreach (var appListApp in steamApps.AppList.Apps) cacheRaw.Add(appListApp); + haveMoreResults = steamApps.AppList.HaveMoreResults; + lastAppId = steamApps.AppList.LastAppid; + } while (haveMoreResults); + var cache = new HashSet(); foreach (var steamApp in cacheRaw) { - steamApp.type = c.SteamAppType; + steamApp.AppType = steamCache.SteamAppType; + steamApp.ComparableName = PrepareStringToCompare(steamApp.Name); cache.Add(steamApp); } - c.Cache = cache; - - _log.Info("Loaded cache into memory!"); - } - catch (NullReferenceException e) - { - Console.WriteLine(e); - throw; + await _db.InsertAllAsync(cache, "OR IGNORE").ConfigureAwait(false); } } } - private async Task GetCache(bool updateNeeded, string steamUri, string cachePath) + public async Task> GetListOfAppsByName(string name) { - string cacheString; - if (updateNeeded) - { - _log.Info("Getting content from API..."); - var client = new HttpClient(); - var response = await client.GetAsync(steamUri).ConfigureAwait(false); - var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - _log.Info("Got content from API successfully. Writing to file..."); - await File.WriteAllTextAsync(cachePath, responseBody, Encoding.UTF8).ConfigureAwait(false); - - _log.Info("Cache written to file successfully."); - cacheString = responseBody; - } - else - { - _log.Info("Cache already up to date!"); - cacheString = await File.ReadAllTextAsync(cachePath).ConfigureAwait(false); - } - - return cacheString; - } - - public IEnumerable GetListOfAppsByName(string name) - { - var listOfAppsByName = _caches[AppType.Game].Cache.Search(x => x.Name) + var query = await _db.Table() + .Where(x => x.AppType == AppTypeGame).ToListAsync().ConfigureAwait(false); + var listOfAppsByName = query.Search(x => x.Name) .SetCulture(StringComparison.OrdinalIgnoreCase) .ContainingAll(name.Split(' ')); return listOfAppsByName; } - public SteamApp GetAppByName(string name) + public async Task GetAppByName(string name) { _log.Info($"Trying to get app {name}"); - var comparableName = Regex.Replace(name, Misc.SpecialCharsRegex, "").ToLower(); - var app = _caches[AppType.Game].Cache.FirstOrDefault(x => x.CompareName(comparableName)); + var comparableName = PrepareStringToCompare(name); + var app = await _db.Table() + .FirstOrDefaultAsync(x => x.AppType == AppTypeGame && x.ComparableName.Equals(comparableName)) + .ConfigureAwait(false); if (app != null) _log.Info($"Successfully got app {app}"); return app; } - public SteamApp GetAppById(int appid) + public async Task GetAppById(int appid) { _log.Info($"Trying to get app with ID {appid}"); - var app = _caches[AppType.Game].Cache.FirstOrDefault(x => x.AppId.Equals(appid)); + var app = await _db.Table().Where(x => x.AppType == AppTypeGame) + .FirstOrDefaultAsync(x => x.AppId.Equals(appid)).ConfigureAwait(false); if (app != null) _log.Info($"Successfully got app {app}"); return app; } - public async Task> GetListOfDlc(SteamApp steamApp, bool useSteamDb) + public async Task> GetListOfAchievements(SteamApp steamApp) { - _log.Info("Get DLC"); - var dlcList = new List(); + var achievementList = new List(); + if (steamApp == null) + { + return achievementList; + } + + _log.Info($"Getting achievements for App {steamApp}"); + + var client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent); + var apiUrl = $"{GameSchemaUrl}?key={Secrets.SteamWebApiKey()}&appid={steamApp.AppId}&l=en"; + + var response = await client.GetAsync(apiUrl); + var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + var jsonResponse = JsonDocument.Parse(responseBody); + var achievementData = jsonResponse.RootElement.GetProperty("game") + .GetProperty("availableGameStats") + .GetProperty("achievements"); + + achievementList = JsonSerializer.Deserialize>(achievementData.GetRawText()); + return achievementList; + } + + public async Task> GetListOfDlc(SteamApp steamApp, bool useSteamDb) + { + var dlcList = new List(); if (steamApp != null) { + _log.Info($"Get DLC for App {steamApp}"); var task = AppDetails.GetAsync(steamApp.AppId); var steamAppDetails = await task.ConfigureAwait(true); - if (steamAppDetails.Type == AppType.Game.Value) + if (steamAppDetails.Type == AppTypeGame) { - steamAppDetails.DLC.ForEach(x => + steamAppDetails.DLC.ForEach(async x => { - var result = _caches[AppType.DLC].Cache.FirstOrDefault(y => y.AppId.Equals(x)) - ?? new SteamApp {AppId = x, Name = $"Unknown DLC {x}"}; - dlcList.Add(result); + var result = await _db.Table().Where(z => z.AppType == AppTypeDlc) + .FirstOrDefaultAsync(y => y.AppId.Equals(x)).ConfigureAwait(true) + ?? new SteamApp() { AppId = x, Name = $"Unknown DLC {x}", ComparableName = $"unknownDlc{x}", AppType = AppTypeDlc }; + dlcList.Add(new DlcApp(result)); + _log.Debug($"{result.AppId}={result.Name}"); }); - dlcList.ForEach(x => _log.Debug($"{x.AppId}={x.Name}")); _log.Info("Got DLC successfully..."); // Get DLC from SteamDB - // Get Cloudflare cookie + // Get Cloudflare cookie (not implemented) // Scrape and parse HTML page // Add missing to DLC list - - // ReSharper disable once InvertIf - if (useSteamDb) + + // Return current list if we don't intend to use SteamDB + if (!useSteamDb) return dlcList; + + try { var steamDbUri = new Uri($"https://steamdb.info/app/{steamApp.AppId}/dlc/"); var client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent); - _log.Info("Get SteamDB App"); + _log.Info($"Get SteamDB App {steamApp}"); var httpCall = client.GetAsync(steamDbUri); var response = await httpCall.ConfigureAwait(false); _log.Debug(httpCall.Status.ToString()); @@ -240,6 +248,7 @@ namespace GoldbergGUI.Core.Services var query1 = doc.QuerySelector("#dlc"); if (query1 != null) { + _log.Info("Got list of DLC from SteamDB."); var query2 = query1.QuerySelectorAll(".app"); foreach (var element in query2) { @@ -248,7 +257,7 @@ namespace GoldbergGUI.Core.Services var dlcName = query3 != null ? query3[1].Text().Replace("\n", "").Trim() : $"Unknown DLC {dlcId}"; - var dlcApp = new SteamApp {AppId = Convert.ToInt32(dlcId), Name = dlcName}; + var dlcApp = new DlcApp { AppId = Convert.ToInt32(dlcId), Name = dlcName }; var i = dlcList.FindIndex(x => x.AppId.Equals(dlcApp.AppId)); if (i > -1) { @@ -268,6 +277,11 @@ namespace GoldbergGUI.Core.Services _log.Error("Could not get DLC from SteamDB!"); } } + catch (Exception e) + { + _log.Error("Could not get DLC from SteamDB! Skipping..."); + _log.Error(e.ToString); + } } else { @@ -281,5 +295,10 @@ namespace GoldbergGUI.Core.Services return dlcList; } + + private static string PrepareStringToCompare(string name) + { + return Regex.Replace(name, Misc.AlphaNumOnlyRegex, "").ToLower(); + } } } \ No newline at end of file diff --git a/GoldbergGUI.Core/Utils/CustomMvxAppStart.cs b/GoldbergGUI.Core/Utils/CustomMvxAppStart.cs index d5fcc3a..8fa735e 100644 --- a/GoldbergGUI.Core/Utils/CustomMvxAppStart.cs +++ b/GoldbergGUI.Core/Utils/CustomMvxAppStart.cs @@ -1,8 +1,7 @@ -using System; -using System.Threading.Tasks; using MvvmCross.Exceptions; using MvvmCross.Navigation; using MvvmCross.ViewModels; +using System.Threading.Tasks; namespace GoldbergGUI.Core.Utils { diff --git a/GoldbergGUI.Core/Utils/Extensions.cs b/GoldbergGUI.Core/Utils/Extensions.cs new file mode 100644 index 0000000..2c03d9a --- /dev/null +++ b/GoldbergGUI.Core/Utils/Extensions.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace GoldbergGUI.Core.Utils +{ + public static class Extensions + { + public static async Task GetFileAsync(this HttpClient client, string requestUri, Stream destination, + CancellationToken cancelToken = default) + { + var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancelToken) + .ConfigureAwait(false); + await using var download = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await download.CopyToAsync(destination, cancelToken).ConfigureAwait(false); + if (destination.CanSeek) destination.Position = 0; + } + } +} \ No newline at end of file diff --git a/GoldbergGUI.Core/Utils/Misc.cs b/GoldbergGUI.Core/Utils/Misc.cs index 2a76437..9e315d3 100644 --- a/GoldbergGUI.Core/Utils/Misc.cs +++ b/GoldbergGUI.Core/Utils/Misc.cs @@ -1,42 +1,25 @@ -using System.Collections.ObjectModel; - namespace GoldbergGUI.Core.Utils { - public class Misc + public static class Misc { - public const string SpecialCharsRegex = "[^0-9a-zA-Z]+"; - public const string DefaultLanguageSelection = "english"; - public static readonly ObservableCollection DefaultLanguages = new ObservableCollection(new[] - { - "arabic", - "bulgarian", - "schinese", - "tchinese", - "czech", - "danish", - "dutch", - "english", - "finnish", - "french", - "german", - "greek", - "hungarian", - "italian", - "japanese", - "koreana", - "norwegian", - "polish", - "portuguese", - "brazilian", - "romanian", - "russian", - "spanish", - "latam", - "swedish", - "thai", - "turkish", - "ukrainian", - "vietnamese" - }); + public const string AlphaNumOnlyRegex = "[^0-9a-zA-Z]+"; + } + + public class GlobalHelp + { + public static string Header => + "Information\n"; + + public static string TextPreLink => + "Usually these settings are saved under"; + + public static string Link => "%APPDATA%\\Goldberg SteamEmu Saves\\settings"; + + public static string TextPostLink => + ", which makes these " + + "available for every game that uses the Goldberg Emulator. However, if you want to set specific settings " + + "for certain games (e.g. different language), you can remove the \"Global\" checkmark next to the option " + + "and then change it. If you want to remove that setting, just empty the field while \"Global\" is " + + "unchecked. (Not implemented yet!)"; } } \ No newline at end of file diff --git a/GoldbergGUI.Core/ViewModels/MainViewModel.cs b/GoldbergGUI.Core/ViewModels/MainViewModel.cs index 667c1b4..54580c7 100644 --- a/GoldbergGUI.Core/ViewModels/MainViewModel.cs +++ b/GoldbergGUI.Core/ViewModels/MainViewModel.cs @@ -1,4 +1,12 @@ -using System; +using GoldbergGUI.Core.Models; +using GoldbergGUI.Core.Services; +using GoldbergGUI.Core.Utils; +using Microsoft.Win32; +using MvvmCross.Commands; +using MvvmCross.Logging; +using MvvmCross.Navigation; +using MvvmCross.ViewModels; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -9,14 +17,6 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; -using GoldbergGUI.Core.Models; -using GoldbergGUI.Core.Services; -using Microsoft.Win32; -using MvvmCross; -using MvvmCross.Commands; -using MvvmCross.Logging; -using MvvmCross.Navigation; -using MvvmCross.ViewModels; namespace GoldbergGUI.Core.ViewModels { @@ -30,7 +30,8 @@ namespace GoldbergGUI.Core.ViewModels private int _appId; //private SteamApp _currentGame; - private ObservableCollection _dlcs; + private ObservableCollection _achievements; + private ObservableCollection _dlcs; private string _accountName; private long _steamId; private bool _offline; @@ -71,11 +72,11 @@ namespace GoldbergGUI.Core.ViewModels SteamLanguages = new ObservableCollection(_goldberg.Languages()); ResetForm(); await _steam.Initialize(_logProvider.GetLogFor()).ConfigureAwait(false); - var (accountName, userSteamId, language) = + var globalConfiguration = await _goldberg.Initialize(_logProvider.GetLogFor()).ConfigureAwait(false); - AccountName = accountName; - SteamId = userSteamId; - SelectedLanguage = language; + AccountName = globalConfiguration.AccountName; + SteamId = globalConfiguration.UserSteamId; + SelectedLanguage = globalConfiguration.Language; } catch (Exception e) { @@ -130,7 +131,7 @@ namespace GoldbergGUI.Core.ViewModels } // ReSharper disable once InconsistentNaming - public ObservableCollection DLCs + public ObservableCollection DLCs { get => _dlcs; set @@ -142,6 +143,16 @@ namespace GoldbergGUI.Core.ViewModels } } + public ObservableCollection Achievements + { + get => _achievements; + set + { + _achievements = value; + RaisePropertyChanged(() => Achievements); + } + } + public string AccountName { get => _accountName; @@ -221,7 +232,15 @@ namespace GoldbergGUI.Core.ViewModels } } - public bool DllSelected => !DllPath.Contains("Path to game's steam_api(64).dll"); + public bool DllSelected + { + get + { + var value = !DllPath.Contains("Path to game's steam_api(64).dll"); + if (!value) _log.Warn("No DLL selected! Skipping..."); + return value; + } + } public ObservableCollection SteamLanguages { @@ -244,9 +263,6 @@ namespace GoldbergGUI.Core.ViewModels } } - public string AboutVersionText => - FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion; - public string StatusText { get => _statusText; @@ -257,6 +273,11 @@ namespace GoldbergGUI.Core.ViewModels } } + public static string AboutVersionText => + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion; + + public static GlobalHelp G => new GlobalHelp(); + // COMMANDS // public IMvxCommand OpenFileCommand => new MvxAsyncCommand(OpenFile); @@ -282,6 +303,7 @@ namespace GoldbergGUI.Core.ViewModels DllPath = dialog.FileName; await ReadConfig().ConfigureAwait(false); + if (!GoldbergApplied) await GetListOfDlc().ConfigureAwait(false); MainWindowEnabled = true; StatusText = "Ready."; } @@ -310,7 +332,7 @@ namespace GoldbergGUI.Core.ViewModels MainWindowEnabled = false; StatusText = "Trying to find AppID..."; - var appByName = _steam.GetAppByName(_gameName); + var appByName = await _steam.GetAppByName(_gameName).ConfigureAwait(false); if (appByName != null) { GameName = appByName.Name; @@ -318,7 +340,7 @@ namespace GoldbergGUI.Core.ViewModels } else { - var list = _steam.GetListOfAppsByName(GameName); + var list = await _steam.GetListOfAppsByName(GameName).ConfigureAwait(false); var steamApps = list as SteamApp[] ?? list.ToArray(); if (steamApps.Length == 1) { @@ -338,7 +360,7 @@ namespace GoldbergGUI.Core.ViewModels await FindIdInList(steamApps).ConfigureAwait(false); } } - + await GetListOfDlc().ConfigureAwait(false); MainWindowEnabled = true; StatusText = "Ready."; } @@ -353,10 +375,37 @@ namespace GoldbergGUI.Core.ViewModels return; } - var steamApp = await Task.Run(() => _steam.GetAppById(AppId)).ConfigureAwait(false); + var steamApp = await _steam.GetAppById(AppId).ConfigureAwait(false); if (steamApp != null) GameName = steamApp.Name; } + public IMvxCommand GetListOfAchievementsCommand => new MvxAsyncCommand(GetListOfAchievements); + + private async Task GetListOfAchievements() + { + if (AppId <= 0) + { + _log.Error("Invalid Steam App!"); + return; + } + + MainWindowEnabled = false; + StatusText = "Trying to get list of achievements..."; + var listOfAchievements = await _steam.GetListOfAchievements(new SteamApp { AppId = AppId, Name = GameName }); + Achievements = new MvxObservableCollection(listOfAchievements); + MainWindowEnabled = true; + + if (Achievements.Count > 0) + { + var empty = Achievements.Count == 1 ? "" : "s"; + StatusText = $"Successfully got {Achievements.Count} achievement{empty}! Ready."; + } + else + { + StatusText = "No achievements found! Ready."; + } + } + public IMvxCommand GetListOfDlcCommand => new MvxAsyncCommand(GetListOfDlc); private async Task GetListOfDlc() @@ -369,9 +418,9 @@ namespace GoldbergGUI.Core.ViewModels MainWindowEnabled = false; StatusText = "Trying to get list of DLCs..."; - var listOfDlc = await _steam.GetListOfDlc(new SteamApp {AppId = AppId, Name = GameName}, true) + var listOfDlc = await _steam.GetListOfDlc(new SteamApp { AppId = AppId, Name = GameName }, true) .ConfigureAwait(false); - DLCs = new MvxObservableCollection(listOfDlc); + DLCs = new MvxObservableCollection(listOfDlc); MainWindowEnabled = true; if (DLCs.Count > 0) { @@ -389,25 +438,28 @@ namespace GoldbergGUI.Core.ViewModels private async Task SaveConfig() { _log.Info("Saving global settings..."); - await _goldberg.SetGlobalSettings(AccountName, SteamId, SelectedLanguage).ConfigureAwait(false); - if (!DllSelected) + var globalConfiguration = new GoldbergGlobalConfiguration { - _log.Error("No DLL selected!"); - return; - } + AccountName = AccountName, + UserSteamId = SteamId, + Language = SelectedLanguage + }; + await _goldberg.SetGlobalSettings(globalConfiguration).ConfigureAwait(false); + if (!DllSelected) return; _log.Info("Saving Goldberg settings..."); if (!GetDllPathDir(out var dirPath)) return; MainWindowEnabled = false; StatusText = "Saving..."; await _goldberg.Save(dirPath, new GoldbergConfiguration - { - AppId = AppId, - DlcList = DLCs.ToList(), - Offline = Offline, - DisableNetworking = DisableNetworking, - DisableOverlay = DisableOverlay - } + { + AppId = AppId, + Achievements = Achievements.ToList(), + DlcList = DLCs.ToList(), + Offline = Offline, + DisableNetworking = DisableNetworking, + DisableOverlay = DisableOverlay + } ).ConfigureAwait(false); GoldbergApplied = _goldberg.GoldbergApplied(dirPath); MainWindowEnabled = true; @@ -418,12 +470,11 @@ namespace GoldbergGUI.Core.ViewModels private async Task ResetConfig() { - (AccountName, SteamId, SelectedLanguage) = await _goldberg.GetGlobalSettings().ConfigureAwait(false); - if (!DllSelected) - { - _log.Error("No DLL selected!"); - return; - } + var globalConfiguration = await _goldberg.GetGlobalSettings().ConfigureAwait(false); + AccountName = globalConfiguration.AccountName; + SteamId = globalConfiguration.UserSteamId; + SelectedLanguage = globalConfiguration.Language; + if (!DllSelected) return; _log.Info("Reset form..."); MainWindowEnabled = false; @@ -437,11 +488,7 @@ namespace GoldbergGUI.Core.ViewModels private async Task GenerateSteamInterfaces() { - if (!DllSelected) - { - _log.Error("No DLL selected!"); - return; - } + if (!DllSelected) return; _log.Info("Generate steam_interfaces.txt..."); MainWindowEnabled = false; @@ -468,29 +515,25 @@ namespace GoldbergGUI.Core.ViewModels } else { - var pastedDlc = new List(); var result = Clipboard.GetText(); var expression = new Regex(@"(?.*) *= *(?.*)"); - foreach (var line in result.Split(new[] - { - "\n", - "\r\n" - }, StringSplitOptions.RemoveEmptyEntries)) - { - var match = expression.Match(line); - if (match.Success) - pastedDlc.Add(new SteamApp - { - AppId = Convert.ToInt32(match.Groups["id"].Value), - Name = match.Groups["name"].Value - }); - } + var pastedDlc = (from line in result.Split(new[] { "\n", "\r\n" }, + StringSplitOptions.RemoveEmptyEntries) + select expression.Match(line) into match + where match.Success + select new DlcApp + { + AppId = Convert.ToInt32(match.Groups["id"].Value), + Name = match.Groups["name"].Value + }).ToList(); if (pastedDlc.Count > 0) { DLCs.Clear(); - DLCs = new ObservableCollection(pastedDlc); - var empty = DLCs.Count == 1 ? "" : "s"; - StatusText = $"Successfully got {DLCs.Count} DLC{empty} from clipboard! Ready."; + DLCs = new ObservableCollection(pastedDlc); + //var empty = DLCs.Count == 1 ? "" : "s"; + //StatusText = $"Successfully got {DLCs.Count} DLC{empty} from clipboard! Ready."; + var statusTextCount = DLCs.Count == 1 ? "one DLC" : $"{DLCs.Count} DLCs"; + StatusText = $"Successfully got {statusTextCount} from clipboard! Ready."; } else { @@ -499,6 +542,22 @@ namespace GoldbergGUI.Core.ViewModels } }); + public IMvxCommand OpenGlobalSettingsFolderCommand => new MvxCommand(OpenGlobalSettingsFolder); + + private void OpenGlobalSettingsFolder() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + StatusText = "Can't open folder (Windows only)! Ready."; + return; + } + + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Goldberg SteamEmu Saves", "settings"); + var start = Process.Start("explorer.exe", path); + start?.Dispose(); + } + // OTHER METHODS // private void ResetForm() @@ -506,7 +565,8 @@ namespace GoldbergGUI.Core.ViewModels DllPath = "Path to game's steam_api(64).dll..."; GameName = "Game name..."; AppId = -1; - DLCs = new ObservableCollection(); + Achievements = new ObservableCollection(); + DLCs = new ObservableCollection(); AccountName = "Account name..."; SteamId = -1; Offline = false; @@ -526,7 +586,8 @@ namespace GoldbergGUI.Core.ViewModels private void SetFormFromConfig(GoldbergConfiguration config) { AppId = config.AppId; - DLCs = new ObservableCollection(config.DlcList); + Achievements = new ObservableCollection(config.Achievements); + DLCs = new ObservableCollection(config.DlcList); Offline = config.Offline; DisableNetworking = config.DisableNetworking; DisableOverlay = config.DisableOverlay; @@ -536,7 +597,6 @@ namespace GoldbergGUI.Core.ViewModels { if (!DllSelected) { - _log.Error("No DLL selected!"); dirPath = null; return false; } diff --git a/GoldbergGUI.Core/ViewModels/SearchResultViewModel.cs b/GoldbergGUI.Core/ViewModels/SearchResultViewModel.cs index dd46194..35f4dc2 100644 --- a/GoldbergGUI.Core/ViewModels/SearchResultViewModel.cs +++ b/GoldbergGUI.Core/ViewModels/SearchResultViewModel.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using GoldbergGUI.Core.Models; using MvvmCross.Commands; using MvvmCross.Logging; using MvvmCross.Navigation; using MvvmCross.ViewModels; +using System.Collections.Generic; +using System.Threading.Tasks; namespace GoldbergGUI.Core.ViewModels { @@ -14,7 +14,7 @@ namespace GoldbergGUI.Core.ViewModels private readonly IMvxLog _log; private IEnumerable _apps; - public SearchResultViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : + public SearchResultViewModel(IMvxLogProvider logProvider, IMvxNavigationService navigationService) : base(logProvider, navigationService) { _log = logProvider.GetLogFor(typeof(SearchResultViewModel)); @@ -25,7 +25,7 @@ namespace GoldbergGUI.Core.ViewModels { Apps = parameter; } - + public IEnumerable Apps { get => _apps; @@ -35,7 +35,7 @@ namespace GoldbergGUI.Core.ViewModels RaisePropertyChanged(() => Apps); } } - + public SteamApp Selected { get; diff --git a/GoldbergGUI.WPF/App.xaml.cs b/GoldbergGUI.WPF/App.xaml.cs index 7b2e5c1..5d14782 100644 --- a/GoldbergGUI.WPF/App.xaml.cs +++ b/GoldbergGUI.WPF/App.xaml.cs @@ -1,5 +1,4 @@ using MvvmCross.Core; -using MvvmCross.Platforms.Wpf.Core; using MvvmCross.Platforms.Wpf.Views; namespace GoldbergGUI.WPF diff --git a/GoldbergGUI.WPF/AssemblyInfo.cs b/GoldbergGUI.WPF/AssemblyInfo.cs index 4a05c7d..4f943de 100644 --- a/GoldbergGUI.WPF/AssemblyInfo.cs +++ b/GoldbergGUI.WPF/AssemblyInfo.cs @@ -2,9 +2,9 @@ using System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) + //(used if a resource is not found in the page, + // or application resource dictionaries) ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) )] \ No newline at end of file diff --git a/GoldbergGUI.WPF/GoldbergGUI.WPF.csproj b/GoldbergGUI.WPF/GoldbergGUI.WPF.csproj index 81eaa19..6e1a7aa 100644 --- a/GoldbergGUI.WPF/GoldbergGUI.WPF.csproj +++ b/GoldbergGUI.WPF/GoldbergGUI.WPF.csproj @@ -2,11 +2,11 @@ WinExe - netcoreapp3.1 + net8.0-windows true - 0.1.0 - 0.1.0 + 0.3.0 Jeddunk + AnyCPU;x86;x64 @@ -20,4 +20,20 @@ + + + + + + + + + + + + + + + + diff --git a/GoldbergGUI.WPF/MainWindow.xaml b/GoldbergGUI.WPF/MainWindow.xaml index 313c271..4d57311 100644 --- a/GoldbergGUI.WPF/MainWindow.xaml +++ b/GoldbergGUI.WPF/MainWindow.xaml @@ -5,6 +5,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="GoldbergGUI" MinHeight="500" MinWidth="600" Background="#FFF0F0F0"> + Title="GoldbergGUI" MinHeight="600" MinWidth="800" Background="#FFF0F0F0"> \ No newline at end of file diff --git a/GoldbergGUI.WPF/Setup.cs b/GoldbergGUI.WPF/Setup.cs index e11f828..c9f6d78 100644 --- a/GoldbergGUI.WPF/Setup.cs +++ b/GoldbergGUI.WPF/Setup.cs @@ -1,20 +1,17 @@ -using System; -using System.IO; -using System.Windows.Controls; -using System.Windows.Threading; using MvvmCross.Logging; using MvvmCross.Platforms.Wpf.Core; using Serilog; +using System.IO; namespace GoldbergGUI.WPF { public class Setup : MvxWpfSetup { public override MvxLogProviderType GetDefaultLogProviderType() => MvxLogProviderType.Serilog; - + protected override IMvxLogProvider CreateLogProvider() { - var logPath = Path.Combine(Directory.GetCurrentDirectory(),"goldberg_.log"); + var logPath = Path.Combine(Directory.GetCurrentDirectory(), "goldberg_.log"); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() diff --git a/GoldbergGUI.WPF/Views/MainView.xaml b/GoldbergGUI.WPF/Views/MainView.xaml index 6f2ce2d..83b115d 100644 --- a/GoldbergGUI.WPF/Views/MainView.xaml +++ b/GoldbergGUI.WPF/Views/MainView.xaml @@ -7,6 +7,9 @@ xmlns:viewmodel="clr-namespace:GoldbergGUI.Core.ViewModels;assembly=GoldbergGUI.Core" mc:Ignorable="d" d:DesignHeight="500" d:DesignWidth="400" d:DataContext="{d:DesignInstance Type=viewmodel:MainViewModel }"> + + + @@ -29,7 +32,7 @@ - + @@ -39,30 +42,71 @@