/* Copyright (c) 2019 Rick (rick 'at' gibbed 'dot' us) * * 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.Generic; using System.ComponentModel; using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Windows.Forms; using APITypes = SAM.API.Types; namespace SAM.Game { internal partial class Manager : Form { private readonly long _GameId; private readonly API.Client _SteamClient; private readonly WebClient _IconDownloader = new WebClient(); private readonly List _IconQueue = new List(); private readonly List _StatDefinitions = new List(); private readonly List _AchievementDefinitions = new List(); private readonly BindingList _Statistics = new BindingList(); // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable private readonly API.Callbacks.UserStatsReceived _UserStatsReceivedCallback; // ReSharper restore PrivateFieldCanBeConvertedToLocalVariable //private API.Callback UserStatsStoredCallback; public Manager(long gameId, API.Client client) { this.InitializeComponent(); this._MainTabControl.SelectedTab = this._AchievementsTabPage; //this.statisticsList.Enabled = this.checkBox1.Checked; this._AchievementImageList.Images.Add("Blank", new Bitmap(64, 64)); this._StatisticsDataGridView.AutoGenerateColumns = false; this._StatisticsDataGridView.Columns.Add("name", "Name"); this._StatisticsDataGridView.Columns[0].ReadOnly = true; this._StatisticsDataGridView.Columns[0].Width = 200; this._StatisticsDataGridView.Columns[0].DataPropertyName = "DisplayName"; this._StatisticsDataGridView.Columns.Add("value", "Value"); this._StatisticsDataGridView.Columns[1].ReadOnly = this._EnableStatsEditingCheckBox.Checked == false; this._StatisticsDataGridView.Columns[1].Width = 90; this._StatisticsDataGridView.Columns[1].DataPropertyName = "Value"; this._StatisticsDataGridView.Columns.Add("extra", "Extra"); this._StatisticsDataGridView.Columns[2].ReadOnly = true; this._StatisticsDataGridView.Columns[2].Width = 200; this._StatisticsDataGridView.Columns[2].DataPropertyName = "Extra"; this._StatisticsDataGridView.DataSource = new BindingSource { DataSource = this._Statistics, }; this._GameId = gameId; this._SteamClient = client; this._IconDownloader.DownloadDataCompleted += this.OnIconDownload; string name = this._SteamClient.SteamApps001.GetAppData((uint)this._GameId, "name"); if (name != null) { base.Text += " | " + name; } else { base.Text += " | " + this._GameId.ToString(CultureInfo.InvariantCulture); } this._UserStatsReceivedCallback = client.CreateAndRegisterCallback(); this._UserStatsReceivedCallback.OnRun += this.OnUserStatsReceived; //this.UserStatsStoredCallback = new API.Callback(1102, new API.Callback.CallbackFunction(this.OnUserStatsStored)); this.RefreshStats(); } private void AddAchievementIcon(Stats.AchievementInfo info, Image icon) { if (icon == null) { info.ImageIndex = 0; } else { info.ImageIndex = this._AchievementImageList.Images.Count; this._AchievementImageList.Images.Add(info.IsAchieved == true ? info.IconNormal : info.IconLocked, icon); } } private void OnIconDownload(object sender, DownloadDataCompletedEventArgs e) { if (e.Error == null && e.Cancelled == false) { var info = e.UserState as Stats.AchievementInfo; Bitmap bitmap; try { using (var stream = new MemoryStream()) { stream.Write(e.Result, 0, e.Result.Length); bitmap = new Bitmap(stream); } } catch (Exception) { bitmap = null; } this.AddAchievementIcon(info, bitmap); this._AchievementListView.Update(); } this.DownloadNextIcon(); } private void DownloadNextIcon() { if (this._IconQueue.Count == 0) { this._DownloadStatusLabel.Visible = false; return; } if (this._IconDownloader.IsBusy == true) { return; } this._DownloadStatusLabel.Text = string.Format( CultureInfo.CurrentCulture, "Downloading {0} icons...", this._IconQueue.Count); this._DownloadStatusLabel.Visible = true; var info = this._IconQueue[0]; this._IconQueue.RemoveAt(0); this._IconDownloader.DownloadDataAsync( new Uri(string.Format( CultureInfo.InvariantCulture, "http://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/{0}/{1}", this._GameId, info.IsAchieved == true ? info.IconNormal : info.IconLocked)), info); } private static string TranslateError(int id) { switch (id) { case 2: { return "generic error -- this usually means you don't own the game"; } } return id.ToString(CultureInfo.InvariantCulture); } private static string GetLocalizedString(KeyValue kv, string language, string defaultValue) { var name = kv[language].AsString(""); if (string.IsNullOrEmpty(name) == false) { return name; } if (language != "english") { name = kv["english"].AsString(""); if (string.IsNullOrEmpty(name) == false) { return name; } } name = kv.AsString(""); if (string.IsNullOrEmpty(name) == false) { return name; } return defaultValue; } private bool LoadUserGameStatsSchema() { string path; try { path = API.Steam.GetInstallPath(); path = Path.Combine(path, "appcache"); path = Path.Combine(path, "stats"); path = Path.Combine(path, string.Format( CultureInfo.InvariantCulture, "UserGameStatsSchema_{0}.bin", this._GameId)); if (File.Exists(path) == false) { return false; } } catch { return false; } var kv = KeyValue.LoadAsBinary(path); if (kv == null) { return false; } var currentLanguage = this._SteamClient.SteamApps008.GetCurrentGameLanguage(); //var currentLanguage = "german"; this._AchievementDefinitions.Clear(); this._StatDefinitions.Clear(); var stats = kv[this._GameId.ToString(CultureInfo.InvariantCulture)]["stats"]; if (stats.Valid == false || stats.Children == null) { return false; } foreach (var stat in stats.Children) { if (stat.Valid == false) { continue; } var rawType = stat["type_int"].Valid ? stat["type_int"].AsInteger(0) : stat["type"].AsInteger(0); var type = (APITypes.UserStatType)rawType; switch (type) { case APITypes.UserStatType.Invalid: { break; } case APITypes.UserStatType.Integer: { var id = stat["name"].AsString(""); string name = GetLocalizedString(stat["display"]["name"], currentLanguage, id); this._StatDefinitions.Add(new Stats.IntegerStatDefinition() { Id = stat["name"].AsString(""), DisplayName = name, MinValue = stat["min"].AsInteger(int.MinValue), MaxValue = stat["max"].AsInteger(int.MaxValue), MaxChange = stat["maxchange"].AsInteger(0), IncrementOnly = stat["incrementonly"].AsBoolean(false), DefaultValue = stat["default"].AsInteger(0), Permission = stat["permission"].AsInteger(0), }); break; } case APITypes.UserStatType.Float: case APITypes.UserStatType.AverageRate: { var id = stat["name"].AsString(""); string name = GetLocalizedString(stat["display"]["name"], currentLanguage, id); this._StatDefinitions.Add(new Stats.FloatStatDefinition() { Id = stat["name"].AsString(""), DisplayName = name, MinValue = stat["min"].AsFloat(float.MinValue), MaxValue = stat["max"].AsFloat(float.MaxValue), MaxChange = stat["maxchange"].AsFloat(0.0f), IncrementOnly = stat["incrementonly"].AsBoolean(false), DefaultValue = stat["default"].AsFloat(0.0f), Permission = stat["permission"].AsInteger(0), }); break; } case APITypes.UserStatType.Achievements: case APITypes.UserStatType.GroupAchievements: { if (stat.Children != null) { foreach (var bits in stat.Children.Where( b => string.Compare(b.Name, "bits", StringComparison.InvariantCultureIgnoreCase) == 0)) { if (bits.Valid == false || bits.Children == null) { continue; } foreach (var bit in bits.Children) { string id = bit["name"].AsString(""); string name = GetLocalizedString(bit["display"]["name"], currentLanguage, id); string desc = GetLocalizedString(bit["display"]["desc"], currentLanguage, ""); this._AchievementDefinitions.Add(new Stats.AchievementDefinition() { Id = id, Name = name, Description = desc, IconNormal = bit["display"]["icon"].AsString(""), IconLocked = bit["display"]["icon_gray"].AsString(""), IsHidden = bit["display"]["hidden"].AsBoolean(false), Permission = bit["permission"].AsInteger(0), }); } } } break; } default: { throw new InvalidOperationException("invalid stat type"); } } } return true; } private void OnUserStatsReceived(APITypes.UserStatsReceived param) { if (param.Result != 1) { this._GameStatusLabel.Text = string.Format( CultureInfo.CurrentCulture, "Error while retrieving stats: {0}", TranslateError(param.Result)); this.EnableInput(); return; } if (this.LoadUserGameStatsSchema() == false) { this._GameStatusLabel.Text = "Failed to load schema."; this.EnableInput(); return; } try { this.GetAchievements(); this.GetStatistics(); } catch (Exception e) { this._GameStatusLabel.Text = "Error when handling stats retrieval."; this.EnableInput(); MessageBox.Show( "Error when handling stats retrieval:\n" + e, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } this._GameStatusLabel.Text = string.Format( CultureInfo.CurrentCulture, "Retrieved {0} achievements and {1} statistics.", this._AchievementListView.Items.Count, this._StatisticsDataGridView.Rows.Count); this.EnableInput(); } private void RefreshStats() { this._AchievementListView.Items.Clear(); this._StatisticsDataGridView.Rows.Clear(); if (this._SteamClient.SteamUserStats.RequestCurrentStats() == false) { MessageBox.Show(this, "Failed.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } this._GameStatusLabel.Text = "Retrieving stat information..."; this.DisableInput(); } private bool _IsUpdatingAchievementList; private void GetAchievements() { this._IsUpdatingAchievementList = true; this._AchievementListView.Items.Clear(); this._AchievementListView.BeginUpdate(); //this.Achievements.Clear(); foreach (var def in this._AchievementDefinitions) { if (string.IsNullOrEmpty(def.Id) == true) { continue; } bool isAchieved; if (this._SteamClient.SteamUserStats.GetAchievementState(def.Id, out isAchieved) == false) { continue; } var info = new Stats.AchievementInfo() { Id = def.Id, IsAchieved = isAchieved, IconNormal = string.IsNullOrEmpty(def.IconNormal) ? null : def.IconNormal, IconLocked = string.IsNullOrEmpty(def.IconLocked) ? def.IconNormal : def.IconLocked, Permission = def.Permission, Name = def.Name, Description = def.Description, }; var item = new ListViewItem() { Checked = isAchieved, Tag = info, Text = info.Name, BackColor = (def.Permission & 3) == 0 ? Color.Black : Color.FromArgb(64, 0, 0), }; info.Item = item; if (item.Text.StartsWith("#", StringComparison.InvariantCulture) == true) { item.Text = info.Id; } else { item.SubItems.Add(info.Description); } info.ImageIndex = 0; this.AddAchievementToIconQueue(info, false); this._AchievementListView.Items.Add(item); //this.Achievements.Add(info.Id, info); } this._AchievementListView.EndUpdate(); this._IsUpdatingAchievementList = false; this.DownloadNextIcon(); } private void GetStatistics() { this._Statistics.Clear(); foreach (var rdef in this._StatDefinitions) { if (string.IsNullOrEmpty(rdef.Id) == true) { continue; } if (rdef is Stats.IntegerStatDefinition) { var def = (Stats.IntegerStatDefinition)rdef; int value; if (this._SteamClient.SteamUserStats.GetStatValue(def.Id, out value)) { this._Statistics.Add(new Stats.IntStatInfo() { Id = def.Id, DisplayName = def.DisplayName, IntValue = value, OriginalValue = value, IsIncrementOnly = def.IncrementOnly, Permission = def.Permission, }); } } else if (rdef is Stats.FloatStatDefinition) { var def = (Stats.FloatStatDefinition)rdef; float value; if (this._SteamClient.SteamUserStats.GetStatValue(def.Id, out value)) { this._Statistics.Add(new Stats.FloatStatInfo() { Id = def.Id, DisplayName = def.DisplayName, FloatValue = value, OriginalValue = value, IsIncrementOnly = def.IncrementOnly, Permission = def.Permission, }); } } } } private void AddAchievementToIconQueue(Stats.AchievementInfo info, bool startDownload) { int imageIndex = this._AchievementImageList.Images.IndexOfKey( info.IsAchieved == true ? info.IconNormal : info.IconLocked); if (imageIndex >= 0) { info.ImageIndex = imageIndex; } else { this._IconQueue.Add(info); if (startDownload == true) { this.DownloadNextIcon(); } } } private int StoreAchievements() { if (this._AchievementListView.Items.Count == 0) { return 0; } var achievements = new List(); foreach (ListViewItem item in this._AchievementListView.Items) { var achievementInfo = item.Tag as Stats.AchievementInfo; if (achievementInfo != null && achievementInfo.IsAchieved != item.Checked) { achievementInfo.IsAchieved = item.Checked; achievements.Add(item.Tag as Stats.AchievementInfo); } } if (achievements.Count == 0) { return 0; } foreach (Stats.AchievementInfo info in achievements) { if (this._SteamClient.SteamUserStats.SetAchievement(info.Id, info.IsAchieved) == false) { MessageBox.Show( this, string.Format( CultureInfo.CurrentCulture, "An error occurred while setting the state for {0}, aborting store.", info.Id), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return -1; } } return achievements.Count; } private int StoreStatistics() { if (this._Statistics.Count == 0) { return 0; } var statistics = this._Statistics.Where(stat => stat.IsModified == true).ToList(); if (statistics.Count == 0) { return 0; } foreach (Stats.StatInfo stat in statistics) { if (stat is Stats.IntStatInfo) { var intStat = (Stats.IntStatInfo)stat; if (this._SteamClient.SteamUserStats.SetStatValue( intStat.Id, intStat.IntValue) == false) { MessageBox.Show( this, string.Format( CultureInfo.CurrentCulture, "An error occurred while setting the value for {0}, aborting store.", stat.Id), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return -1; } } else if (stat is Stats.FloatStatInfo) { var floatStat = (Stats.FloatStatInfo)stat; if (this._SteamClient.SteamUserStats.SetStatValue( floatStat.Id, floatStat.FloatValue) == false) { MessageBox.Show( this, string.Format( CultureInfo.CurrentCulture, "An error occurred while setting the value for {0}, aborting store.", stat.Id), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return -1; } } else { throw new InvalidOperationException("unsupported stat type"); } } return statistics.Count; } private void DisableInput() { this._ReloadButton.Enabled = false; this._StoreButton.Enabled = false; } private void EnableInput() { this._ReloadButton.Enabled = true; this._StoreButton.Enabled = true; } private void OnTimer(object sender, EventArgs e) { this._CallbackTimer.Enabled = false; this._SteamClient.RunCallbacks(false); this._CallbackTimer.Enabled = true; } private void OnRefresh(object sender, EventArgs e) { this.RefreshStats(); } private void OnLockAll(object sender, EventArgs e) { foreach (ListViewItem item in this._AchievementListView.Items) { item.Checked = false; } } private void OnInvertAll(object sender, EventArgs e) { foreach (ListViewItem item in this._AchievementListView.Items) { item.Checked = !item.Checked; } } private void OnUnlockAll(object sender, EventArgs e) { foreach (ListViewItem item in this._AchievementListView.Items) { item.Checked = true; } } private bool Store() { if (this._SteamClient.SteamUserStats.StoreStats() == false) { MessageBox.Show( this, "An error occurred while storing, aborting.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } return true; } private void OnStore(object sender, EventArgs e) { int achievements = this.StoreAchievements(); if (achievements < 0) { this.RefreshStats(); return; } int stats = this.StoreStatistics(); if (stats < 0) { this.RefreshStats(); return; } if (this.Store() == false) { this.RefreshStats(); return; } MessageBox.Show( this, string.Format( CultureInfo.CurrentCulture, "Stored {0} achievements and {1} statistics.", achievements, stats), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); this.RefreshStats(); } private void OnStatDataError(object sender, DataGridViewDataErrorEventArgs e) { if (e.Context == DataGridViewDataErrorContexts.Commit) { var view = (DataGridView)sender; if (e.Exception is Stats.StatIsProtectedException) { e.ThrowException = false; e.Cancel = true; view.Rows[e.RowIndex].ErrorText = "Stat is protected! -- you can't modify it"; } else { e.ThrowException = false; e.Cancel = true; view.Rows[e.RowIndex].ErrorText = "Invalid value"; } } } private void OnStatAgreementChecked(object sender, EventArgs e) { this._StatisticsDataGridView.Columns[1].ReadOnly = this._EnableStatsEditingCheckBox.Checked == false; } private void OnStatCellEndEdit(object sender, DataGridViewCellEventArgs e) { var view = (DataGridView)sender; view.Rows[e.RowIndex].ErrorText = ""; } private void OnResetAllStats(object sender, EventArgs e) { if (MessageBox.Show( "Are you absolutely sure you want to reset stats?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No) { return; } bool achievementsToo = DialogResult.Yes == MessageBox.Show( "Do you want to reset achievements too?", "Question", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (MessageBox.Show( "Really really sure?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Error) == DialogResult.No) { return; } if (this._SteamClient.SteamUserStats.ResetAllStats(achievementsToo) == false) { MessageBox.Show(this, "Failed.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } this.RefreshStats(); } private void OnCheckAchievement(object sender, ItemCheckEventArgs e) { if (sender != this._AchievementListView) { return; } if (this._IsUpdatingAchievementList == true) { return; } var info = this._AchievementListView.Items[e.Index].Tag as Stats.AchievementInfo; if (info == null) { return; } if ((info.Permission & 3) != 0) { MessageBox.Show( this, "Sorry, but this is a protected achievement and cannot be managed with Steam Achievement Manager.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); e.NewValue = e.CurrentValue; } } } }