mirror of
https://github.com/zebrajr/SamRewritten.git
synced 2025-12-06 12:19:51 +01:00
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:
parent
eab5ee6b6a
commit
7ff28cf5d8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
1769
src/common/lodepng.h
1769
src/common/lodepng.h
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
|
||||
populate_achievements() {
|
||||
// Get_achievements from game server
|
||||
std::vector<Achievement_t> achievements = g_steam->get_achievements();
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// => 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;
|
||||
}
|
||||
|
||||
for( Achievement_t achievement : achievements ) {
|
||||
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;
|
||||
}
|
||||
|
||||
g_main_gui->confirm_achievement_list();
|
||||
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;
|
||||
}
|
||||
|
|
@ -274,7 +400,6 @@ extern "C"
|
|||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
292
src/types/KeyValue.cpp
Normal 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
78
src/types/KeyValue.h
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
138
src/types/UserGameStatsSchemaParser.cpp
Normal file
138
src/types/UserGameStatsSchemaParser.cpp
Normal 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;
|
||||
}
|
||||
34
src/types/UserGameStatsSchemaParser.h
Normal file
34
src/types/UserGameStatsSchemaParser.h
Normal 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
37
src/types/UserStatType.h
Normal 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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user