Merge pull request #51 from PaulCombal/feat-statsmodel

Feat statsmodel
This commit is contained in:
PaulCombal 2020-02-20 09:22:45 +01:00 committed by GitHub
commit 5988e0dd3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 787 additions and 197 deletions

3
.vscode/launch.json vendored
View File

@ -2,6 +2,7 @@
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
// iBomber pacific 206690
"version": "0.2.0",
"configurations": [
{
@ -9,7 +10,7 @@
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/samrewritten",
"args": [],
"args": ["730", "--ls"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [

View File

@ -15,7 +15,7 @@ all: ${CURDIR}/bin/samrewritten
@echo -e "==== Use '\033[1mmake clean\033[0m' to remove object files"
@echo -e "==== Nothing left to do."
dev: CXXFLAGS += -g
dev: CXXFLAGS += -g -DDEBUG_CERR
dev: ${CURDIR}/bin/samrewritten
clean:

View File

@ -60,7 +60,7 @@ Usage:
-a, --app arg Set which AppId you want to use. Same as using positional
'AppId'
-i, --idle Set your Steam profile as 'ingame'. Ctrl+c to stop.
--ls Display stats (TODO) and achievements for selected app.
--ls Display achievements and stats for selected app.
--sort arg Sort option for --ls. You can leave empty or set to
'unlock_rate'
--unlock arg Unlock achievements for an AppId. Separate achievement

4
TODO Normal file
View File

@ -0,0 +1,4 @@
-- done? I finished the stat encoding, but did not test. ALso, now do the decoding
Next task: try printing stats in the client side. I just finished decode_stats.

View File

@ -2,6 +2,7 @@
#include "../controller/MySteam.h"
#include "../globals.h"
#include "../common/cxxopts.hpp"
#include "../common/functions.h"
#include <string>
#include <iostream>
@ -44,7 +45,7 @@ bool go_cli_mode(int argc, char* argv[]) {
("h,help", "Show CLI help.")
("a,app", "Set which AppId you want to use. Same as using positional 'AppId'", cxxopts::value<AppId_t>())
("i,idle", "Set your Steam profile as 'ingame'. Ctrl+c to stop.")
("ls", "Display stats (TODO) and achievements for selected app.")
("ls", "Display achievements and stats for selected app.")
("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>>())
@ -104,8 +105,9 @@ bool go_cli_mode(int argc, char* argv[]) {
}
g_steam->launch_app(app);
g_steam->refresh_achievements();
g_steam->refresh_stats_and_achievements();
auto achievements = g_steam->get_achievements();
auto stats = g_steam->get_stats();
g_steam->quit_game();
if (result.count("sort") > 0)
@ -121,7 +123,7 @@ bool go_cli_mode(int argc, char* argv[]) {
// https://github.com/haarcuba/cpp-text-table -> worth? nah but best I've found
std::cout << "API Name \t\tName \t\tDescription \t\tUnlock rate \t\tUnlocked" << std::endl;
std::cout << "API Name \t\tName \t\tDescription \t\tUnlock rate \t\tUnlocked\n";
std::cout << "--------------------------------------------------------------" << std::endl;
for ( Achievement_t& it : achievements )
{
@ -132,6 +134,28 @@ bool go_cli_mode(int argc, char* argv[]) {
<< it.global_achieved_rate << "% \t"
<< (it.achieved ? "✔️" : "") << std::endl;
}
std::cout << "\n";
if ( stats.size() == 0 )
{
std::cout << "No stats found for this app.." << std::endl;
}
else
{
std::cout << "\nSTATS\n";
std::cout << "API Name \t\tValue \t\t Increment Only\n";
std::cout << "----------------------------------------" << std::endl;
for (auto stat : stats )
{
std::cout
<< stat.id << " \t"
<< GET_STAT_VALUE(stat) << " \t"
<< (stat.incrementonly ? "Yes" : "No")
<< std::endl;
}
}
}
if (result.count("unlock") > 0)

View File

@ -23,6 +23,15 @@ Downloader::download_file(const std::string& file_url, const std::string& local_
curl = curl_easy_init();
if (curl) {
fp = fopen(local_path.c_str(),"wb");
if (fp == NULL)
{
std::cerr << "An error occurred downloading " << file_url << " and saving it to ";
std::cerr << local_path << std::endl;
zenity("An error occurred downloading a file. Please report it to the developers!");
exit(EXIT_FAILURE);
}
curl_easy_setopt(curl, CURLOPT_URL, file_url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);

View File

@ -16,9 +16,11 @@ PerfMon::~PerfMon()
void
PerfMon::log(const std::string& what)
{
#ifdef DEBUG_CERR
std::cerr
<< "[PID:" << getpid()
<< " TRD:" << std::this_thread::get_id()
<< " TME:" << double(std::clock() - m_start) / CLOCKS_PER_SEC
<< "] \t" << what << std::endl;
#endif
}

View File

@ -1096,7 +1096,6 @@ namespace cxxopts
return m_count;
}
// TODO: maybe default options should count towards the number of arguments
bool
has_default() const noexcept
{

View File

@ -87,12 +87,12 @@ void mkdir_default(const char *pathname)
}
}
std::string get_app_icon_path(AppId_t app_id) {
return std::string(g_cache_folder) + "/" + std::to_string(app_id) + "/banner.jpg";
std::string get_app_icon_path(std::string cache_folder, AppId_t app_id) {
return cache_folder + "/" + std::to_string(app_id) + "/banner.jpg";
}
std::string get_achievement_icon_path(AppId_t app_id, std::string id) {
return std::string(g_cache_folder) + "/" + std::to_string(app_id) + "/" + id + ".jpg";
std::string get_achievement_icon_path(std::string cache_folder, AppId_t app_id, std::string id) {
return cache_folder + "/" + std::to_string(app_id) + "/" + id + ".jpg";
}
void escape_html(std::string& data) {

View File

@ -4,6 +4,13 @@
#include "../globals.h"
#include "../../steam/steam_api.h"
/**
* Stats hold a value of type any, but can be either int or float.
* This macro casts the value to the right type.
* The cast types are the ones used by YAJL
*/
#define GET_STAT_VALUE(stat) (stat.type == UserStatType::Integer ? std::any_cast<long long>(stat.value) : std::any_cast<double>(stat.value))
/**
* Wrapper for fork()
*/
@ -47,12 +54,12 @@ void mkdir_default(const char *pathname);
/**
* Generate path to given app icon
*/
std::string get_app_icon_path(AppId_t app_id);
std::string get_app_icon_path(std::string cache_folder, AppId_t app_id);
/**
* Generate path to given app achievement icon
*/
std::string get_achievement_icon_path(AppId_t app_id, std::string id);
std::string get_achievement_icon_path(std::string cache_folder, AppId_t app_id, std::string id);
/**
* Escape html characters inline a string.
@ -64,4 +71,4 @@ 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
*/
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");

View File

@ -12,6 +12,24 @@
#include <bits/stdc++.h>
MySteam::MySteam() {
// Cache folder
if (getenv("XDG_CACHE_HOME")) {
m_cache_folder = std::string(getenv("XDG_CACHE_HOME")) + "/SamRewritten";
} else {
m_cache_folder = std::string(getenv("HOME")) + "/.cache/SamRewritten";
}
mkdir_default(m_cache_folder.c_str());
// Runtime folder
if (getenv("XDG_RUNTIME_DIR")) {
m_runtime_folder = std::string(getenv("XDG_RUNTIME_DIR")) + "/SamRewritten";
mkdir_default(m_runtime_folder.c_str());
} else {
std::cerr << "XDG_RUNTIME_DIR is not set! Your distribution is improper... falling back to cache dir" << std::endl;
m_runtime_folder = m_cache_folder;
}
// Steam folder
std::string data_home_path;
if (getenv("XDG_DATA_HOME") != NULL) {
data_home_path = getenv("XDG_DATA_HOME");
@ -74,14 +92,16 @@ MySteam::launch_app(AppId_t appID) {
return false;
}
// Set the appID BEFORE forking, otherwise the child won't be able to access it
m_app_id = appID;
m_ipc_socket = m_server_manager.quick_server_create(appID);
if (m_ipc_socket == nullptr) {
std::cerr << "Failed to get connection to game" << std::endl;
m_app_id = 0;
return false;
}
m_app_id = appID;
return true;
}
// => launch_app
@ -134,16 +154,6 @@ MySteam::refresh_owned_apps() {
}
// => refresh_owned_apps
/**
* Tries to locate the steam folder in multiple locations,
* which is not a failsafe implementation.
*/
std::string
MySteam::get_steam_install_path() {
return m_steam_install_dir;
}
// => get_steam_install_path
/**
* Reminder that download_app_icon does check if the file is
* already there before attempting to download from the web.
@ -157,13 +167,12 @@ MySteam::refresh_app_icon(AppId_t app_id) {
void
MySteam::refresh_achievement_icon(std::string id, std::string icon_download_name) {
SteamAppDAO *appDAO = SteamAppDAO::get_instance();
appDAO->download_achievement_icon(m_app_id, id, icon_download_name);
SteamAppDAO::get_instance()->download_achievement_icon(m_app_id, id, icon_download_name);
}
// => refresh_achievement_icon
void
MySteam::refresh_achievements() {
MySteam::refresh_stats_and_achievements() {
if (m_ipc_socket == nullptr) {
std::cerr << "Connection to game is broken" << std::endl;
@ -178,6 +187,7 @@ MySteam::refresh_achievements() {
}
m_achievements = decode_achievements(response);
m_stats = decode_stats(response);
set_special_flags();
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "../types/Game.h"
#include "../types/Achievement.h"
#include "../types/StatValue.h"
#include "../sockets/MyClientSocket.h"
#include "GameServerManager.h"
#include <string>
@ -24,7 +25,31 @@ public:
* This is not failsafe and may require some tweaking to add
* support for your distribution
*/
std::string get_steam_install_path();
std::string get_steam_install_path() const { return m_steam_install_dir; };
/**
* Returns the absolute path to the runtime folder used by SAM.
* It's used for storing the UNIX sockets used by the program.
*
* XDG Base Directory Specification is followed:
* If present, this variable uses XDG_RUNTIME_DIR and takes the value
* $XDG_RUNTIME_DIR/SamRewritten
*
* Warnings are properly issued if it is not set, but still defaults to the
* cache folder for simplicity
*/
std::string get_runtime_path() const { return m_runtime_folder; };
/**
* Returns the absolute path to the cache folder used by SAM
*
* XDG Base Directory Specification is followed:
* If present, this variable uses XDG_CACHE_HOME and takes the value
* $XDG_CACHE_HOME/SamRewritten
* Otherwise, this variable properly defaults to
* ~/.cache/SamRewritten
*/
std::string get_cache_path() const { return m_cache_folder; };
/**
* Starts a process that will emulate a steam game with the
@ -67,7 +92,7 @@ public:
/**
* Makes a list of all achievements for the currently running app
*/
void refresh_achievements();
void refresh_stats_and_achievements();
/**
* Get achievements of the launched app
@ -78,9 +103,12 @@ public:
* Make sure to call refresh_achievements at least once to get
* correct results
*
* The same goes for the stats
*
* TODO: maybe don't name this the same as GameServer::get_achievements?
*/
std::vector<Achievement_t> get_achievements() const { return m_achievements; };
std::vector<StatValue_t> get_stats() const { return m_stats; };
/**
* Simple getter
@ -128,8 +156,10 @@ private:
*/
void set_special_flags();
// Absolute path to Steam install dir
// Absolute path to some important directories
std::string m_steam_install_dir;
std::string m_cache_folder;
std::string m_runtime_folder;
// Current app_id
AppId_t m_app_id;
@ -139,6 +169,7 @@ private:
std::vector<Game_t> m_all_subscribed_apps;
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;

View File

@ -29,14 +29,14 @@ SteamAppDAO::get_instance() {
// => get_instance
bool
SteamAppDAO::need_to_redownload(const char * file_path) {
SteamAppDAO::need_to_redownload(const std::string file_path) {
struct stat file_info;
const std::time_t current_time(std::time(0));
bool b_need_to_redownload = false;
if (file_exists(file_path)) {
//Check the last time it was updated
if (stat(file_path, &file_info) == 0) {
if (stat(file_path.c_str(), &file_info) == 0) {
//If a week has passed
if (current_time - file_info.st_mtime > 60 * 60 * 24 * 7) {
b_need_to_redownload = true;
@ -46,7 +46,7 @@ SteamAppDAO::need_to_redownload(const char * file_path) {
std::cerr << "~/.cache/SamRewritten/app_names exists but an error occurred analyzing it. To avoid further complications, ";
std::cerr << "the program will stop here. Before retrying make sure you have enough privilege to read and write to ";
std::cerr << "your home folder folder." << std::endl;
zenity("An error occurred writing the cache files. Try deleting the cache folder (" + std::string(g_cache_folder) + ") and make sure you have enough permissions to write to it.");
zenity("An error occurred writing the cache files. Try deleting the cache folder (" + g_steam->get_cache_path() + ") and make sure you have enough permissions to write to it.");
exit(EXIT_FAILURE);
}
}
@ -75,24 +75,25 @@ SteamAppDAO::update_name_database() {
// TODO: turn this into a command line option, or a setting
const int retrieval_strategy = 1;
static const char* file_url[2];
static const char* local_file_name[2];
static std::string file_url[2];
static std::string local_file_name[2];
const std::string cache_folder = g_steam->get_cache_path();
int file_count = 0;
if (retrieval_strategy == 1) {
file_url[0] = "https://raw.githubusercontent.com/PaulCombal/SteamAppsListDumps/master/game_achievements_list.json";
local_file_name[0] = concat(g_cache_folder, "/game_list.json");
local_file_name[0] = cache_folder + "/game_list.json";
file_count = 1;
} else if (retrieval_strategy == 2) {
// need to download 2 files for this one
file_url[0] = "http://api.steampowered.com/ISteamApps/GetAppList/v0002/";
local_file_name[0] = concat(g_cache_folder, "/app_names");
local_file_name[0] = cache_folder + "/app_names";
file_url[1] = "https://raw.githubusercontent.com/PaulCombal/SteamAppsListDumps/master/not_games.json";
local_file_name[1] = concat(g_cache_folder, "/not_games.json");
local_file_name[1] = cache_folder + "/not_games.json";
file_count = 2;
} else if (retrieval_strategy == 3) {
file_url[0] = "http://api.steampowered.com/ISteamApps/GetAppList/v0002/";
local_file_name[0] = concat(g_cache_folder, "/app_names");
local_file_name[0] = cache_folder + "/app_names";
file_count = 1;
}
@ -132,8 +133,9 @@ SteamAppDAO::get_app_name(AppId_t app_id) {
void
SteamAppDAO::download_app_icon(AppId_t app_id) {
const std::string local_folder(std::string(g_cache_folder) + "/" + std::to_string(app_id));
const std::string local_path = get_app_icon_path(app_id);
const std::string cache_folder = g_steam->get_cache_path();
const std::string local_folder(cache_folder + "/" + std::to_string(app_id));
const std::string local_path = get_app_icon_path(cache_folder, app_id);
const std::string url("http://cdn.akamai.steamstatic.com/steam/apps/" + std::to_string(app_id) + "/header_292x136.jpg");
mkdir_default(local_folder.c_str());
@ -142,43 +144,49 @@ SteamAppDAO::download_app_icon(AppId_t app_id) {
void
SteamAppDAO::download_achievement_icon(AppId_t app_id, std::string id, std::string icon_download_name) {
const std::string local_folder(std::string(g_cache_folder) + "/" + std::to_string(app_id));
const std::string local_path = get_achievement_icon_path(app_id, id);
const std::string cache_folder = g_steam->get_cache_path();
const std::string local_folder(cache_folder + "/" + std::to_string(app_id));
const std::string local_path = get_achievement_icon_path(cache_folder, app_id, id);
const std::string url("http://media.steamcommunity.com/steamcommunity/public/images/apps/" + std::to_string(app_id) + "/" + icon_download_name);
#ifdef DEBUG_CERR
std::cerr << "Asking DAO to download achievement icon " << id << " from " << url << std::endl;
#endif
mkdir_default(local_folder.c_str());
Downloader::get_instance()->download_file(url, local_path);
}
void
SteamAppDAO::parse_app_names(const char * file_path, std::map<AppId_t, std::string>* app_names) {
SteamAppDAO::parse_app_names(const std::string file_path, std::map<AppId_t, std::string>* app_names) {
app_names->clear();
size_t rd;
yajl_val node;
char errbuf[1024];
char fileData[5000000];
FILE *f = fopen(file_path, "rb");
std::string file_contents;
std::ifstream stream(file_path);
/* null plug buffers */
fileData[0] = errbuf[0] = 0;
/* read the entire config file */
rd = fread((void *) fileData, 1, sizeof(fileData) - 1, f);
/* file read error handling */
if (rd == 0 && !feof(stdin)) {
std::cerr << "Error encountered on file read: " << file_path << std::endl;
zenity();
exit(EXIT_FAILURE);
} else if (rd >= sizeof(fileData) - 1) {
std::cerr << "app_names file too big (just increase the buffer size)" << std::endl;
if ( !stream.is_open() ) {
std::cerr << "SteamAppDAO::parse_app_names: Unable to open file " << file_path << std::endl;
zenity();
exit(EXIT_FAILURE);
}
try {
std::stringstream buffer;
buffer << stream.rdbuf();
file_contents = buffer.str();
} catch(std::exception& e) {
std::cerr << "SteamAppDAO::parse_app_names: " << e.what() << std::endl;
zenity();
exit(EXIT_FAILURE);
}
// Stream is RAII, but we close it here since we won't use it after
stream.close();
/* we have the whole config file in memory. let's parse it ... */
node = yajl_tree_parse((const char *) fileData, errbuf, sizeof(errbuf));
node = yajl_tree_parse(file_contents.c_str(), errbuf, sizeof(errbuf));
/* parse error handling */
if (node == NULL) {
@ -213,7 +221,6 @@ SteamAppDAO::parse_app_names(const char * file_path, std::map<AppId_t, std::stri
app_names->insert(std::pair<AppId_t, std::string>(tmp_appid, tmp_appname));
}
fclose(f);
yajl_tree_free(node);
}

View File

@ -58,11 +58,11 @@ private:
/**
* Check if a given file exists and if so, if it's too old
*/
static bool need_to_redownload(const char * file_path);
static bool need_to_redownload(const std::string file_path);
/**
* Parse the app names from a given file and store to a give app names map
*/
static void parse_app_names(const char * file_path, std::map<AppId_t, std::string>* app_names);
static void parse_app_names(const std::string file_path, std::map<AppId_t, std::string>* app_names);
std::map<AppId_t, std::string> m_app_names;
};

View File

@ -5,7 +5,8 @@ class MainPickerWindow;
class MySteamClient;
class PerfMon;
// Reason for the globals is that it's easier to access them in GTK callbacks
// Reason for the globals is that it's easier to access them in GTK callbacks,
// or the data they hold may be needed both by the parent process and its children.
// All those variables are initialised in main.cpp
/**
@ -16,42 +17,17 @@ class PerfMon;
extern MySteamClient *g_steamclient;
/**
* g_steam is the backend of all of this, the M of the MVC. It will analyse the
* steam folder and give data about it: games owned, icons, etc.
* g_steam is the backend of all of this. This is used to retrieve achievements,
* stats, achievement icons, etc.
*/
extern MySteam *g_steam;
/**
* g_main_gui is the main application window. If you can read this, it is very
* glitchy, freezes instead of showing the loading widget, but still looks
* half-decent. It's the V of the MVC.
* g_main_gui is the main application window.
*/
extern MainPickerWindow *g_main_gui;
/**
* The absolute path to the cache folder. It's global, because everyone will
* need to write or read into this folder at some point.
* XDG Base Directory Specification is followed:
* If present, this variable uses XDG_CACHE_HOME and takes the value
* $XDG_CACHE_HOME/SamRewritten
* Otherwise, this variable properly defaults to
* ~/.cache/SamRewritten
* A basic performance monitor. Used to log stuff along with process info.
*/
extern char *g_cache_folder;
/**
* The absolute path to the runtime folder. It's global, because everyone will
* need to write or read into this folder at some point. It's used for storing
* sockets used by the program.
* XDG Base Directory Specification is followed:
* If present, this variable uses XDG_RUNTIME_DIR and takes the value
* $XDG_RUNTIME_DIR/SamRewritten
* Warnings are properly issued if it is not set, but still defaults to the
* cache folder for simplicity
*/
extern char *g_runtime_folder;
/**
* A basic performance monitor
*/
extern PerfMon *g_perfmon;
extern PerfMon *g_perfmon;

View File

@ -9,8 +9,9 @@
#include <future>
#include <glibmm-2.4/glibmm.h>
// TODO use #ifndef __VALGRIND_H or similar
// #include <valgrind/valgrind.h>
#ifdef DEBUG_CERR
#include <valgrind/valgrind.h>
#endif
AsyncGuiLoader::AsyncGuiLoader(MainPickerWindow* window)
: m_window(window)
@ -23,7 +24,7 @@ AsyncGuiLoader::load_achievements_idle()
{
if (m_achievement_idle_data.state == ACH_STATE_STARTED) {
g_perfmon->log("Starting achievement retrieval");
m_achievements_future = std::async(std::launch::async, []{g_steam->refresh_achievements();});
m_achievements_future = std::async(std::launch::async, []{g_steam->refresh_stats_and_achievements();});
m_achievement_idle_data.state = ACH_STATE_WAITING_FOR_ACHIEVEMENTS;
return G_SOURCE_CONTINUE;
}
@ -32,9 +33,14 @@ AsyncGuiLoader::load_achievements_idle()
if (m_achievements_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
g_perfmon->log("Done retrieving achievements");
#if DEBUG_CERR
std::cerr << "Printing achievement ID & icon:" << std::endl;
for (auto a : g_steam->get_achievements()) {
std::cerr << a.id << " " << a.icon_name << std::endl;
}
#endif
// Fire off the schema parsing now.
// TODO: figure out if all the icons are already there and skip parsing schema
m_schema_parser_future = std::async(std::launch::async, [this]{return m_schema_parser.load_user_game_stats_schema();});
m_achievement_idle_data.state = ACH_STATE_LOADING_GUI;
}
return G_SOURCE_CONTINUE;
@ -56,7 +62,7 @@ AsyncGuiLoader::load_achievements_idle()
return G_SOURCE_REMOVE;
}
m_achievement_idle_data.state = ACH_STATE_WAITING_FOR_SCHEMA_PARSER;
m_achievement_idle_data.state = ACH_STATE_DOWNLOADING_ICONS;
m_achievement_idle_data.current_item = 0;
return G_SOURCE_CONTINUE;
}
@ -67,23 +73,6 @@ AsyncGuiLoader::load_achievements_idle()
return G_SOURCE_CONTINUE;
}
if (m_achievement_idle_data.state == ACH_STATE_WAITING_FOR_SCHEMA_PARSER) {
if (m_schema_parser_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
g_perfmon->log("Done parsing schema to find achievement icon download names");
if (!m_schema_parser_future.get()) {
std::cerr << "Schema parsing failed, skipping icon downloads" << std::endl;
m_achievement_idle_data.state = ACH_STATE_FINISHED;
g_perfmon->log("Achievements retrieved, no icons.");
m_window->show_no_achievements_found_placeholder();
m_achievement_refresh_lock.unlock();
return G_SOURCE_REMOVE;
}
m_achievement_idle_data.state = ACH_STATE_DOWNLOADING_ICONS;
}
return G_SOURCE_CONTINUE;
}
if (m_achievement_idle_data.state == ACH_STATE_DOWNLOADING_ICONS) {
// this could hang if we failed to parse all the icon download names
bool done_starting_downloads = (m_achievement_idle_data.current_item == g_steam->get_achievements().size());
@ -94,16 +83,19 @@ AsyncGuiLoader::load_achievements_idle()
m_window->show_no_achievements_found_placeholder();
m_achievement_refresh_lock.unlock();
// See top of the file
// VALGRIND_MONITOR_COMMAND("detailed_snapshot");
#ifdef DEBUG_CERR
VALGRIND_MONITOR_COMMAND("detailed_snapshot");
#endif
return G_SOURCE_REMOVE;
}
if ( !done_starting_downloads && (m_concurrent_icon_downloads < MAX_CONCURRENT_ICON_DOWNLOADS)) {
const Achievement_t ach = g_steam->get_achievements()[m_achievement_idle_data.current_item];
// Fire off a new download thread
std::string id = g_steam->get_achievements()[m_achievement_idle_data.current_item].id;
std::string icon_download_name = m_schema_parser.get_icon_download_names()[id];
std::string id = ach.id;
std::string icon_download_name = ach.icon_name;
// Assuming it returns empty string on failing to lookup
if (icon_download_name.empty()) {
@ -147,10 +139,6 @@ AsyncGuiLoader::populate_achievements() {
m_window->show_fetch_achievements_placeholder();
Glib::signal_idle().connect(sigc::mem_fun(this, &AsyncGuiLoader::load_achievements_idle), G_PRIORITY_LOW);
// g_idle_add_full (G_PRIORITY_LOW,
// this->load_achievements_idle,
// data,
// finish_load_achievements);
} else {
std::cerr << "Not launching game/refreshing achievements because a refresh is already in progress" << std::endl;

View File

@ -27,7 +27,6 @@ enum {
ACH_STATE_STARTED, /* start state */
ACH_STATE_WAITING_FOR_ACHIEVEMENTS, /* waiting for child to fetch achievements */
ACH_STATE_LOADING_GUI, /* feeding game rows to the model */
ACH_STATE_WAITING_FOR_SCHEMA_PARSER,/* waiting for stats schema parser to get icons download names */
ACH_STATE_DOWNLOADING_ICONS, /* fire off icon downloads (to be added to the model) */
ACH_STATE_FINISHED /* fin ish state */
};
@ -88,6 +87,5 @@ private:
IdleData m_app_idle_data;
IdleData m_achievement_idle_data;
UserGameStatsSchemaParser m_schema_parser;
MainPickerWindow* m_window;
};

View File

@ -284,7 +284,7 @@ MainPickerWindow::confirm_game_list() {
*/
void
MainPickerWindow::refresh_app_icon(AppId_t app_id) {
const std::string path = get_app_icon_path(app_id);
const std::string path = get_app_icon_path(g_steam->get_cache_path(), app_id);
for ( AppBoxRow* i : m_app_list_rows )
{
@ -303,9 +303,8 @@ MainPickerWindow::refresh_app_icon(AppId_t app_id) {
*/
void
MainPickerWindow::refresh_achievement_icon(AppId_t app_id, std::string id) {
const std::string path = get_achievement_icon_path(app_id, id);
const std::string path = get_achievement_icon_path(g_steam->get_cache_path(), app_id, id);
// replace_icon(path, m_achievement_list_rows[id]->get_main_widget(), 64, 64);
for ( AchievementBoxRow* i : m_achievement_list_rows )
{
if ( i->get_achievement().id == id ) {

View File

@ -63,6 +63,9 @@ encode_achievement(yajl_gen handle, Achievement_t achievement) {
yajl_gen_string_wrap(handle, ID_STR);
yajl_gen_string_wrap(handle, achievement.id.c_str());
yajl_gen_string_wrap(handle, ICON_STR);
yajl_gen_string_wrap(handle, achievement.icon_name.c_str());
// https://github.com/lloyd/yajl/issues/222
yajl_gen_string_wrap(handle, RATE_STR);
if (yajl_gen_double(handle, (double)achievement.global_achieved_rate) != yajl_gen_status_ok) {
@ -80,12 +83,60 @@ encode_achievement(yajl_gen handle, Achievement_t achievement) {
}
}
/**
* Encode an individual stat into a given YAJL handle
*/
void
encode_stat(yajl_gen handle, StatValue_t stat) {
yajl_gen_string_wrap(handle, STAT_DISPLAY_STR);
yajl_gen_string_wrap(handle, stat.display_name.c_str());
yajl_gen_string_wrap(handle, STAT_ID_STR);
yajl_gen_string_wrap(handle, stat.id.c_str());
yajl_gen_string_wrap(handle, STAT_INCREMENTONLY_STR);
if (yajl_gen_bool(handle, stat.incrementonly) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
yajl_gen_string_wrap(handle, STAT_PERMISSION_STR);
if (yajl_gen_integer(handle, stat.permission) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
yajl_gen_string_wrap(handle, STAT_TYPE_STR);
if (yajl_gen_integer(handle, (int)stat.type) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
if ( stat.type == UserStatType::Integer )
{
int value = std::any_cast<int>(stat.value);
yajl_gen_string_wrap(handle, STAT_VALUE_STR);
if (yajl_gen_integer(handle, value) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
else if ( stat.type == UserStatType::Float )
{
float value = std::any_cast<float>(stat.value);
yajl_gen_string_wrap(handle, STAT_VALUE_STR);
if (yajl_gen_double(handle, value) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
}
/**
* Encode an achievement vector into a given YAJL handle
*/
void
encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements) {
encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements, std::vector<StatValue_t> stats) {
// Achievements array
yajl_gen_string_wrap(handle, ACHIEVEMENT_LIST_STR);
if (yajl_gen_array_open(handle) != yajl_gen_status_ok) {
@ -107,9 +158,122 @@ encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements) {
if (yajl_gen_array_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
// Stats array
yajl_gen_string_wrap(handle, STAT_LIST_STR);
if (yajl_gen_array_open(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
for (StatValue_t stat : stats) {
if (yajl_gen_map_open(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
encode_stat(handle, stat);
if (yajl_gen_map_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
if (yajl_gen_array_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
//parsing the array inline would not be nice, so just extract them all here
std::vector<StatValue_t>
decode_stats(std::string response) {
std::vector<StatValue_t> stats;
char error_buffer[500];
yajl_val node = yajl_tree_parse(response.c_str(), error_buffer, 500);
if (node == NULL) {
std::cerr << "Parsing error: " << error_buffer << std::endl;
zenity();
exit(EXIT_FAILURE);
}
const char * stat_path[] = { STAT_LIST_STR, (const char*)0 };
const char * type_path[] = { STAT_TYPE_STR, (const char*)0 };
const char * id_path[] = { STAT_ID_STR, (const char*)0 };
const char * display_path[] = { STAT_DISPLAY_STR, (const char*)0 };
const char * value_path[] = { STAT_VALUE_STR, (const char*)0 };
const char * incrementonly_path[] = { STAT_INCREMENTONLY_STR, (const char*)0 };
const char * permission_path[] = { STAT_PERMISSION_STR, (const char*)0 };
yajl_val v = yajl_tree_get(node, stat_path, yajl_t_array);
if (v == NULL) {
std::cerr << "parsing error (no stat listing found)" << std::endl;
}
yajl_val *w = YAJL_GET_ARRAY(v)->values;
size_t array_len = YAJL_GET_ARRAY(v)->len;
stats.clear();
stats.resize(array_len);
for(unsigned i = 0; i < array_len; i++) {
yajl_val cur_node = w[i];
yajl_val cur_val;
cur_val = yajl_tree_get(cur_node, type_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error (stat type)" << std::endl;
}
stats[i].type = (UserStatType)YAJL_GET_INTEGER(cur_val);
cur_val = yajl_tree_get(cur_node, id_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error (stat id)" << std::endl;
}
stats[i].id = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, display_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error (stat display)" << std::endl;
}
stats[i].display_name = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, incrementonly_path, yajl_t_any);
if (cur_val == NULL) {
std::cerr << "parsing error (stat incrementonly)" << std::endl;
}
if (!YAJL_IS_TRUE(cur_val) && !YAJL_IS_FALSE(cur_val)) {
std::cerr << "bool parsing error (stat incrementonly)" << std::endl;
}
stats[i].incrementonly = YAJL_IS_TRUE(cur_val);
cur_val = yajl_tree_get(cur_node, permission_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error (stat permission)" << std::endl;
}
stats[i].permission = YAJL_GET_INTEGER(cur_val);
if (stats[i].type == UserStatType::Integer)
{
cur_val = yajl_tree_get(cur_node, value_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error (stat value int)" << std::endl;
}
stats[i].value = YAJL_GET_INTEGER(cur_val);
}
else if (stats[i].type == UserStatType::Float) {
cur_val = yajl_tree_get(cur_node, value_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error (stat value float)" << std::endl;
}
stats[i].value = YAJL_GET_DOUBLE(cur_val);
}
else {
std::cerr << "Unable to get stat value: Unsupported type. " << stats[i].id << std::endl;
}
}
return stats;
}
std::vector<Achievement_t>
decode_achievements(std::string response) {
@ -128,6 +292,7 @@ decode_achievements(std::string response) {
// dumb defines for required interface for yajl_tree
const char * list_path[] = { ACHIEVEMENT_LIST_STR, (const char*)0 };
const char * name_path[] = { NAME_STR, (const char*)0 };
const char * icon_path[] = { ICON_STR, (const char*)0 };
const char * desc_path[] = { DESC_STR, (const char*)0 };
const char * id_path[] = { ID_STR, (const char*)0 };
const char * rate_path[] = { RATE_STR, (const char*)0 };
@ -154,25 +319,31 @@ decode_achievements(std::string response) {
// and via YAJL_IS_* checks if type alone isn't sufficient
cur_val = yajl_tree_get(cur_node, name_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
std::cerr << "parsing error (ach name)" << std::endl;
}
achievements[i].name = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, icon_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error (ach icon)" << std::endl;
}
achievements[i].icon_name = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, desc_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
std::cerr << "parsing error (ach desc)" << std::endl;
}
achievements[i].desc = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, id_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
std::cerr << "parsing error (ach id)" << std::endl;
}
achievements[i].id = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, rate_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
std::cerr << "parsing error (ach rate)" << std::endl;
}
if (!YAJL_IS_DOUBLE(cur_val)) {
std::cerr << "double float parsing error" << std::endl;
@ -182,7 +353,7 @@ decode_achievements(std::string response) {
// why is bool parsing weird
cur_val = yajl_tree_get(cur_node, achieved_path, yajl_t_any);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
std::cerr << "parsing error (ach achieved)" << std::endl;
}
if (!YAJL_IS_TRUE(cur_val) && !YAJL_IS_FALSE(cur_val)) {
std::cerr << "bool parsing error" << std::endl;
@ -191,7 +362,7 @@ decode_achievements(std::string response) {
cur_val = yajl_tree_get(cur_node, hidden_path, yajl_t_any);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
std::cerr << "parsing error (ach hidden)" << std::endl;
}
if (!YAJL_IS_TRUE(cur_val) && !YAJL_IS_FALSE(cur_val)) {
std::cerr << "bool parsing error" << std::endl;

View File

@ -5,6 +5,7 @@
#include <yajl/yajl_gen.h>
#include <yajl/yajl_tree.h>
#include "../types/Achievement.h"
#include "../types/StatValue.h"
#include "../types/Actions.h"
#include "ProcessedGameServerRequest.h"
@ -35,13 +36,18 @@ void encode_request(yajl_gen handle, const char * request);
* Encode an array at a time because decoding individual
* achievements in YAJL would be messy
*/
void encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements);
void encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements, std::vector<StatValue_t> stats);
/**
* Decode the achievement vector from a json response
*/
std::vector<Achievement_t> decode_achievements(std::string response);
/**
* Decode the stats vector from a json response
*/
std::vector<StatValue_t> decode_stats(std::string response);
/**
* Encode just achievement changes into a handle
*/

View File

@ -3,12 +3,12 @@
* this project. Thank you.
*************************************/
#include "schema_parser/UserGameStatsSchemaParser.h"
#include "controller/MySteam.h"
#include "controller/MySteamClient.h"
#include "controller/MyGameServer.h"
#include "gui/MainPickerWindowFactory.h"
#include "cli/cli_funcs.h"
#include "common/functions.h"
#include "common/PerfMon.h"
#include "globals.h"
@ -21,12 +21,9 @@
**************************************/
MySteam* g_steam = nullptr;
MainPickerWindow* g_main_gui = nullptr;
char* g_cache_folder = nullptr;
char* g_runtime_folder = nullptr;
MySteamClient* g_steamclient = nullptr;
PerfMon* g_perfmon = nullptr;
/**************************************
* Main entry point
**************************************/
@ -39,21 +36,6 @@ main(int argc, char *argv[])
exit(EXIT_FAILURE);
}
if (getenv("XDG_CACHE_HOME")) {
g_cache_folder = concat( getenv("XDG_CACHE_HOME"), "/SamRewritten" );
} else {
g_cache_folder = concat( getenv("HOME"), "/.cache/SamRewritten" );
}
mkdir_default(g_cache_folder);
if (getenv("XDG_RUNTIME_DIR")) {
g_runtime_folder = concat( getenv("XDG_RUNTIME_DIR"), "/SamRewritten" );
mkdir_default(g_runtime_folder);
} else {
std::cerr << "XDG_RUNTIME_DIR is not set! Your distribution is improper... falling back to cache dir" << std::endl;
g_runtime_folder = g_cache_folder;
}
g_perfmon = new PerfMon();
g_steam = MySteam::get_instance();
g_steamclient = new MySteamClient();

View File

@ -28,6 +28,9 @@
#include "KeyValue.h"
#include <strings.h>
// https://stackoverflow.com/questions/8267847/why-should-i-initialize-static-class-variables-in-c
KeyValue* KeyValue::Invalid = new KeyValue();
// Stream helper functions
int8_t read_value_u8(std::istream * is) {
@ -74,11 +77,11 @@ std::string read_string(std::istream * is) {
return std::string(buf);
}
// returns NULL if no children or children not found
// returns KeyValue::Invalid if no children or children not found
//KeyValue* KeyValue::operator[](std::string key) {
KeyValue* KeyValue::get(std::string key) {
if (this->valid == false) {
return NULL;
return KeyValue::Invalid;
}
KeyValue* select_child = NULL;
@ -88,7 +91,8 @@ KeyValue* KeyValue::get(std::string key) {
select_child = child;
}
}
return select_child;
return select_child ?: KeyValue::Invalid;
}
KeyValue* KeyValue::get2(std::string key1, std::string key2) {
@ -139,10 +143,9 @@ int KeyValue::as_integer(int default_value) {
case KeyValueType::String:
case KeyValueType::WideString:
{
// eh exception handling
try {
return std::stoi(std::any_cast<std::string>(this->value));
} catch (std::exception& e) {
} catch (std::exception&) {
return default_value;
}
}
@ -163,25 +166,101 @@ int KeyValue::as_integer(int default_value) {
break;
}
case KeyValueType::Color:
case KeyValueType::Pointer:
default: {}
}
return default_value;
}
float KeyValue::as_float(float default_value) {
if (this->valid == false) {
return default_value;
}
if (!this->value.has_value()) { // This check is not part of original C# SAM
return default_value;
}
switch (this->type) {
case KeyValueType::String:
case KeyValueType::WideString:
{
break;
try {
return std::stof(std::any_cast<std::string>(this->value));
} catch (std::exception&) {
return default_value;
}
}
default:
case KeyValueType::Int32:
{
std::cout << "Invalid type referenced in stats parser!" << std::endl;
return default_value;
return std::any_cast<int32_t>(this->value);
}
case KeyValueType::UInt64:
{
return std::any_cast<uint64_t>(this->value);
}
case KeyValueType::Float32:
{
return std::any_cast<float>(this->value);
}
default: {}
}
return default_value;
}
bool KeyValue::as_boolean(bool default_value) {
if (this->valid == false)
{
return default_value;
}
switch (this->type)
{
case KeyValueType::String:
case KeyValueType::WideString:
{
try {
int pr = std::stoi(std::any_cast<std::string>(this->value));
return pr != 0;
} catch (std::exception&) {
return default_value;
}
}
case KeyValueType::Int32:
{
return std::any_cast<int32_t>(this->value) != 0;
}
case KeyValueType::UInt64:
{
return std::any_cast<uint64_t>(this->value) != 0;
}
case KeyValueType::Float32:
{
return std::any_cast<float>(this->value) != 0;
}
default: {}
}
return default_value;
}
KeyValue* KeyValue::load_as_binary(std::string path) {
#ifdef DEBUG_CERR
std::cerr << "Parsing schema file " << path << std::endl;
#endif
std::filebuf fb;
if (!fb.open(path, std::ios::in)) {
std::cerr << "Unable to open schema file: " << path << std::endl;
return NULL;
}

View File

@ -46,8 +46,7 @@ enum class KeyValueType : unsigned char
class KeyValue {
private:
// invalid doesn't map easily to C++,
// just use NULL for bad return of [] operator
static KeyValue* Invalid;
public:
std::string name = "<root>";
@ -62,6 +61,8 @@ public:
std::string as_string(std::string default_value);
int as_integer(int default_value);
float as_float(float default_value);
bool as_boolean(bool default_value);
// Other as_type function can be implemented as needed
static KeyValue* load_as_binary(std::string path);
@ -74,4 +75,4 @@ public:
}
};
};
};

View File

@ -24,17 +24,23 @@
// The original SAM is available at https://github.com/gibbed/SteamAchievementManager
// To comply with copyright, the above license is included.
#include <strings.h>
#include <memory>
#include "UserGameStatsSchemaParser.h"
#include "KeyValue.h"
#include "../types/UserStatType.h"
#include "../controller/MySteam.h"
#include "../globals.h"
#include <strings.h>
#include "../types/IntegerStatDefinition.h"
#include "../types/FloatStatDefinition.h"
bool
UserGameStatsSchemaParser::load_user_game_stats_schema() {
m_icon_download_names.clear();
clear_stat_definitions();
std::string appid_string = std::to_string(g_steam->get_current_appid());
std::string schema_file = g_steam->get_steam_install_path() + "/appcache/stats/UserGameStatsSchema_" + appid_string + ".bin";
@ -74,14 +80,34 @@ UserGameStatsSchemaParser::load_user_game_stats_schema() {
case UserStatType::Integer:
{
// don't care currently
IntegerStatDefinition* def = new IntegerStatDefinition;
def->type = UserStatType::Integer;
def->Id = stat->get("name")->as_string("");
def->DisplayName = stat->get2("display", "name")->as_string("");
def->MinValue = stat->get("min")->as_integer(0);
def->MaxValue = stat->get("max")->as_integer(0);
def->MaxChange = stat->get("maxchange")->as_integer(0);
def->IncrementOnly = stat->get("incrementonly")->as_boolean(false);
def->DefaultValue = stat->get("default")->as_integer(0);
def->Permission = stat->get("permission")->as_integer(0);
m_stats.push_back(def);
break;
}
case UserStatType::Float:
case UserStatType::AverageRate:
{
// don't care currently
FloatStatDefinition* def = new FloatStatDefinition;
def->type = UserStatType::Float;
def->Id = stat->get("name")->as_string("");
def->DisplayName = stat->get2("display", "name")->as_string("");
def->MinValue = stat->get("min")->as_float(0);
def->MaxValue = stat->get("max")->as_float(0);
def->MaxChange = stat->get("maxchange")->as_float(0);
def->IncrementOnly = stat->get("incrementonly")->as_boolean(false);
def->DefaultValue = stat->get("default")->as_float(0);
def->Permission = stat->get("permission")->as_integer(0);
m_stats.push_back(def);
break;
}
@ -136,4 +162,13 @@ UserGameStatsSchemaParser::load_user_game_stats_schema() {
delete kv;
return true;
}
void
UserGameStatsSchemaParser::clear_stat_definitions() {
for (auto p : m_stats )
{
delete p;
}
m_stats.clear();
}

View File

@ -27,7 +27,11 @@
#pragma once
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "../types/StatDefinition.h"
class UserGameStatsSchemaParser
{
@ -40,11 +44,22 @@ public:
bool load_user_game_stats_schema();
/**
* Simple getter
* Simple getters
*/
std::map<std::string, std::string> get_icon_download_names() const { return m_icon_download_names; };
std::vector<StatDefinition*> get_stat_definitions() const { return m_stats; };
/**
* Because stats are pointers, we need to free them before clearing the array
* Should I use make_unique instead? Probably
*/
void clear_stat_definitions();
private:
// Mapping between achievement ID and the actual icon name on servers.
// Icon name is retrieved by the stats schema parser
std::map<std::string, std::string> m_icon_download_names;
// Container for app stats. Pointer for later casts
std::vector<StatDefinition*> m_stats;
};

View File

@ -53,6 +53,11 @@ MyClientSocket::request_response(std::string request)
send_message(request);
std::string ret = receive_message();
disconnect();
#ifdef DEBUG_CERR
std::cerr << "<-- Receiving " << ret << std::endl;
#endif
return ret;
}

View File

@ -2,11 +2,16 @@
#include <yajl/yajl_tree.h>
#include "MyGameSocket.h"
#include "../types/Actions.h"
#include "../types/IntegerStatDefinition.h"
#include "../types/FloatStatDefinition.h"
#include "../json/ProcessedGameServerRequest.h"
#include "../json/yajlHelpers.h"
#include "../globals.h"
#include "../common/PerfMon.h"
#include "../common/functions.h"
#include "../schema_parser/UserGameStatsSchemaParser.h"
#include "../controller/MySteam.h"
MyGameSocket::MyGameSocket(AppId_t appid) :
MyServerSocket(appid),
@ -30,7 +35,8 @@ MyGameSocket::process_request(std::string request, bool& quit) {
switch (r.getAction()) {
case GET_ACHIEVEMENTS:
encode_achievements(handle, get_achievements()); // Write achievements to handle
populate_stats_and_achievements();
encode_achievements(handle, get_achievements(), get_stats()); // Write achievements to handle
break;
case STORE_ACHIEVEMENTS:
@ -56,16 +62,18 @@ MyGameSocket::process_request(std::string request, bool& quit) {
ret = std::string((const char*)buf);
yajl_gen_free(handle);
#ifdef DEBUG_CERR
std::cerr << "--> Sending buffer " << ret << std::endl;
#endif
return ret;
}
std::vector<Achievement_t>
MyGameSocket::get_achievements() {
void
MyGameSocket::populate_stats_and_achievements() {
m_stats_callback_received = false;
ISteamUserStats *stats_api = SteamUserStats();
if (!stats_api->RequestCurrentStats()) {
if (!SteamUserStats()->RequestCurrentStats()) {
std::cerr << "ERROR: User not logged in, exiting." << std::endl;
zenity("Please login to Steam before using SamRewritten. If this is the case, please report it with a Github issue.");
exit(EXIT_FAILURE);
@ -79,16 +87,14 @@ MyGameSocket::get_achievements() {
if (!get_global_stats())
{
std::cerr << "An error occurred getting global stats." << std::endl;
return m_achievement_list;
return;
}
if (!get_global_achievements_stats())
{
std::cerr << "An error occurred getting global achievements stats." << std::endl;
return m_achievement_list;
return;
}
return m_achievement_list;
}
bool
@ -135,14 +141,16 @@ MyGameSocket::get_global_achievements_stats() {
void
MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
m_achievement_list.clear();
m_stats_list.clear();
// Check if we received the values for the correct app
if (SteamUtils()->GetAppID() == callback->m_nGameID) {
if ( k_EResultOK == callback->m_eResult ) {
ISteamUserStats *stats_api = SteamUserStats();
m_schema_parser.load_user_game_stats_schema();
// ==============================
// RETRIEVE IDS
// RETRIEVE ACHIEVEMENTS
// ==============================
const unsigned num_ach = stats_api->GetNumAchievements();
@ -153,17 +161,81 @@ MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
m_achievement_list.resize(num_ach);
for (unsigned i = 0; i < num_ach ; i++) {
const char * pchName = stats_api->GetAchievementName(i);
m_achievement_list[i].id = stats_api->GetAchievementName(i);
const char * pchName = m_achievement_list[i].id.c_str();
m_achievement_list[i].id = pchName;
m_achievement_list[i].name = stats_api->GetAchievementDisplayAttribute(pchName, "name");
m_achievement_list[i].desc = stats_api->GetAchievementDisplayAttribute(pchName, "desc");
// Value set in OnGlobalAchievementPercentagesReceived
m_achievement_list[i].global_achieved_rate = 0;
stats_api->GetAchievement(pchName, &(m_achievement_list[i].achieved));
m_achievement_list[i].hidden = (bool)strcmp(stats_api->GetAchievementDisplayAttribute(pchName, "hidden" ), "0");
m_achievement_list[i].icon_name = m_schema_parser.get_icon_download_names()[m_achievement_list[i].id];
}
// ============================
// RETRIEVE STATS
// ============================
for ( auto def : m_schema_parser.get_stat_definitions() )
{
StatValue_t sv;
if ( def->type == UserStatType::Integer )
{
IntegerStatDefinition* cast = dynamic_cast<IntegerStatDefinition*>(def);
if (cast == NULL) {
std::cerr << "Cast error to int for " << def->Id << ", please report to Github!" << std::endl;
continue;
}
int value;
bool success = stats_api->GetStat(def->Id.c_str(), &value);
if ( !success )
{
std::cerr << "Unable to get int stat " << def->Id << std::endl;
continue;
}
sv.type = cast->type;
sv.display_name = cast->DisplayName;
sv.id = cast->Id;
sv.incrementonly = cast->IncrementOnly;
sv.original_value = value;
sv.value = value;
sv.permission = cast->Permission;
m_stats_list.push_back(sv);
}
else if ( def->type == UserStatType::Float )
{
FloatStatDefinition* cast = dynamic_cast<FloatStatDefinition*>(def);
if (cast == NULL) {
std::cerr << "Cast error to float for " << def->Id << ", please report to Github!" << std::endl;
continue;
}
float value;
bool success = stats_api->GetStat(def->Id.c_str(), &value);
if ( !success )
{
std::cerr << "Unable to get float stat " << def->Id << std::endl;
continue;
}
sv.type = cast->type;
sv.display_name = cast->DisplayName;
sv.id = cast->Id;
sv.incrementonly = cast->IncrementOnly;
sv.original_value = value;
sv.value = value;
sv.permission = cast->Permission;
m_stats_list.push_back(sv);
}
}
} else {
@ -183,7 +255,10 @@ MyGameSocket::OnGlobalStatsReceived(GlobalStatsReceived_t *callback, bool bIOFai
std::cerr << "GlobalStatsReceived_t failed! Enum: " << callback->m_eResult << std::endl;
}
#ifdef DEBUG_CERR
std::cout << "Got stats, maybe I can do cool stuff with them, gotta check." << std::endl;
#endif
m_global_callback_received = true;
}

View File

@ -1,6 +1,8 @@
#pragma once
#include "MyServerSocket.h"
#include "../schema_parser/UserGameStatsSchemaParser.h"
#include "../types/Achievement.h"
#include "../types/StatValue.h"
#include "../../steam/steam_api.h"
#include <iostream>
@ -13,13 +15,19 @@
class MyGameSocket : public MyServerSocket
{
public:
MyGameSocket(AppId_t appid);
std::string process_request(std::string request, bool& quit);
std::vector<Achievement_t> get_achievements(void);
void populate_stats_and_achievements(void);
bool get_global_stats(void);
bool get_global_achievements_stats(void);
void process_changes(std::vector<AchievementChange_t> changes);
MyGameSocket(AppId_t appid);
/**
* Simple getters
*/
std::vector<Achievement_t> get_achievements() const { return m_achievement_list; };
std::vector<StatValue_t> get_stats() const { return m_stats_list; };
/**
* Steam API callback to handle the received stats and achievements
@ -27,6 +35,8 @@ public:
STEAM_CALLBACK( MyGameSocket, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived );
private:
UserGameStatsSchemaParser m_schema_parser;
std::atomic_bool m_stats_callback_received;
std::atomic_bool m_global_callback_received;
std::atomic_bool m_global_achievements_callback_ready;
@ -38,6 +48,6 @@ private:
CCallResult< MyGameSocket, GlobalAchievementPercentagesReady_t > m_GlobalAchievementPercentagesReadyCallResult;
std::vector<Achievement_t> m_achievement_list;
std::vector<StatValue_t> m_stats_list;
};

View File

@ -66,7 +66,9 @@ MyServerSocket::run_server()
if (quit)
{
#ifdef DEBUG_CERR
std::cout << "Shutting down server safely." << std:: endl;
#endif
// destruction of this object will take care of shutdown
break;
}

View File

@ -2,12 +2,13 @@
#include "../globals.h"
#include "../common/functions.h"
#include "../controller/MySteam.h"
#include <iostream>
MySocket::MySocket(AppId_t appid) : m_appid(appid), m_socket_fd(-1)
{
m_socket_path = std::string(g_runtime_folder) + "/" + std::to_string(m_appid) + "-ipc.sock";
m_socket_path = g_steam->get_runtime_path() + "/" + std::to_string(m_appid) + "-ipc.sock";
}
MySocket::~MySocket()

View File

@ -31,6 +31,7 @@ struct Achievement_t {
std::string name;
std::string desc;
std::string id;
std::string icon_name;
float global_achieved_rate;
bool achieved;
bool hidden;

View File

@ -9,13 +9,24 @@
// Mirroring structure of Achievement_t, should be combined with that
#define SAM_ACTION_STR "SAM_ACTION"
#define ACHIEVEMENT_LIST_STR "ACHIEVEMENT_LIST"
#define STAT_LIST_STR "STAT_LIST"
#define NAME_STR "NAME"
#define DESC_STR "DESC"
#define ICON_STR "ICON"
#define ID_STR "ID"
#define RATE_STR "RATE"
#define ACHIEVED_STR "ACHIEVED"
#define HIDDEN_STR "HIDDEN"
// Mirroring structure of StatValue_t, should be combined with that
#define STAT_TYPE_STR "STAT_TYPE"
#define STAT_ID_STR "STAT_ID"
#define STAT_DISPLAY_STR "STAT_DISPLAY"
#define STAT_VALUE_STR "STAT_VALUE"
#define STAT_INCREMENTONLY_STR "STAT_INCREMENTONLY"
#define STAT_PERMISSION_STR "STAT_PERMISSION"
// Misc
#define SAM_ACK_STR "SAM_ACK"
// TODO: Add SAM_START as an action to this too?
@ -65,6 +76,20 @@ Server --> Client
.
.
.
],
STAT_LIST_STR:
[
{
STAT_DISPLAY_STR: "desc",
STAT_ID_STR: "ID",
STAT_INCREMENTONLY_STR: true/false,
STAT_PERMISSION_STR: <int value>,
STAT_TYPE_STR: <int cast of UserStatType>,
STAT_VALUE_STR: <int or float>
},
.
.
.
]
}

View File

@ -0,0 +1,35 @@
/* 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.
*/
#pragma once
#include "StatDefinition.h"
struct FloatStatDefinition : StatDefinition
{
float MinValue;
float MaxValue;
float MaxChange;
bool IncrementOnly;
float DefaultValue;
};
typedef struct FloatStatDefinition FloatStatDefinition;

View File

@ -0,0 +1,35 @@
/* 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.
*/
#pragma once
#include "StatDefinition.h"
struct IntegerStatDefinition : StatDefinition
{
int MinValue;
int MaxValue;
int MaxChange;
bool IncrementOnly;
int DefaultValue;
};
typedef struct IntegerStatDefinition IntegerStatDefinition;

View File

@ -0,0 +1,39 @@
/* 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.
*/
#pragma once
#include <string>
#include "UserStatType.h"
struct StatDefinition
{
std::string Id;
std::string DisplayName;
int Permission;
UserStatType type;
// https://stackoverflow.com/questions/36736167/c-cast-struct-dynamically-based-on-subtype
// We need this or any other virtual member to make Base polymorphic
virtual ~StatDefinition () { }
};
typedef struct StatDefinition StatDefinition;

18
src/types/StatValue.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <any>
#include "UserStatType.h"
struct StatValue_t {
UserStatType type;
std::string id;
std::string display_name;
std::any value;
std::any original_value;
bool incrementonly;
int permission;
};
typedef struct StatValue_t StatValue_t;