Add support for adding stat modifications

Support is added for both GUI and CLI
Remove unused original_value in StatValue_t - changes
are encoded in StatChange_t.
Make pending modification more intuitive to work with
by using Change_t struct types.
Actually committing stats changes will be done in next commit.
This commit is contained in:
William Pierce 2020-04-13 02:04:30 -07:00
parent f891e25535
commit f153bdbe2c
10 changed files with 219 additions and 34 deletions

View File

@ -49,6 +49,8 @@ bool go_cli_mode(int argc, char* argv[]) {
("sort", "Sort option for --ls. You can leave empty or set to 'unlock_rate'", cxxopts::value<std::string>())
("unlock", "Unlock achievements for an AppId. Separate achievement names by a comma.", cxxopts::value<std::vector<std::string>>())
("lock", "Lock achievements for an AppId. Separate achievement names by a comma.", cxxopts::value<std::vector<std::string>>())
("statnames", "Change stats for an AppId. Separate stat names by a comma. Use with statvalues to name the values in order", cxxopts::value<std::vector<std::string>>())
("statvalues", "Change stats for an AppId. Separate stat values by a comma. Use with statnames to name the values in order", cxxopts::value<std::vector<std::string>>())
("launch", "Actually just launch the app.");
@ -58,7 +60,7 @@ bool go_cli_mode(int argc, char* argv[]) {
bool cli = false;
AppId_t app = 0;
if (result.count("help"))
if (result.count("help") > 0)
{
std::cout << options.help() << std::endl;
return true;
@ -206,6 +208,57 @@ bool go_cli_mode(int argc, char* argv[]) {
g_steam->quit_game();
}
if (result.count("statnames") > 0 || result.count("statvalues") > 0)
{
if (app == 0)
{
std::cout << "Please provide an AppId argument before changing stats." << std::endl;
return true;
}
if (result.count("statnames") != result.count("statvalues"))
{
std::cout << "Number of statnames does not equal number of statvalues." << std::endl;
return true;
}
cli = true;
size_t num_stats = result.count("statnames");
const std::vector<std::string> stat_names = result["statnames"].as<std::vector<std::string>>();
const std::vector<std::string> stat_values = result["statvalues"].as<std::vector<std::string>>();
for (size_t i = 0; i < num_stats; i++) {
// We don't know the type at this point, so try both of them
StatValue_t stat;
bool valid_conversion;
std::any new_value;
valid_conversion = convert_user_stat_value(UserStatType::Integer, stat_values[i], &new_value);
if (valid_conversion) {
stat.type = UserStatType::Integer;
} else {
valid_conversion = convert_user_stat_value(UserStatType::Float, stat_values[i], &new_value);
if (valid_conversion) {
stat.type = UserStatType::Float;
}
}
if (!valid_conversion) {
g_steam->clear_changes();
return true;
}
// No validation done for the name
stat.id = stat_names[i];
g_steam->add_modification_stat(stat, new_value);
}
g_steam->launch_app(app);
g_steam->commit_changes();
g_steam->quit_game();
}
if (result.count("launch") > 0)
{
if (app == 0)

View File

@ -113,4 +113,34 @@ void escape_html(std::string& data) {
int zenity(const std::string text, const std::string type) {
return system( std::string("zenity " + type + " --text=\"" + text + "\" 2> /dev/null").c_str() );
}
bool convert_user_stat_value(UserStatType type, std::string buf, std::any* new_value) {
bool valid_conversion;
size_t idx = 0;
try {
// Cast these in their native steam implementation so
// we don't truncate any user input precision
//
// We could also plumb in input validation for the
// min/max/incremental values here
if (type == UserStatType::Integer) {
*new_value = std::stoi(buf, &idx);
} else if (type == UserStatType::Float) {
*new_value = std::stof(buf, &idx);
} else {
valid_conversion = false;
}
} catch(std::exception& e) {
// Invalid user input, but this is not fatal to the program
valid_conversion = false;
}
// If the whole string hasn't been consumed, treat it as invalid
if (buf[idx] != '\0') {
valid_conversion = false;
}
return valid_conversion;
}

View File

@ -1,8 +1,10 @@
#pragma once
#include <string>
#include <any>
#include "../globals.h"
#include "../../steam/steam_api.h"
#include "../types/UserStatType.h"
/**
* Stats hold a value of type any, but can be either int or float.
@ -69,6 +71,13 @@ void escape_html(std::string& data);
/**
* Show a regular dialog box. Return value is ignored for now,
* but feel free to add functionnlitie to this
* but feel free to add functionnlity to this
*/
int zenity(const std::string text = "An internal error occurred, please open a Github issue with the console output to get it fixed!", const std::string type = "--error --no-wrap");
int zenity(const std::string text = "An internal error occurred, please open a Github issue with the console output to get it fixed!", const std::string type = "--error --no-wrap");
/**
* Convert a user stat value string buffer to the specified stat type
* Directly modifies new_value
* Returns whether the conversion was successful
*/
bool convert_user_stat_value(UserStatType type, std::string buf, std::any* new_value);

View File

@ -197,10 +197,10 @@ MySteam::refresh_achievements_and_stats() {
* Adds an achievement to the list of achievements to unlock/lock
*/
void
MySteam::add_modification_ach(const std::string& ach_id, const bool& new_value) {
std::cout << "Adding modification: " << ach_id << ", " << (new_value ? "to unlock" : "to relock") << std::endl;
MySteam::add_modification_ach(const std::string& ach_id, bool new_value) {
std::cout << "Adding achievement modification: " << ach_id << ", " << (new_value ? "to unlock" : "to relock") << std::endl;
if ( m_pending_ach_modifications.find(ach_id) == m_pending_ach_modifications.end() ) {
m_pending_ach_modifications.insert( std::pair<std::string, bool>(ach_id, new_value) );
m_pending_ach_modifications.insert( std::pair<std::string, AchievementChange_t>(ach_id, (AchievementChange_t){ach_id, new_value} ) );
} else {
std::cerr << "Warning: Cannot append " << ach_id << ", value already exists." << std::endl;
}
@ -212,7 +212,7 @@ MySteam::add_modification_ach(const std::string& ach_id, const bool& new_value)
*/
void
MySteam::remove_modification_ach(const std::string& ach_id) {
std::cout << "Removing modification: " << ach_id << std::endl;
std::cout << "Removing achievement modification: " << ach_id << std::endl;
if ( m_pending_ach_modifications.find(ach_id) == m_pending_ach_modifications.end() ) {
std::cerr << "WARNING: Could not cancel: modification was not pending: " << ach_id << std::endl;
} else {
@ -221,6 +221,45 @@ MySteam::remove_modification_ach(const std::string& ach_id) {
}
// => remove_modification_ach
/**
* Adds a stat modification to be done on the launched app.
* Commit the change with commit_changes
*/
void
MySteam::add_modification_stat(const StatValue_t& stat, std::any new_value) {
// The value must already be the proper type for it to be added to the list
std::cout << "Adding stat modification: " << stat.id << ", ";
if (stat.type == UserStatType::Integer) {
std::cout << "Integer " << std::to_string(std::any_cast<long long>(new_value));
} else if (stat.type == UserStatType::Float) {
std::cout << "Float " << std::to_string(std::any_cast<double>(new_value));
} else {
// Input has already been checked in StatBoxRow
exit(EXIT_FAILURE);
}
std::cout << std::endl;
if ( m_pending_stat_modifications.find(stat.id) == m_pending_stat_modifications.end() ) {
m_pending_stat_modifications.insert( std::pair<std::string, StatChange_t>(stat.id, (StatChange_t){stat.type, stat.id, new_value} ) );
} else {
std::cerr << "Warning: Cannot append " << stat.id << ", value already exists." << std::endl;
}
}
// => add_modification_stat
/**
* Removes a stat modification that would have been done on the launched app.
*/
void
MySteam::remove_modification_stat(const StatValue_t& stat) {
std::cout << "Removing stat modification: " << stat.id << std::endl;
// If there's not one pending, don't treat it as a warning currently
// because we don't really care to differentiate the
// 0->1 and 2->1 character length transitions over in StatBoxRow
m_pending_stat_modifications.erase(stat.id);
}
// => remove_modification_stat
/**
* Commit pending achievement changes
*/
@ -229,9 +268,10 @@ MySteam::commit_changes() {
std::vector<AchievementChange_t> changes;
for ( const auto& [key, val] : m_pending_ach_modifications) {
std::cerr << "key " << key << "val " << val << std::endl;
changes.push_back( (AchievementChange_t){ key, val } );
changes.push_back(val);
}
//TODO: pending_stat_modifications
std::string response = m_ipc_socket->request_response(make_store_achivements_request_string(changes));
@ -241,6 +281,7 @@ MySteam::commit_changes() {
// Clear all pending changes
m_pending_ach_modifications.clear();
m_pending_stat_modifications.clear();
}
// => commit_changes
@ -248,6 +289,7 @@ void
MySteam::clear_changes() {
// Clear all pending changes
m_pending_ach_modifications.clear();
m_pending_stat_modifications.clear();
}
// => clear_changes

View File

@ -116,20 +116,26 @@ public:
AppId_t get_current_appid() const { return m_app_id; };
/**
* Adds a modification to be done on the launched app.
* Adds an achievement modification to be done on the launched app.
* Commit the change with commit_changes
*/
void add_modification_ach(const std::string& ach_id, const bool& new_value);
void add_modification_ach(const std::string& ach_id, bool new_value);
/**
* Adds a modification to be done on the launched app.
* Removes an achievement modificatiothat would have been done on the launched app.
*/
void remove_modification_ach(const std::string& ach_id);
/**
* Adds a modification to be done on the launched app.
* Commit the change with commit_modifications.
* Adds a stat modification to be done on the launched app.
* Commit the change with commit_changes
*/
//void add_modification_stat(const std::string& stat_id, const double& new_value); // TODO: IMPLEMENT
void add_modification_stat(const StatValue_t& stat, std::any new_value);
/**
* Removes a stat modification that would have been done on the launched app.
*/
void remove_modification_stat(const StatValue_t& stat);
/**
* Commit pending changes
@ -171,6 +177,6 @@ private:
std::vector<Achievement_t> m_achievements;
std::vector<StatValue_t> m_stats;
std::map<std::string, bool> m_pending_ach_modifications;
std::map<std::string, double> m_pending_stat_modifications;
std::map<std::string, AchievementChange_t> m_pending_ach_modifications;
std::map<std::string, StatChange_t> m_pending_stat_modifications;
};

View File

@ -1,32 +1,35 @@
#include "StatBoxRow.h"
#include "../controller/MySteam.h"
#include "../globals.h"
#include "../common/functions.h"
#include <string>
#include <gtkmm-3.0/gtkmm/box.h>
#include <gtkmm-3.0/gtkmm/label.h>
#include <gtkmm-3.0/gtkmm/entry.h>
StatBoxRow::StatBoxRow(const StatValue_t& data)
: m_data(data)
{
std::string stat_title_text, escaped_name;
escaped_name = data.display_name;
escape_html(escaped_name);
stat_title_text = "<b>" + escaped_name + "</b>";
Gtk::Box* layout = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::ORIENTATION_HORIZONTAL, 0);
Gtk::Box* title_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::ORIENTATION_VERTICAL, 0);
Gtk::Box* type_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::ORIENTATION_VERTICAL, 0);
Gtk::Box* values_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::ORIENTATION_VERTICAL, 0);
Gtk::Box* new_values_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::ORIENTATION_HORIZONTAL, 0);
Gtk::Label* title_label = Gtk::make_managed<Gtk::Label>("");
Gtk::Label* type_label = Gtk::make_managed<Gtk::Label>("");
Gtk::Label* cur_value_label = Gtk::make_managed<Gtk::Label>("");
// TODO: This will be an input box
Gtk::Label* new_value_label = Gtk::make_managed<Gtk::Label>("");
set_size_request(-1, 40);
type_box->set_size_request(150, -1);
values_box->set_size_request(150, -1);
m_new_value_entry.set_width_chars(10);
m_new_value_entry.set_max_width_chars(10);
// Bound this to some reasonable value
m_new_value_entry.set_max_length(100);
if (data.type == UserStatType::Integer) {
type_label->set_label("Type: Integer");
// TODO: may want to bound the size of this
@ -40,18 +43,46 @@ StatBoxRow::StatBoxRow(const StatValue_t& data)
cur_value_label->set_label("Current value: Unknown");
}
new_value_label->set_label("Placeholder");
new_value_label->set_label("New value: ");
title_label->set_markup(stat_title_text);
title_box->pack_start(*title_label, true, true, 0);
// Left align labels
cur_value_label->set_xalign(0);
new_value_label->set_xalign(0);
title_label->set_label(data.display_name);
title_box->pack_start(*title_label, false, true, 0);
type_box->pack_start(*type_label, false, true, 0);
new_values_box->pack_start(*new_value_label, true, true, 0);
new_values_box->pack_start(m_new_value_entry, true, true, 0);
values_box->pack_start(*cur_value_label, false, true, 0);
values_box->pack_start(*new_value_label, false, true, 0);
values_box->pack_start(*new_values_box, false, true, 0);
layout->pack_start(*title_box, true, true, 0);
layout->pack_start(*type_box, false, true, 0);
layout->pack_start(*values_box, false, true, 0);
add(*layout);
m_new_value_entry.signal_changed().connect(sigc::mem_fun(this, &StatBoxRow::on_new_value_changed));
}
StatBoxRow::~StatBoxRow() {}
StatBoxRow::~StatBoxRow() {}
void
StatBoxRow::on_new_value_changed(void) {
// Note this is not run after a short delay of the text not being changed
// so maybe we don't want to edit the list every single time a button is pressed.
// The alternative is to read through all stat boxes when commit changes is
// pressed.
bool valid_conversion;
std::any new_value;
const std::string& buf = m_new_value_entry.get_text();
valid_conversion = convert_user_stat_value(m_data.type, buf, &new_value);
// Remove pending modifications with different values, if any
g_steam->remove_modification_stat(m_data);
if (valid_conversion) {
g_steam->add_modification_stat(m_data, new_value);
}
}

View File

@ -4,6 +4,7 @@
#include <string>
#include <gtkmm-3.0/gtkmm/listboxrow.h>
#include <gtkmm-3.0/gtkmm/entry.h>
/**
* This class represents a stat entry on the stats view
@ -14,7 +15,12 @@ public:
StatBoxRow(const StatValue_t& stat);
virtual ~StatBoxRow();
/**
* Interpret a change in the text field
*/
void on_new_value_changed(void);
private:
StatValue_t m_data;
// more stuff here
Gtk::Entry m_new_value_entry;
};

View File

@ -203,7 +203,6 @@ MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
sv.display_name = cast->DisplayName;
sv.id = cast->Id;
sv.incrementonly = cast->IncrementOnly;
sv.original_value = value;
sv.value = value;
sv.permission = cast->Permission;
@ -230,7 +229,6 @@ MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
sv.display_name = cast->DisplayName;
sv.id = cast->Id;
sv.incrementonly = cast->IncrementOnly;
sv.original_value = value;
sv.value = value;
sv.permission = cast->Permission;

View File

@ -44,7 +44,6 @@ typedef struct Achievement_t Achievement_t;
* AchievementChange structure
* Minimum information needed to change an
* achievement
* TODO add stats
*/
struct AchievementChange_t {
std::string id;

View File

@ -10,9 +10,20 @@ struct StatValue_t {
std::string id;
std::string display_name;
std::any value;
std::any original_value;
bool incrementonly;
int permission;
};
typedef struct StatValue_t StatValue_t;
/**
* StatChange structure
* Minimum information needed to change a stat
*/
struct StatChange_t {
UserStatType type;
std::string id;
std::any new_value;
};
typedef struct StatChange_t StatChange_t;