/* Copyright (c) 2019 Rick (rick 'at' gibbed 'dot' us) * Copyright (c) 2018-2019 Jan Klass (kissaki@posteo.de) * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would * be appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not * be misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source * distribution. */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Windows.Forms; using System.Xml.XPath; using APITypes = SAM.API.Types; namespace SAM.Picker { internal partial class GamePicker : Form { private static string VersionAgnosticLocalAppdata = Directory.GetParent(Application.LocalUserAppDataPath).ToString(); private static string CacheDir = Path.Combine(VersionAgnosticLocalAppdata, "cache"); private readonly API.Client _SteamClient; private readonly Dictionary _Games = new Dictionary(); private readonly List _FilteredGames = new List(); private int _SelectedGameIndex = -1; private readonly List _LogosAttempted = new List(); private readonly ConcurrentQueue _LogoQueue = new ConcurrentQueue(); // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable private readonly API.Callbacks.AppDataChanged _AppDataChangedCallback; // ReSharper restore PrivateFieldCanBeConvertedToLocalVariable public GamePicker(API.Client client) { this.InitializeComponent(); var blank = new Bitmap(this._LogoImageList.ImageSize.Width, this._LogoImageList.ImageSize.Height); using (var g = Graphics.FromImage(blank)) { g.Clear(Color.DimGray); } this._LogoImageList.Images.Add("Blank", blank); this._SteamClient = client; this._AppDataChangedCallback = client.CreateAndRegisterCallback(); this._AppDataChangedCallback.OnRun += this.OnAppDataChanged; this.AddGames(); } private void OnAppDataChanged(APITypes.AppDataChanged param) { if (param.Result == true && this._Games.ContainsKey(param.Id)) { var game = this._Games[param.Id]; game.Name = this._SteamClient.SteamApps001.GetAppData(game.Id, "name"); this.AddGameToLogoQueue(game); this.DownloadNextLogo(); } } private void DoDownloadList(object sender, DoWorkEventArgs e) { var pairs = new List>(); byte[] bytes; using (var downloader = new WebClient()) { bytes = downloader.DownloadData(new Uri("http://gib.me/sam/games.xml")); } using (var stream = new MemoryStream(bytes, false)) { var document = new XPathDocument(stream); var navigator = document.CreateNavigator(); var nodes = navigator.Select("/games/game"); while (nodes.MoveNext()) { string type = nodes.Current.GetAttribute("type", ""); if (string.IsNullOrEmpty(type) == true) { type = "normal"; } pairs.Add(new KeyValuePair((uint)nodes.Current.ValueAsLong, type)); } } foreach (var kv in pairs) { this.AddGame(kv.Key, kv.Value); } } private void OnDownloadList(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null || e.Cancelled) { this.AddDefaultGames(); //MessageBox.Show(e.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } this.RefreshGames(); this._RefreshGamesButton.Enabled = true; this.DownloadNextLogo(); } private void RefreshGames() { this._SelectedGameIndex = -1; this._FilteredGames.Clear(); foreach (var info in this._Games.Values.OrderBy(gi => gi.Name)) { if (info.Type == "normal" && _FilterGamesMenuItem.Checked == false) { continue; } if (info.Type == "demo" && this._FilterDemosMenuItem.Checked == false) { continue; } if (info.Type == "mod" && this._FilterModsMenuItem.Checked == false) { continue; } if (info.Type == "junk" && this._FilterJunkMenuItem.Checked == false) { continue; } this._FilteredGames.Add(info); this.AddGameToLogoQueue(info); } this._GameListView.VirtualListSize = this._FilteredGames.Count; this._PickerStatusLabel.Text = string.Format( CultureInfo.CurrentCulture, "Displaying {0} games. Total {1} games.", this._GameListView.Items.Count, this._Games.Count); if (this._GameListView.Items.Count > 0) { this._GameListView.Items[0].Selected = true; this._GameListView.Select(); } } private void OnGameListViewRetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { var info = this._FilteredGames[e.ItemIndex]; e.Item = new ListViewItem() { Text = info.Name, ImageIndex = info.ImageIndex, }; } private void OnGameListViewSearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e) { if (e.Direction != SearchDirectionHint.Down || e.IsTextSearch == false) { return; } var count = this._FilteredGames.Count; if (count < 2) { return; } var text = e.Text; int startIndex = e.StartIndex; Predicate predicate; /*if (e.IsPrefixSearch == true)*/ { predicate = gi => gi.Name != null && gi.Name.StartsWith(text, StringComparison.CurrentCultureIgnoreCase); } /*else { predicate = gi => gi.Name != null && string.Compare(gi.Name, text, StringComparison.CurrentCultureIgnoreCase) == 0; }*/ int index; if (e.StartIndex >= count) { // starting from the last item in the list index = this._FilteredGames.FindIndex(0, startIndex - 1, predicate); } else if (startIndex <= 0) { // starting from the first item in the list index = this._FilteredGames.FindIndex(0, count, predicate); } else { index = this._FilteredGames.FindIndex(startIndex, count - startIndex, predicate); if (index < 0) { index = this._FilteredGames.FindIndex(0, startIndex - 1, predicate); } } e.Index = index < 0 ? -1 : index; } private void DoDownloadLogo(object sender, DoWorkEventArgs e) { var info = (GameInfo)e.Argument; Directory.CreateDirectory(CacheDir); var appDir = Path.Combine(CacheDir, info.Id.ToString("D")); var imgPath = Path.Combine(appDir, info.Logo + ".jpg"); if (File.Exists(imgPath)) { e.Result = new LogoInfo(info.Id, new Bitmap(File.OpenRead(imgPath))); return; } var logoPath = string.Format( CultureInfo.InvariantCulture, "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/{0}/{1}.jpg", info.Id, info.Logo); using (var downloader = new WebClient()) { try { var data = downloader.DownloadData(new Uri(logoPath)); using (var stream = new MemoryStream(data, false)) { var bitmap = new Bitmap(stream); Directory.CreateDirectory(appDir); bitmap.Save(imgPath); e.Result = new LogoInfo(info.Id, bitmap); } } catch (Exception) { e.Result = new LogoInfo(info.Id, null); } } } private void OnDownloadLogo(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null || e.Cancelled == true) { return; } var logoInfo = (LogoInfo)e.Result; if (logoInfo.Bitmap != null && this._Games.TryGetValue(logoInfo.Id, out var gameInfo)) { this._GameListView.BeginUpdate(); var imageIndex = this._LogoImageList.Images.Count; this._LogoImageList.Images.Add(gameInfo.Logo, logoInfo.Bitmap); gameInfo.ImageIndex = imageIndex; this._GameListView.EndUpdate(); } this.DownloadNextLogo(); } private void DownloadNextLogo() { if (this._LogoWorker.IsBusy == true) { return; } GameInfo info; if (this._LogoQueue.TryDequeue(out info) == false) { this._DownloadStatusLabel.Visible = false; return; } this._DownloadStatusLabel.Text = string.Format( CultureInfo.CurrentCulture, "Downloading {0} game icons...", this._LogoQueue.Count); this._DownloadStatusLabel.Visible = true; this._LogoWorker.RunWorkerAsync(info); } private void AddGameToLogoQueue(GameInfo info) { string logo = this._SteamClient.SteamApps001.GetAppData(info.Id, "logo"); if (logo == null) { return; } info.Logo = logo; int imageIndex = this._LogoImageList.Images.IndexOfKey(logo); if (imageIndex >= 0) { info.ImageIndex = imageIndex; } else if (this._LogosAttempted.Contains(logo) == false) { this._LogosAttempted.Add(logo); this._LogoQueue.Enqueue(info); } } private bool OwnsGame(uint id) { return this._SteamClient.SteamApps008.IsSubscribedApp(id); } private void AddGame(uint id, string type) { if (this._Games.ContainsKey(id)) { return; } if (this.OwnsGame(id) == false) { return; } var info = new GameInfo(id, type); info.Name = this._SteamClient.SteamApps001.GetAppData(info.Id, "name"); this._Games.Add(id, info); } private void AddGames() { this._Games.Clear(); this._RefreshGamesButton.Enabled = false; this._ListWorker.RunWorkerAsync(); } private void AddDefaultGames() { this.AddGame(480, "normal"); // Spacewar } private void OnTimer(object sender, EventArgs e) { this._CallbackTimer.Enabled = false; this._SteamClient.RunCallbacks(false); this._CallbackTimer.Enabled = true; } private void OnSelectGame(object sender, ListViewItemSelectionChangedEventArgs e) { this._SelectedGameIndex = e.ItemIndex; } private void OnActivateGame(object sender, EventArgs e) { var index = this._SelectedGameIndex; if (index < 0 || index >= this._FilteredGames.Count) { return; } var info = this._FilteredGames[index]; if (info == null) { return; } try { Process.Start("SAM.Game.exe", info.Id.ToString(CultureInfo.InvariantCulture)); } catch (Win32Exception) { MessageBox.Show( this, "Failed to start SAM.Game.exe.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void OnRefresh(object sender, EventArgs e) { this._AddGameTextBox.Text = ""; this.AddGames(); } private void OnAddGame(object sender, EventArgs e) { uint id; if (uint.TryParse(this._AddGameTextBox.Text, out id) == false) { MessageBox.Show( this, "Please enter a valid game ID.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (this.OwnsGame(id) == false) { MessageBox.Show(this, "You don't own that game.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } while (this._LogoQueue.TryDequeue(out var logo)) { // clear the download queue because we will be showing only one app // TODO: https://github.com/gibbed/SteamAchievementManager/issues/106 this._LogosAttempted.Remove(logo.Logo); } this._AddGameTextBox.Text = ""; this._Games.Clear(); this.AddGame(id, "normal"); this._FilterGamesMenuItem.Checked = true; this.RefreshGames(); this.DownloadNextLogo(); } private void OnFilterUpdate(object sender, EventArgs e) { this.RefreshGames(); } } }