Implement achievement icon parsing and downloading (#39)

* Implement achievement icon parsing and downloading

Port statsSchema parsing from gibbed's original
Steam Achievement Manager and include appropriate copyrights.

Remove previous code that would do icon parsing.
Implement async achievement icon downloading.
Remove lodepng since it's no longer needed

* minor changes

* Load achievement icons into GUI
This commit is contained in:
William Pierce 2019-11-15 11:14:16 -08:00 committed by GitHub
parent eab5ee6b6a
commit 7ff28cf5d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 944 additions and 8172 deletions

View File

@ -3,6 +3,7 @@
#include <fstream>
#include <algorithm>
#include <dirent.h>
#include <bits/stdc++.h>
#include <yajl/yajl_gen.h>
#include <yajl/yajl_tree.h>
#include "types/Game.h"
@ -12,7 +13,32 @@
#include "json/yajlHelpers.h"
MySteam::MySteam() {
std::string data_home_path;
if (getenv("XDG_DATA_HOME") != NULL) {
data_home_path = getenv("XDG_DATA_HOME");
} else {
data_home_path = getenv("HOME") + std::string("/.local/share");
}
if (file_exists(data_home_path + "/Steam/appcache/appinfo.vdf")) {
m_steam_install_dir = std::string(data_home_path + "/Steam");
return;
}
const std::string home_path = getenv("HOME");
if (file_exists(home_path + "/.steam/appcache/appinfo.vdf")) {
m_steam_install_dir = std::string(home_path + "/.steam");
return;
}
else if (file_exists(home_path + "/.steam/steam/appcache/appinfo.vdf")) {
m_steam_install_dir = std::string(home_path + "/.steam/steam");
return;
}
else {
std::cerr << "Unable to locate the steam directory. TODO: implement a folder picker here" << std::endl;
system("zenity --error --no-wrap --text=\"Unable to find your Steam installation directory.. Please report this on Github!\"");
exit(EXIT_FAILURE);
}
}
// => Constructor
@ -56,6 +82,7 @@ MySteam::launch_game(AppId_t appID) {
return false;
}
m_app_id = appID;
return true;
}
// => launch_game
@ -111,52 +138,33 @@ MySteam::refresh_owned_apps() {
/**
* Tries to locate the steam folder in multiple locations,
* which is not a failsafe implementation.
*
* The original steamclient library path is the returned path + "/linux64/steamclient.so"
*/
std::string
MySteam::get_steam_install_path() {
std::string data_home_path;
if (getenv("XDG_DATA_HOME") != NULL) {
data_home_path = getenv("XDG_DATA_HOME");
} else {
data_home_path = getenv("HOME") + std::string("/.local/share");
}
if (file_exists(data_home_path + "/Steam/appcache/appinfo.vdf")) {
return std::string(data_home_path + "/Steam");
}
std::cerr << "Trying to find Steam install with legacy Steam paths" << std::endl;
const std::string home_path = getenv("HOME");
if (file_exists(home_path + "/.steam/appcache/appinfo.vdf")) {
return std::string(home_path + "/.steam");
}
else if (file_exists(home_path + "/.steam/steam/appcache/appinfo.vdf")) {
return std::string(home_path + "/.steam/steam");
}
else {
std::cerr << "Unable to locate the steam directory. TODO: implement a folder picker here" << std::endl;
exit(EXIT_FAILURE);
}
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.
* It also has a "callback" that will refresh the view.
*/
void
MySteam::refresh_icon(AppId_t app_id) {
MySteam::refresh_app_icon(AppId_t app_id) {
SteamAppDAO *appDAO = SteamAppDAO::get_instance();
appDAO->download_app_icon(app_id);
}
// => refresh_icons
// => refresh_app_icon
std::vector<Achievement_t>
MySteam::get_achievements() {
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);
}
// => refresh_achievement_icon
void
MySteam::refresh_achievements() {
if (m_ipc_socket == nullptr) {
std::cerr << "Connection to game is broken" << std::endl;
@ -169,9 +177,9 @@ MySteam::get_achievements() {
std::cerr << "Failed to receive ack!" << std::endl;
}
return decode_achievements(response);
m_achievements = decode_achievements(response);
}
// => get_achievements
// => refresh_achievements
/**
* Adds an achievement to the list of achievements to unlock/lock

View File

@ -24,7 +24,7 @@ public:
* This is not failsafe and may require some tweaking to add
* support for your distribution
*/
static std::string get_steam_install_path();
std::string get_steam_install_path();
/**
* Starts a process that will emulate a steam game with the
@ -39,16 +39,24 @@ public:
*/
bool quit_game();
/**
* Fetches icon for given app
*/
static void refresh_app_icon(AppId_t app_id);
/**
* Fetches all the achievment icon for a given app and
* stores it as id.jpg for ease of identification
* Not static because it uses the cached m_app_id to know
* which folder to put the icon in
*/
void refresh_achievement_icon(std::string id, std::string icon_download_name);
/**
* Makes a list of all owned games with stats or achievements.
*/
void refresh_owned_apps();
/**
* Fetches all the app icons either online or on the disk.
*/
static void refresh_icon(AppId_t app_id);
/**
* Returns all the already loaded retrieved apps by the latest logged
* in user. Make sure to call refresh_owned_apps at least once to get
@ -56,15 +64,23 @@ public:
*/
std::vector<Game_t> get_subscribed_apps() { return m_all_subscribed_apps; };
/**
* Makes a list of all achievements for the currently running app
*/
void refresh_achievements();
/**
* Get achievements of the launched app
*
* For now use an Achievement_t for ease of extension
* to count-based achievements
*
* Make sure to call refresh_achievements at least once to get
* correct results
*
* TODO: maybe don't name this the same as GameServer::get_achievements?
*/
std::vector<Achievement_t> get_achievements();
std::vector<Achievement_t> get_achievements() { return m_achievements; };
/**
* Adds a modification to be done on the launched app.
@ -94,6 +110,20 @@ public:
MySteam(MySteam const&) = delete;
void operator=(MySteam const&) = delete;
// TODO: have this public now so we can operate on them..
// Achievements for the currently running game
std::vector<Achievement_t> m_achievements;
// 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;
// Absolute path to Steam install dir
std::string m_steam_install_dir;
// Current app_id
AppId_t m_app_id;
private:
MySteam();
@ -106,6 +136,8 @@ private:
MyClientSocket* m_ipc_socket;
std::vector<Game_t> m_all_subscribed_apps;
std::map<std::string, bool> m_pending_ach_modifications;
std::map<std::string, double> m_pending_stat_modifications;
};

View File

@ -3,6 +3,7 @@
#include <dlfcn.h>
#include "../steam/steam_api.h"
#include "MySteam.h"
#include "globals.h"
#define RELATIVE_STEAM_CLIENT_LIB_PATH "/linux64/steamclient.so"
@ -46,7 +47,7 @@ public:
}
MySteamClient() {
char* error;
const std::string steam_client_lib_path = MySteam::get_steam_install_path() + RELATIVE_STEAM_CLIENT_LIB_PATH;
const std::string steam_client_lib_path = g_steam->get_steam_install_path() + RELATIVE_STEAM_CLIENT_LIB_PATH;
m_handle = dlopen(steam_client_lib_path.c_str(), RTLD_LAZY);
if (!m_handle) {
std::cerr << "Error opening the Steam Client library. Exiting. Info:" << std::endl;

View File

@ -134,11 +134,20 @@ 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(local_folder + "/banner");
const std::string local_path = get_app_icon_path(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());
Downloader::get_instance()->download_file(url, local_path);
}
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 url("http://media.steamcommunity.com/steamcommunity/public/images/apps/" + std::to_string(app_id) + "/" + icon_download_name);
mkdir_default(local_folder.c_str());
Downloader::get_instance()->download_file(url, local_path);
}

View File

@ -31,6 +31,12 @@ public:
*/
static void download_app_icon(AppId_t app_id);
/**
* Download the achievement icon to cache_folder/app_id/id.jpg
* If it fails, nothing is written on the disk.
*/
static void download_achievement_icon(AppId_t app_id, std::string id, std::string icon_download_name);
/**
* After it parsed all apps from the latest updates, returns the parsed apps
*/

View File

@ -84,6 +84,14 @@ 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_achievement_icon_path(AppId_t app_id, std::string id) {
return std::string(g_cache_folder) + "/" + std::to_string(app_id) + "/" + id + ".jpg";
}
void escape_html(std::string& data) {
std::string buffer;
buffer.reserve(data.size());

View File

@ -1,6 +1,8 @@
#pragma once
#include <string>
#include "../globals.h"
#include "../../steam/steam_api.h"
/**
* Wrapper for fork()
@ -42,6 +44,16 @@ bool digits_only(const std::string& str);
*/
void mkdir_default(const char *pathname);
/**
* Generate path to given app icon
*/
std::string get_app_icon_path(AppId_t app_id);
/**
* Generate path to given app achievement icon
*/
std::string get_achievement_icon_path(AppId_t app_id, std::string id);
/**
* Escape html characters inline a string.
* From https://stackoverflow.com/a/5665377

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -66,8 +66,7 @@ MainPickerWindow::reset_game_list() {
void
MainPickerWindow::reset_achievements_list() {
for ( GtkAchievementBoxRow* row : m_achievement_list_rows )
{
for ( auto& [id, row] : m_achievement_list_rows) {
// gtk_widget_destroy called in destructor of GtkAchievementBoxRow
delete row;
row = nullptr;
@ -121,12 +120,14 @@ MainPickerWindow::add_to_game_list(const Game_t& app) {
void
MainPickerWindow::add_to_achievement_list(const Achievement_t& achievement) {
GtkAchievementBoxRow *row = new GtkAchievementBoxRow(achievement);
m_achievement_list_rows.push_back(row);
m_achievement_list_rows.insert(std::make_pair(achievement.id, row));
gtk_list_box_insert(m_achievement_list, GTK_WIDGET( row->get_main_widget() ), -1);
}
// => add_to_achievement_list
/**
* Draws all the achievements that have not been shown yet
*/
void
MainPickerWindow::confirm_achievement_list() {
gtk_widget_show_all( GTK_WIDGET(m_achievement_list) );
@ -144,53 +145,70 @@ MainPickerWindow::confirm_game_list() {
// => confirm_game_list
/**
* Refreshes the icon for the specified app ID
* As per GTK, this must only ever be called from the main thread.
* Helper to replace icon in the same location within a row
*/
void
MainPickerWindow::refresh_app_icon(AppId_t app_id) {
if(app_id == 0)
return;
//TODO make sure app_id is index of m_game_list_rows
void
replace_icon(std::string icon_path, GtkWidget * row, int dest_width, int dest_height) {
GList *children;
GtkImage *img;
GdkPixbuf *pixbuf;
GError *error;
std::string path(g_cache_folder);
GError *error = nullptr;
error = nullptr;
path += "/";
path += std::to_string(app_id);
path += "/banner";
children = gtk_container_get_children(GTK_CONTAINER(m_game_list_rows[app_id])); //children = the layout
children = gtk_container_get_children(GTK_CONTAINER(row)); //children = the layout
children = gtk_container_get_children(GTK_CONTAINER(children->data)); //children = first element of layout
//children = g_list_next(children); //children = second element of layout...
img = GTK_IMAGE(children->data);
if( !GTK_IS_IMAGE(img) ) {
std::cerr << "It looks like the GUI has been modified, or something went wrong." << std::endl;
std::cerr << "Inform the developer or look at MainPickerWindow::refresh_app_icon." << std::endl;
std::cerr << "Inform the developer or look at MainPickerWindow's replace_icon." << std::endl;
exit(EXIT_FAILURE);
}
g_list_free(children);
pixbuf = gdk_pixbuf_new_from_file(path.c_str(), &error);
pixbuf = gdk_pixbuf_new_from_file(icon_path.c_str(), &error);
if (error == NULL) {
// Quick and jerky, quality isn't key here
// Is the excess of memory freed though?
pixbuf = gdk_pixbuf_scale_simple(pixbuf, 146, 68, GDK_INTERP_NEAREST);
pixbuf = gdk_pixbuf_scale_simple(pixbuf, dest_width, dest_height, GDK_INTERP_NEAREST);
gtk_image_set_from_pixbuf(img, pixbuf);
}
else {
std::cerr << "Error loading banner: " << error->message << std::endl;
std::cerr << "Error loading icon: " << error->message << std::endl;
}
}
// => replace_icon
/**
* Refreshes the icon for the specified app ID
* As per GTK, this must only ever be called from the main thread.
*/
void
MainPickerWindow::refresh_app_icon(AppId_t app_id) {
//TODO make sure app_id is index of m_game_list_rows
std::string path = get_app_icon_path(app_id);
// Scale down the banner a bit
// Quick and jerky, quality isn't key here
replace_icon(path, m_game_list_rows[app_id], 146, 68);
}
// => refresh_app_icon
/**
* Refreshes the icon for the specified achievement icon
* Requires app_id information to know where to find the icon
*/
void
MainPickerWindow::refresh_achievement_icon(AppId_t app_id, std::string id) {
std::string path = get_achievement_icon_path(app_id, id);
// The achievement icons are 64x64, so no resizing
// This modifies the GtkAchievementBoxRow directly, but eh
replace_icon(path, m_achievement_list_rows[id]->get_main_widget(), 64, 64);
}
// => refresh_achievement_icon
void
MainPickerWindow::filter_games(const char* filter_text) {
const std::string text_filter(filter_text);
@ -243,8 +261,7 @@ MainPickerWindow::filter_achievements(const char* filter_text) {
return;
}
for ( GtkAchievementBoxRow* row : m_achievement_list_rows )
{
for ( const auto& [id, row] : m_achievement_list_rows) {
if (!strstri(row->get_achievement().name, text_filter)) {
gtk_widget_hide( row->get_main_widget() );
}
@ -266,8 +283,7 @@ MainPickerWindow::get_corresponding_appid_for_row(GtkListBoxRow *row) {
void
MainPickerWindow::unlock_all_achievements() {
for ( GtkAchievementBoxRow* row : m_achievement_list_rows )
{
for ( const auto& [id, row] : m_achievement_list_rows) {
row->unlock();
}
}
@ -275,8 +291,7 @@ MainPickerWindow::unlock_all_achievements() {
void
MainPickerWindow::lock_all_achievements() {
for ( GtkAchievementBoxRow* row : m_achievement_list_rows )
{
for ( const auto& [id, row] : m_achievement_list_rows) {
row->lock();
}
}
@ -284,8 +299,7 @@ MainPickerWindow::lock_all_achievements() {
void
MainPickerWindow::invert_all_achievements() {
for ( GtkAchievementBoxRow* row : m_achievement_list_rows )
{
for ( const auto& [id, row] : m_achievement_list_rows) {
row->invert();
}
}

View File

@ -77,6 +77,11 @@ public:
*/
void refresh_app_icon(AppId_t app_id);
/**
* Same as above for each achievement
*/
void refresh_achievement_icon(AppId_t app_id, std::string id);
/**
* Filters the game list. For a title to stay displayed,
* filter_text must be included in it
@ -177,10 +182,16 @@ public:
* and allowing multiple idle threads to corrupt the main window.
*/
std::mutex m_game_refresh_lock;
std::mutex m_achievement_refresh_lock;
int outstanding_icon_downloads;
std::future<void> owned_apps_future;
std::map<AppId_t, std::future<void>> icon_download_futures;
std::map<AppId_t, std::future<void>> app_icon_download_futures;
// Achievement info for the currently running game
std::future<void> achievements_future;
std::future<bool> schema_parser_future;
std::map<std::string, std::future<void>> achievement_icon_download_futures;
private:
GtkWidget *m_main_window;
@ -207,5 +218,5 @@ private:
GtkWidget *m_input_appid_row;
std::map<AppId_t, GtkWidget*> m_game_list_rows;
std::vector<GtkAchievementBoxRow*> m_achievement_list_rows;
std::map<std::string, GtkAchievementBoxRow*> m_achievement_list_rows;
};

View File

@ -6,26 +6,25 @@
#include "../common/PerfMon.h"
#include "../MySteam.h"
#include "../globals.h"
#include "../types/UserGameStatsSchemaParser.h"
// See comments in the header file
extern "C"
{
// Assigning special to achievements
void
parse_special() {
// TODO: Maybe split this up to be more amenable to threaded GUI loading, could fire off in thread
// TODO: achievements made public in MySteam, so we can modify them directly here, fix that
void
populate_achievements() {
// Get_achievements from game server
std::vector<Achievement_t> achievements = g_steam->get_achievements();
g_main_gui->reset_achievements_list();
// Assigning special to achievements
long next_most_achieved_index = -1;
float next_most_achieved_rate = 0;
Achievement_t tmp;
for(size_t i = 0; i < achievements.size(); i++) {
tmp = achievements[i];
achievements[i].special = ACHIEVEMENT_NORMAL;
for(size_t i = 0; i < g_steam->m_achievements.size(); i++) {
tmp = g_steam->m_achievements[i];
g_steam->m_achievements[i].special = ACHIEVEMENT_NORMAL;
if ( !tmp.achieved && tmp.global_achieved_rate > next_most_achieved_rate )
{
@ -35,20 +34,149 @@ extern "C"
if ( tmp.global_achieved_rate <= 5.f )
{
achievements[i].special = ACHIEVEMENT_RARE;
g_steam->m_achievements[i].special = ACHIEVEMENT_RARE;
}
}
if ( next_most_achieved_index != -1 )
{
achievements[next_most_achieved_index].special |= ACHIEVEMENT_NEXT_MOST_ACHIEVED;
g_steam->m_achievements[next_most_achieved_index].special |= ACHIEVEMENT_NEXT_MOST_ACHIEVED;
}
for( Achievement_t achievement : achievements ) {
g_main_gui->add_to_achievement_list(achievement);
}
// => parse_special
// see comments in load_apps_idle
static gboolean
load_achievements_idle (gpointer data_)
{
IdleData *data = (IdleData *)data_;
if (data->state == ACH_STATE_STARTED) {
g_perfmon->log("Starting achievement retrieval");
g_main_gui->achievements_future = std::async(std::launch::async, []{g_steam->refresh_achievements();});
data->state = ACH_STATE_WAITING_FOR_ACHIEVEMENTS;
return G_SOURCE_CONTINUE;
}
g_main_gui->confirm_achievement_list();
if (data->state == ACH_STATE_WAITING_FOR_ACHIEVEMENTS) {
if (g_main_gui->achievements_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
g_perfmon->log("Done retrieving achievements");
// Fire off the schema parsing now.
// It will modify g_steam->m_icon_download_names directly
// TODO: figure out if all the icons are already there and skip parsing schema
g_main_gui->schema_parser_future = std::async(std::launch::async, load_user_game_stats_schema);
parse_special();
data->state = ACH_STATE_LOADING_GUI;
}
return G_SOURCE_CONTINUE;
}
if (data->state == ACH_STATE_LOADING_GUI) {
if (data->current_item == g_steam->get_achievements().size()) {
g_perfmon->log("Done adding achievements to GUI");
g_main_gui->confirm_achievement_list();
data->state = ACH_STATE_WAITING_FOR_SCHEMA_PARSER;
data->current_item = 0;
return G_SOURCE_CONTINUE;
}
auto achievement = g_steam->get_achievements()[data->current_item];
g_main_gui->add_to_achievement_list(achievement);
data->current_item++;
return G_SOURCE_CONTINUE;
}
if (data->state == ACH_STATE_WAITING_FOR_SCHEMA_PARSER) {
if (g_main_gui->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 (!g_main_gui->schema_parser_future.get()) {
std::cerr << "Schema parsing failed, skipping icon downloads" << std::endl;
data->state = ACH_STATE_FINISHED;
return G_SOURCE_REMOVE;
}
data->state = ACH_STATE_DOWNLOADING_ICONS;
}
return G_SOURCE_CONTINUE;
}
if (data->state == ACH_STATE_DOWNLOADING_ICONS) {
// this could hang if we failed to parse all the icon download names
bool done_starting_downloads = (data->current_item == g_steam->get_achievements().size());
if (done_starting_downloads && (g_main_gui->achievement_icon_download_futures.size() == 0)) {
g_perfmon->log("Done downloading achievement icons");
data->state = ACH_STATE_FINISHED;
return G_SOURCE_REMOVE;
}
if ( !done_starting_downloads && (g_main_gui->outstanding_icon_downloads < MAX_OUTSTANDING_ICON_DOWNLOADS)) {
// Fire off a new download thread
std::string id = g_steam->get_achievements()[data->current_item].id;
std::string icon_download_name = g_steam->m_icon_download_names[id];
// Assuming it returns empty string on failing to lookup
if (icon_download_name.empty()) {
std::cerr << "Failed to lookup achievement icon name: " << id << std::endl;
} else {
g_main_gui->achievement_icon_download_futures.insert(std::make_pair(
id, std::async(std::launch::async, [id, icon_download_name]{g_steam->refresh_achievement_icon(id, icon_download_name);})));
g_main_gui->outstanding_icon_downloads++;
}
data->current_item++;
// continue on to service a thread if it's finished
}
for (auto const& [id, this_future] : g_main_gui->achievement_icon_download_futures) {
if (this_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
g_main_gui->refresh_achievement_icon(g_steam->m_app_id, id);
g_main_gui->achievement_icon_download_futures.erase(id);
g_main_gui->outstanding_icon_downloads--;
// let's only process one at a time
return G_SOURCE_CONTINUE;
}
}
// return G_SOURCE_CONTINUE;
}
// Should never reach here
return G_SOURCE_CONTINUE;
}
// => load_achievements_idle
static void
finish_load_achievements (gpointer data_)
{
IdleData *data = (IdleData *)data_;
g_perfmon->log("Achievements retrieved");
g_free(data);
g_main_gui->show_no_achievements_found_placeholder();
g_main_gui->m_achievement_refresh_lock.unlock();
}
// => finish_load_achievements
void
populate_achievements() {
if (g_main_gui->m_achievement_refresh_lock.try_lock()) {
IdleData *data = g_new(IdleData, 1);
data->current_item = 0;
data->state = ACH_STATE_STARTED;
g_main_gui->outstanding_icon_downloads = 0;
g_main_gui->reset_achievements_list();
g_main_gui->show_fetch_achievements_placeholder();
g_idle_add_full (G_PRIORITY_LOW,
load_achievements_idle,
data,
finish_load_achievements);
} else {
std::cerr << "Not launching game/refreshing achievements because a refresh is already in progress" << std::endl;
}
}
// => populate_achievements
@ -88,31 +216,31 @@ extern "C"
* main loop to these latencies and potentially make it laggy.
*/
static gboolean
load_items_idle (gpointer data_)
load_apps_idle (gpointer data_)
{
IdleData *data = (IdleData *)data_;
if (data->state == STATE_STARTED) {
if (data->state == APPS_STATE_STARTED) {
g_main_gui->reset_game_list();
g_perfmon->log("Starting library parsing.");
g_main_gui->owned_apps_future = std::async(std::launch::async, []{g_steam->refresh_owned_apps();});
data->state = STATE_WAITING_FOR_OWNED_APPS;
data->state = APPS_STATE_WAITING_FOR_OWNED_APPS;
return G_SOURCE_CONTINUE;
}
if (data->state == STATE_WAITING_FOR_OWNED_APPS) {
if (data->state == APPS_STATE_WAITING_FOR_OWNED_APPS) {
if (g_main_gui->owned_apps_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
g_perfmon->log("Done retrieving and filtering owned apps");
data->state = STATE_LOADING_GUI;
data->state = APPS_STATE_LOADING_GUI;
}
return G_SOURCE_CONTINUE;
}
if (data->state == STATE_LOADING_GUI) {
if (data->state == APPS_STATE_LOADING_GUI) {
if (data->current_item == g_steam->get_subscribed_apps().size()) {
g_perfmon->log("Done adding apps to GUI");
g_main_gui->confirm_game_list();
data->state = STATE_DOWNLOADING_ICONS;
data->state = APPS_STATE_DOWNLOADING_ICONS;
data->current_item = 0;
return G_SOURCE_CONTINUE;
}
@ -123,7 +251,7 @@ extern "C"
return G_SOURCE_CONTINUE;
}
if (data->state == STATE_DOWNLOADING_ICONS) {
if (data->state == APPS_STATE_DOWNLOADING_ICONS) {
// This must occur after the main gui game_list is
// complete, otherwise we might have concurrent
// access and modification of the GUI's game_list
@ -131,9 +259,9 @@ extern "C"
bool done_starting_downloads = (data->current_item == g_steam->get_subscribed_apps().size());
// Make sure we're done starting all downloads and finshed with outstanding downloads
if (done_starting_downloads && (g_main_gui->icon_download_futures.size() == 0)) {
g_perfmon->log("Done downloading icons");
data->state = STATE_FINISHED;
if (done_starting_downloads && (g_main_gui->app_icon_download_futures.size() == 0)) {
g_perfmon->log("Done downloading app icons");
data->state = APPS_STATE_FINISHED;
return G_SOURCE_REMOVE;
}
@ -151,7 +279,7 @@ extern "C"
if ( !done_starting_downloads && (g_main_gui->outstanding_icon_downloads < MAX_OUTSTANDING_ICON_DOWNLOADS)) {
// Fire off a new download thread
Game_t app = g_steam->get_subscribed_apps()[data->current_item];
g_main_gui->icon_download_futures.insert(std::make_pair(app.app_id, std::async(std::launch::async, g_steam->refresh_icon, app.app_id)));
g_main_gui->app_icon_download_futures.insert(std::make_pair(app.app_id, std::async(std::launch::async, g_steam->refresh_app_icon, app.app_id)));
g_main_gui->outstanding_icon_downloads++;
data->current_item++;
@ -160,14 +288,14 @@ extern "C"
// Try to find a thread that is finished. Only process at most 1 per GTK main loop.
// The max time this takes to traverse is controlled by the size of the
// icon_download_futures size, which is controlled by MAX_ICON_DOWNLOADS.
// app_icon_download_futures size, which is controlled by MAX_ICON_DOWNLOADS.
// Increasing this could lead to GUI stutter if it needs to traverse a large map,
// although the map has logarithmic traversal and update complexity.
for (auto const& [app_id, this_future] : g_main_gui->icon_download_futures) {
for (auto const& [app_id, this_future] : g_main_gui->app_icon_download_futures) {
if (this_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
// TODO: remove the app if it has a bad icon? (because then it's mostly likely not a game)
g_main_gui->refresh_app_icon(app_id);
g_main_gui->icon_download_futures.erase(app_id);
g_main_gui->app_icon_download_futures.erase(app_id);
g_main_gui->outstanding_icon_downloads--;
// let's only process one at a time
return G_SOURCE_CONTINUE;
@ -180,19 +308,19 @@ extern "C"
// Should never reach here
return G_SOURCE_CONTINUE;
}
// => load_items_idle
// => load_apps_idle
/* the finish function */
static void
finish_load_items (gpointer data_)
finish_load_apps (gpointer data_)
{
IdleData *data = (IdleData *)data_;
g_perfmon->log("Library parsed.");
g_free(data);
g_main_gui->m_game_refresh_lock.unlock();
g_main_gui->show_no_games_found_placeholder();
g_main_gui->m_game_refresh_lock.unlock();
}
// => finish_load_items
// => finish_load_apps
void
on_about_button_clicked() {
@ -207,20 +335,18 @@ extern "C"
void
on_refresh_games_button_clicked() {
if (g_main_gui->m_game_refresh_lock.try_lock()) {
IdleData *data;
data = g_new(IdleData, 1);
IdleData *data = g_new(IdleData, 1);
data->current_item = 0;
data->state = STATE_STARTED;
data->state = APPS_STATE_STARTED;
g_main_gui->outstanding_icon_downloads = 0;
g_main_gui->show_fetch_games_placeholder();
// Use low priority so we don't block showing the main window
// This allows the main window to show up immediately
g_idle_add_full (G_PRIORITY_LOW,
load_items_idle,
load_apps_idle,
data,
finish_load_items);
finish_load_apps);
} else {
std::cerr << "Not refreshing games because a refresh is already in progress" << std::endl;
}
@ -272,9 +398,8 @@ extern "C"
}
// => on_achievement_search_changed
void
void
on_game_row_activated(GtkListBox *box, GtkListBoxRow *row) {
AppId_t appId = g_main_gui->get_corresponding_appid_for_row(row);
if ( appId == 0 ) {
@ -286,18 +411,9 @@ extern "C"
return;
}
// Currently this doesn't actually show the fetch_achievements_placeholder
// because the thread gets blocked behind populate_achievements and gtk_main
// never gets a chance to run and refresh the window before it's replaced
// with achievement rows.
// So TODO: fire this populate_achievements in a different thread to not
// block main thread?
g_main_gui->show_fetch_achievements_placeholder();
g_main_gui->switch_to_achievement_page();
g_steam->launch_game(appId);
populate_achievements();
g_main_gui->show_no_achievements_found_placeholder();
}
// => on_game_row_activated

View File

@ -11,13 +11,22 @@ extern "C"
* source: https://www.bassi.io/pages/lazy-loading/
*/
/* states in the GUI-loading FSM */
/* states in the GUI-loading FSMs */
enum {
STATE_STARTED, /* start state */
STATE_WAITING_FOR_OWNED_APPS, /* waiting for owned apps thread to finish */
STATE_LOADING_GUI, /* feeding game rows to the model */
STATE_DOWNLOADING_ICONS, /* fire off icon downloads (to be added to the model) */
STATE_FINISHED /* finish state */
APPS_STATE_STARTED, /* start state */
APPS_STATE_WAITING_FOR_OWNED_APPS, /* waiting for owned apps thread to finish */
APPS_STATE_LOADING_GUI, /* feeding game rows to the model */
APPS_STATE_DOWNLOADING_ICONS, /* fire off icon downloads (to be added to the model) */
APPS_STATE_FINISHED /* finish state */
};
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 */
};
/* data to be passed to the idle handler */

View File

@ -67,11 +67,6 @@ encode_achievement(yajl_gen handle, Achievement_t achievement) {
std::cerr << "failed to make json" << std::endl;
}
yajl_gen_string_wrap(handle, ICON_STR);
if (yajl_gen_integer(handle, achievement.icon_handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
yajl_gen_string_wrap(handle, ACHIEVED_STR);
if (yajl_gen_bool(handle, achievement.achieved) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
@ -132,7 +127,6 @@ decode_achievements(std::string response) {
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 };
const char * icon_path[] = { ICON_STR, (const char*)0 };
const char * achieved_path[] = { ACHIEVED_STR, (const char*)0 };
const char * hidden_path[] = { HIDDEN_STR, (const char*)0 };
@ -181,15 +175,6 @@ decode_achievements(std::string response) {
}
achievements[i].global_achieved_rate = YAJL_GET_DOUBLE(cur_val);
cur_val = yajl_tree_get(cur_node, icon_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
}
if (!YAJL_IS_INTEGER(cur_val)) {
std::cerr << "integer parsing error" << std::endl;
}
achievements[i].icon_handle = YAJL_GET_INTEGER(cur_val);
// why is bool parsing weird
cur_val = yajl_tree_get(cur_node, achieved_path, yajl_t_any);
if (cur_val == NULL) {

View File

@ -45,6 +45,7 @@ main(int argc, char *argv[])
g_perfmon = new PerfMon();
gtk_init(&argc, &argv);
g_steam = MySteam::get_instance();
g_steamclient = new MySteamClient();
if (getenv("XDG_CACHE_HOME")) {
@ -62,7 +63,6 @@ main(int argc, char *argv[])
g_runtime_folder = g_cache_folder;
}
g_steam = MySteam::get_instance();
g_main_gui = new MainPickerWindow();
g_perfmon->log("Globals initialized.");

View File

@ -161,8 +161,6 @@ MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
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_handle = stats_api->GetAchievementIcon(pchName);
//m_achievement_list[i].icon_handle = 0; // TODO
}
} else {

View File

@ -32,7 +32,6 @@ struct Achievement_t {
std::string desc;
std::string id;
float global_achieved_rate;
int icon_handle; //0 : incorrect, error occurred, RTFM
bool achieved;
bool hidden;
eAchievementSpecial special;

View File

@ -13,7 +13,6 @@
#define DESC_STR "DESC"
#define ID_STR "ID"
#define RATE_STR "RATE"
#define ICON_STR "ICON"
#define ACHIEVED_STR "ACHIEVED"
#define HIDDEN_STR "HIDDEN"

292
src/types/KeyValue.cpp Normal file
View File

@ -0,0 +1,292 @@
/* Copyright (c) 2017 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.
*/
// This code is ported to C++ from gibbed's original Steam Achievement Manager.
// The original SAM is available at https://github.com/gibbed/SteamAchievementManager
// To comply with copyright, the above license is included.
#include "KeyValue.h"
#include <strings.h>
// Stream helper functions
int8_t read_value_u8(std::istream * is) {
return static_cast<int8_t>(is->get());
}
int32_t read_value_s32(std::istream * is) {
char buf[4];
is->read(buf, 4);
return *reinterpret_cast<int32_t*>(buf);
}
uint32_t read_value_u32(std::istream * is) {
char buf[4];
is->read(buf, 4);
return *reinterpret_cast<uint32_t*>(buf);
}
uint64_t read_value_u64(std::istream * is) {
char buf[8];
is->read(buf, 8);
return *reinterpret_cast<uint64_t*>(buf);
}
float read_value_f32(std::istream * is) {
char buf[4];
is->read(buf, 4);
return *reinterpret_cast<float*>(buf);
}
std::string read_string(std::istream * is) {
char buf[256];
// Even in unicode, NULL terminator will terminate the string.
// C++ actually automatically interprets an array of bytes
// as unicode if there are unicode encodings in it,
// so no special modification is needed.
// Eat the string and NULL terminator with getline.
is->getline( buf, 256, L'\0');
// NULL is automatically appended
return std::string(buf);
}
// returns NULL 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;
}
KeyValue* select_child = NULL;
for (auto child : this->children) {
if (strcasecmp(child->name.c_str(), key.c_str()) == 0) {
select_child = child;
}
}
return select_child;
}
KeyValue* KeyValue::get2(std::string key1, std::string key2) {
auto a = this->get(key1);
if (a == NULL) {
return NULL;
}
auto b = a->get(key2);
if (b == NULL) {
return NULL;
}
return b;
}
std::string KeyValue::as_string(std::string default_value) {
if (this->valid == false) {
return default_value;
}
if (!this->value.has_value()) {
return default_value;
}
// I don't think support for any other types is needed right now,
// but if it is, fail hard to avoid complications
if (this->value.type() != typeid(std::string)) {
std::cout << "Stats parser encountered fatal error!" << std::endl;
std::cout << "as_string attempted on non-string type" << std::endl;
std::cout << "exiting now to avoid complications" << std::endl;
exit(EXIT_FAILURE);
}
return std::any_cast<std::string>(this->value);
}
int KeyValue::as_integer(int default_value) {
if (this->valid == false) {
return default_value;
}
if (!this->value.has_value()) {
return default_value;
}
switch (this->type) {
case KeyValueType::String:
case KeyValueType::WideString:
{
// eh exception handling
try {
return std::stoi(std::any_cast<std::string>(this->value));
} catch (std::exception& e) {
return default_value;
}
}
case KeyValueType::Int32:
{
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);
break;
}
case KeyValueType::Color:
case KeyValueType::Pointer:
{
break;
}
default:
{
std::cout << "Invalid type referenced in stats parser!" << std::endl;
return default_value;
}
}
return default_value;
}
KeyValue* KeyValue::load_as_binary(std::string path) {
std::filebuf fb;
if (!fb.open(path, std::ios::in)) {
return NULL;
}
std::istream is(&fb);
auto kv = new KeyValue();
if (!kv->read_as_binary(&is)) {
delete kv;
kv = NULL;
}
fb.close();
return kv;
};
bool KeyValue::read_as_binary(std::istream* is) {
while (true) {
KeyValueType type = static_cast<KeyValueType>(is->get());
if (type == KeyValueType::End) {
break;
}
auto current = new KeyValue();
current->type = type;
current->name = read_string(is);
switch (type)
{
case KeyValueType::None:
{
current->read_as_binary(is);
break;
}
case KeyValueType::String:
{
current->valid = true;
current->value = read_string(is);
break;
}
case KeyValueType::WideString:
{
std::cout << "Stats parser encountered invalid unsupported wide string!" << std::endl;
delete current;
return false;
}
case KeyValueType::Int32:
{
current->valid = true;
current->value = read_value_s32(is);
break;
}
case KeyValueType::UInt64:
{
current->valid = true;
current->value = read_value_u64(is);
break;
}
case KeyValueType::Float32:
{
current->valid = true;
current->value = read_value_f32(is);
break;
}
case KeyValueType::Color:
{
current->valid = true;
current->value = read_value_u32(is);
break;
}
case KeyValueType::Pointer:
{
current->valid = true;
current->value = read_value_u32(is);
break;
}
default:
{
std::cout << "Stats parser encountered invalid type!" << std::endl;
delete current;
return false;
}
}
this->children.push_back(current);
}
this->valid = true;
// Make sure the stream is ok before reading for EOF
if (!is->good()) {
return false;
}
// Then check that we're at the end
if (is->peek() != std::ifstream::traits_type::eof()) {
return false;
}
return true;
}

78
src/types/KeyValue.h Normal file
View File

@ -0,0 +1,78 @@
/* Copyright (c) 2017 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.
*/
// This code is ported to C++ from gibbed's original Steam Achievement Manager.
// The original SAM is available at https://github.com/gibbed/SteamAchievementManager
// To comply with copyright, the above license is included.
#pragma once
#include <fstream>
#include <iostream>
#include <vector>
#include <any>
enum class KeyValueType : unsigned char
{
None = 0,
String = 1,
Int32 = 2,
Float32 = 3,
Pointer = 4,
WideString = 5,
Color = 6,
UInt64 = 7,
End = 8
};
class KeyValue {
private:
// invalid doesn't map easily to C++,
// just use NULL for bad return of [] operator
public:
std::string name = "<root>";
KeyValueType type = KeyValueType::None;
std::any value;
bool valid = false;
std::vector<KeyValue*> children;
// TODO: ugh operators
//KeyValue* operator[](std::string key);
KeyValue* get(std::string key);
KeyValue* get2(std::string key1, std::string key2);
std::string as_string(std::string default_value);
int as_integer(int default_value);
// Other as_type function can be implemented as needed
static KeyValue* load_as_binary(std::string path);
bool read_as_binary(std::istream *is);
KeyValue() { valid = true;};
~KeyValue() {
for (auto child : children) {
delete child;
}
};
};

View File

@ -0,0 +1,138 @@
/* Copyright (c) 2017 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.
*/
// This code is ported to C++ from gibbed's original Steam Achievement Manager.
// The original SAM is available at https://github.com/gibbed/SteamAchievementManager
// To comply with copyright, the above license is included.
#include "KeyValue.h"
#include "UserStatType.h"
#include "../MySteam.h"
#include "../globals.h"
#include <strings.h>
// this has at least as much error checking as the original version
bool load_user_game_stats_schema() {
g_steam->m_icon_download_names.clear();
std::string appid_string = std::to_string(g_steam->m_app_id);
std::string schema_file = g_steam->get_steam_install_path() + "/appcache/stats/UserGameStatsSchema_" + appid_string + ".bin";
KeyValue* kv = KeyValue::load_as_binary(schema_file);
if (kv == NULL) {
return false;
}
auto stats = kv->get2(appid_string, "stats");
if (stats == NULL) {
delete kv;
return false;
}
if (stats->valid == false || stats->children.size() == 0) {
delete kv;
return false;
}
for (auto stat : stats->children) {
if (stat->valid == false) {
continue;
}
int rawType = stat->get("type_int")->valid
? stat->get("type_int")->as_integer(0)
: stat->get("type")->as_integer(0);
UserStatType type = static_cast<UserStatType>(rawType);
switch (type)
{
case UserStatType::Invalid:
{
break;
}
case UserStatType::Integer:
{
// don't care currently
break;
}
case UserStatType::Float:
case UserStatType::AverageRate:
{
// don't care currently
break;
}
case UserStatType::Achievements:
case UserStatType::GroupAchievements:
{
if (stat->children.size() == 0) {
continue;
}
for (auto bits : stat->children) {
if (strcasecmp(bits->name.c_str(), "bits") != 0) {
continue;
}
if (bits->valid == false || bits->children.size() == 0) {
continue;
}
for (auto bit : bits->children) {
// don't care about the rest for now
auto id_kv = bit->get("name");
if (id_kv == NULL) {
std::cerr << "Failed to parse achievement id" << std::endl;
continue;
}
// just get regular icon for now, not icon_gray
auto icon_kv = bit->get2("display", "icon");
if (icon_kv == NULL) {
std::cerr << "Failed to parse achievement icon" << std::endl;
continue;
}
// inject into a map for later extraction and icon downloading
g_steam->m_icon_download_names.insert(std::make_pair(id_kv->as_string(""), icon_kv->as_string("")));
}
}
break;
}
default:
{
std::cerr << "invalid stat type" << std::endl;
delete kv;
return false;
}
}
}
delete kv;
return true;
}

View File

@ -0,0 +1,34 @@
/* Copyright (c) 2017 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.
*/
// This code is ported to C++ from gibbed's original Steam Achievement Manager.
// The original SAM is available at https://github.com/gibbed/SteamAchievementManager
// To comply with copyright, the above license is included.
#pragma once
/**
* Parse the schema file and store information in g_steam
* For now, just parse out icon download information.
* This has the potential to parse out much more info as needed.
*/
bool load_user_game_stats_schema();

37
src/types/UserStatType.h Normal file
View File

@ -0,0 +1,37 @@
/* Copyright (c) 2017 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.
*/
// This code is ported to C++ from gibbed's original Steam Achievement Manager.
// The original SAM is available at https://github.com/gibbed/SteamAchievementManager
// To comply with copyright, the above license is included.
#pragma once
enum class UserStatType : unsigned char
{
Invalid = 0,
Integer = 1,
Float = 2,
AverageRate = 3,
Achievements = 4,
GroupAchievements = 5,
};