Files
SteamAchievementManager/SAM.Game/Manager.cs
2019-05-24 13:48:09 -05:00

866 lines
30 KiB
C#

/* 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<Stats.AchievementInfo> _IconQueue = new List<Stats.AchievementInfo>();
private readonly List<Stats.StatDefinition> _StatDefinitions = new List<Stats.StatDefinition>();
private readonly List<Stats.AchievementDefinition> _AchievementDefinitions =
new List<Stats.AchievementDefinition>();
private readonly BindingList<Stats.StatInfo> _Statistics = new BindingList<Stats.StatInfo>();
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
private readonly API.Callbacks.UserStatsReceived _UserStatsReceivedCallback;
// ReSharper restore PrivateFieldCanBeConvertedToLocalVariable
//private API.Callback<APITypes.UserStatsStored> 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<API.Callbacks.UserStatsReceived>();
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://media.steamcommunity.com/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.SteamApps003.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<Stats.AchievementInfo>();
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;
}
}
}
}