Implement remainder of JSON achievement retrieval

Implement the rest of the logic needed for retrieving
achievements via a JSON interface. Use YAJL for this
encoding and decoding achievements and requests.
Pull out most of the YAJL functionality into YAJL helpers.
The YAJL wrapping and abstraction can still be done better,
but now at least a good start is in place.

Upgrade Achievement_t to use C++ types
Delete GameEmulator.h/c since the sockets are feature-complete enough
Next commit will implement storing achievements via JSON interface.
This commit is contained in:
William Pierce 2019-07-31 01:58:13 -07:00
parent c6c1aaee9e
commit 6fe0acfbad
11 changed files with 381 additions and 767 deletions

View File

@ -1,435 +0,0 @@
#include "GameEmulator.h"
#include <csignal>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "common/functions.h"
#include "globals.h"
#include "gui/MainPickerWindow.h"
/****************************
* SIGNAL CALLBACKS
****************************/
/**
* Used by the child process when the parent tells him to
* stop the steam app. The child process will die.
*/
void
handle_sigterm(int signum) {
SteamAPI_Shutdown();
exit(EXIT_SUCCESS);
}
/**
* Used by the parent process to remove the zombie process
* of the child, once it terminated
*/
void
handle_sigchld(int signum) {
pid_t pid;
GameEmulator* emulator = GameEmulator::get_instance();
while (true) {
pid = waitpid(WAIT_ANY, NULL, WNOHANG);
if (pid == 0)
return;
else if (pid == -1)
return;
else {
std::cerr << "Steam game terminated." << std::endl;
emulator->m_son_pid = -1;
}
}
}
/**
* When the parent receives SIGUSR1, it will read the pipe,
* and if everything goes well, it should fill the member
* achievements list, and the achievement count.
* Once done, will update the view
*
* This one might need a little more documentation on the technical side.
* This method is only received on the parent process. The son,
* will retrieve the stats and achievements from steam, and once it is done,
* it will send a SIGUSR1 to the parent back. The parent will then save the
* new data, and update the view accordingly.
*
* TODO: Check for errors
*/
void handle_sigusr1_parent(int signum) {
GameEmulator *inst = GameEmulator::get_instance();
read_count(inst->m_pipe[0], &inst->m_achievement_count, sizeof(unsigned));
if (inst->m_achievement_list != nullptr) {
free(inst->m_achievement_list);
inst->m_achievement_list = nullptr;
}
inst->m_achievement_list = (Achievement_t*)malloc(inst->m_achievement_count * sizeof(Achievement_t));
if (!inst->m_achievement_list) {
std::cerr << "ERROR: could not allocate memory." << std::endl;
exit(EXIT_FAILURE);
}
for (unsigned i = 0; i < inst->m_achievement_count; i++) {
read_count(inst->m_pipe[0], &(inst->m_achievement_list[i]), sizeof(Achievement_t));
}
inst->update_view();
}
/**
* We are the child and we will receive data/stats of achievements to unlock/relock
* then auto-commit changes and update the parent. Coodinating based on the number
* of changes avoids potentially losing signals because of multiple being in flight.
*
* Data will have this shitty format:
* - 1 unsigned for number of changes
* - 1 char, 'a' for achievement, 's' for "stat"
* - 1 unsigned int, 0 => locked, 1 => unlocked, or the stat progression
* - The length of MAX_ID_LENGTH to get the achievement ID
*
* If someone wants to update this repo, maybe IPC is too low level for what I want
* to achieve. This doesn't require super fast speed or anything, maybe see if
* TCP server is easier to use. Or at least one type of interfacing because this code
* gets really ugly over time, bad practices become usual.
*/
void handle_sigusr1_child(int signum) {
GameEmulator *inst = GameEmulator::get_instance();
ISteamUserStats *stats_api = SteamUserStats();
int* pipe = inst->m_pipe;
char type;
// Read number of changes
unsigned num_changes;
read_count(pipe[0], &num_changes, sizeof(unsigned));
for (unsigned i = 0; i < num_changes; i++)
{
read_count(pipe[0], &type, sizeof(char));
if (type == 'a') {
// We want to edit an achievement
unsigned value;
char achievement_id[MAX_ACHIEVEMENT_ID_LENGTH];
read_count(pipe[0], &value, sizeof(unsigned));
read_count(pipe[0], &achievement_id, MAX_ACHIEVEMENT_ID_LENGTH * sizeof(char));
if (value == 0) {
// We want to relock an achievement
if (!stats_api->ClearAchievement(achievement_id)) {
std::cerr << "Relocking achievement " << achievement_id << " failed" << std::endl;
//keep going so we don't corrupt the pipe
} else {
std::cerr << "Relocked achievement " << achievement_id << std::endl;
}
} else {
// We want to unlock an achievement
if (!stats_api->SetAchievement(achievement_id)) {
std::cerr << "Unlocking achievement " << achievement_id << " failed " << std::endl;
//keep going so we don't corrupt the pipe
} else {
std::cerr << "Unlocked achievement " << achievement_id << std::endl;
}
}
} else if (type == 's') {
// We want to edit a stat
// TODO
}
}
// Auto-commit and auto-update after we receive everything
if (!stats_api->StoreStats()) {
std::cerr << "Committing changes failed" << std::endl;
}
// After the child changes achievements/stats, it will retrieve all achievements,
// and send a SIGUSR1 signal to the parent when done, and start writing
// to the pipe.
std::cerr << "Child is updating parent" << std::endl;
inst->retrieve_achievements();
}
/*********************************
* CLASS METHODS DEFINITION
********************************/
GameEmulator::GameEmulator() :
m_CallbackUserStatsReceived( this, &GameEmulator::OnUserStatsReceived ),
m_achievement_list( nullptr ),
m_son_pid( -1 ),
m_have_stats_been_requested( false )
{
}
// => Constructor
GameEmulator*
GameEmulator::get_instance() {
static GameEmulator me;
return &me;
}
// => get_instance
bool
GameEmulator::init_app(const std::string& app_id) {
if(m_son_pid > 0) {
std::cerr << "Warning: trying to initialize a Steam App while one is already running." << std::endl;
return false;
}
// Create the pipe
// We never close it because they are bidirectional (I know it's ugly)
if( pipe(m_pipe) == -1 ) {
std::cerr << "Could not create a pipe. Exitting." << std::endl;
std::cerr << "errno: " << errno << std::endl;
exit(EXIT_FAILURE);
}
pid_t pid;
if((pid = fork()) == 0) {
//Son's process
setenv("SteamAppId", app_id.c_str(), 1);
if( !SteamAPI_Init() ) {
std::cerr << "An error occurred launching the Steam API. Aborting." << std::endl;
std::cerr << "Make sure you are trying to run an app you own, and running with lauch.sh" << std::endl;
exit(EXIT_FAILURE);
}
signal(SIGTERM, handle_sigterm);
// Communicate game stats to parent, and
// read from parents stats to modify
signal(SIGUSR1, handle_sigusr1_child);
retrieve_achievements();
for(;;) {
SteamAPI_RunCallbacks();
sleep(1);
}
}
else if (pid == -1) {
std::cerr << "An error occurred while forking. Exitting." << std::endl;
exit(EXIT_FAILURE);
}
else {
//Main process
signal(SIGCHLD, handle_sigchld);
signal(SIGUSR1, handle_sigusr1_parent);
m_son_pid = pid;
}
//Only a successful parent will reach this
return true;
}
// => init_app
bool
GameEmulator::kill_running_app() {
if(m_son_pid > 0) {
kill(m_son_pid, SIGTERM);
free(m_achievement_list);
m_achievement_list = nullptr;
// We will set the pid back to -1 when son's death is confirmed
return true;
}
else {
if (g_main_gui != NULL)
std::cerr << "Warning: trying to kill the Steam Game while it's not running." << std::endl;
return true;
}
return true;
}
// => kill_running app
void
GameEmulator::retrieve_achievements() {
if (!m_have_stats_been_requested) {
m_have_stats_been_requested = true;
ISteamUserStats *stats_api = SteamUserStats();
if (!stats_api->RequestCurrentStats()) {
std::cerr << "ERROR: User not logged in, exiting" << std::endl;
exit(EXIT_FAILURE);
}
} else {
std::cerr << "WARNING: Stats have already been requested" << std::endl;
}
}
// => retrieve_achievements
void
GameEmulator::update_view() {
for(unsigned i = 0; i < m_achievement_count; i++) {
g_main_gui->add_to_achievement_list(m_achievement_list[i]);
}
g_main_gui->confirm_stats_list();
}
// => update_view
/**
* This method must only be called on the parent process.
* It reset the GUI window in anticipation of the child
* process sending it new information on the achievements
* via handle_sigusr1_parent
*
*/
void
GameEmulator::update_data_and_view() {
// Must be run by the parent
if(m_son_pid > 0) {
g_main_gui->reset_achievements_list();
g_main_gui->confirm_stats_list();
// Child will autoupdate the parent after committing changes
} else {
std::cerr << "Could not update data & view, no child found." << std::endl;
}
}
// => update_data_and_view
bool
GameEmulator::send_num_changes(unsigned num_changes) const {
// We assume the son process is already running
// Write the number of changes
write(m_pipe[1], &num_changes, sizeof(unsigned));
// Send it a signal after buffering the number of changes
kill(m_son_pid, SIGUSR1);
return false; // Yeah error handling? Maybe later (TODO)
}
// => unlock_achievement
/**
* The parent process requests the son process to unlock an
* achievement. So this code will be executed in the parent process,
* so send a message to the son, associated with an achievement ID
*/
bool
GameEmulator::unlock_achievement(const char* ach_api_name) const {
// We assume the son process is already running
static const unsigned unlock_state = 1;
// Write "a1" (for achievement unlock) then the achievement id
write(m_pipe[1], "a", sizeof(char));
write(m_pipe[1], &unlock_state, sizeof(unsigned));
write(m_pipe[1], ach_api_name, MAX_ACHIEVEMENT_ID_LENGTH * sizeof(char));
return false; // Yeah error handling? Maybe later (TODO)
}
// => unlock_achievement
bool
GameEmulator::relock_achievement(const char* ach_api_name) const {
// We assume the son process is already running
static const unsigned unlock_state = 0;
// Write "a1" (for achievement unlock) then the achievement id
write(m_pipe[1], "a", sizeof(char));
write(m_pipe[1], &unlock_state, sizeof(unsigned));
write(m_pipe[1], ach_api_name, MAX_ACHIEVEMENT_ID_LENGTH * sizeof(char));
return false; // Yeah error handling? Maybe later (TODO)
}
// => relock_achievement
//TODO: send stats
/*****************************************
* STEAM API CALLBACKS BELOW
****************************************/
/**
* Retrieves all achievements data, then pipes the data to the
* parent process.
*/
void
GameEmulator::OnUserStatsReceived(UserStatsReceived_t *callback) {
// Check if we received the values for the good app
if (std::string(getenv("SteamAppId")) == std::to_string(callback->m_nGameID)) {
if ( k_EResultOK == callback->m_eResult ) {
ISteamUserStats *stats_api = SteamUserStats();
// ==============================
// RETRIEVE IDS
// ==============================
const unsigned num_ach = stats_api->GetNumAchievements();
if (num_ach == 0) {
std::cerr << "No achievements for current game" << std::endl;
}
if (m_achievement_list != nullptr) {
free(m_achievement_list);
m_achievement_list = nullptr;
}
m_achievement_list = (Achievement_t*)malloc(num_ach * sizeof(Achievement_t));
for (unsigned i = 0; i < num_ach ; i++) {
// TODO: strncpy is slow, because it fills the remaining space with NULLs
// This is last stage optimisation but, could have used strcpy, or sprintf,
// making sure strings are NULL terminated
// see "man strncpy" for a possible implementation
strncpy(
m_achievement_list[i].id,
stats_api->GetAchievementName(i),
MAX_ACHIEVEMENT_ID_LENGTH);
strncpy(
m_achievement_list[i].name,
stats_api->GetAchievementDisplayAttribute(m_achievement_list[i].id, "name"),
MAX_ACHIEVEMENT_NAME_LENGTH);
strncpy(
m_achievement_list[i].desc,
stats_api->GetAchievementDisplayAttribute(m_achievement_list[i].id, "desc"),
MAX_ACHIEVEMENT_DESC_LENGTH);
// TODO
// https://partner.steamgames.com/doc/api/ISteamUserStats#RequestGlobalAchievementPercentages
//stats_api->GetAchievementAchievedPercent(m_achievement_list[i].id, &(m_achievement_list[i].global_achieved_rate));
m_achievement_list[i].global_achieved_rate = 0;
stats_api->GetAchievement(m_achievement_list[i].id, &(m_achievement_list[i].achieved));
m_achievement_list[i].hidden = (bool)strcmp(stats_api->GetAchievementDisplayAttribute( m_achievement_list[i].id, "hidden" ), "0");
m_achievement_list[i].icon_handle = stats_api->GetAchievementIcon( m_achievement_list[i].id );
}
//Tell parent that he must read
kill(getppid(), SIGUSR1);
//Start writing
write(m_pipe[1], &num_ach, sizeof(unsigned));
// We could send all the memory bloc at once, but the pipe buffer
// might not be big enough on some systems, so let's just loop
for(unsigned i = 0; i < num_ach; i++) {
write(m_pipe[1], &(m_achievement_list[i]), sizeof(Achievement_t));
}
m_have_stats_been_requested = false;
} else {
std::cerr << "Received stats for the game, but an error occurrred." << std::endl;
}
} else {
std::cerr << "Received stats for wrong game" << std::endl;
}
}
// => OnUserStatsReceived

View File

@ -1,106 +0,0 @@
#pragma once
#include <string>
#include "types/Achievement.h"
#include "../steam/steam_api.h"
/**
* This class will play the part of being the emulated app
* It is responsible for retrieving all stats and achievements
* for a give steam app id.
*
* Technically, it calls fork, and the child process will have
* the role of a steam app, that will retrieve all the data
* and pipe it to the parent process.
* It uses SIGUSR1, so don't be surprised if you get callbacks
* triggered if you use this signal somewhere else.
*/
class GameEmulator {
public:
/**
* Singleton method to get the unique instance
*/
static GameEmulator* get_instance();
/**
* Starts the Steam app corresponding to the given app_id
* Will automatically call update_view
*/
bool init_app(const std::string& app_id);
/**
* Will stop the currently running Steam app, launched with
* init_app
*/
bool kill_running_app();
/**
* Will update the main view, adding all achievements to the
* list of achievements
*/
void update_view();
/**
* Will refetch data from the steam API.
* Will update the main view, adding all achievements to the
* list of achievements
*/
void update_data_and_view();
/**
* Send the number of changes to store to the child.
* The child will then wait for that many changes.
*/
bool send_num_changes(unsigned num_changes) const;
/**
* Will unlock the achivement given it's API name.
* Will return the value of SetAchievement.
* https://partner.steamgames.com/doc/api/ISteamUserStats#SetAchievement
*/
bool unlock_achievement(const char* ach_api_name) const;
/**
* Will relock the achivement given it's API name.
* Will return the value of ClearAchievement.
* https://partner.steamgames.com/doc/api/ISteamUserStats#ClearAchievement
*/
bool relock_achievement(const char* ach_api_name) const;
/**
* Will actually commit the stats and achievements previously
* changed locally to the server and cause achievement
* notifications to pop up
* https://partner.steamgames.com/doc/api/ISteamUserStats#StoreStats
*/
// Child inherently does this now
//bool commit_changes();
/**
* Steam API callback to handle the received stats and achievements
*/
STEAM_CALLBACK( GameEmulator, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived );
/**
* Prevent using the default constructor because we use the
* singleton pattern
*/
GameEmulator(GameEmulator const&) = delete;
void operator=(GameEmulator const&) = delete;
private:
void retrieve_achievements();
Achievement_t *m_achievement_list;
pid_t m_son_pid;
bool m_have_stats_been_requested;
int m_pipe[2];
unsigned m_achievement_count;
friend void handle_sigchld(int);
friend void handle_sigusr1_parent(int);
friend void handle_sigusr1_child(int);
GameEmulator();
~GameEmulator() {};
};

View File

@ -8,8 +8,8 @@
#include "types/Game.h"
#include "types/Actions.h"
#include "SteamAppDAO.h"
#include "GameEmulator.h"
#include "common/functions.h"
#include "common/yajlHelpers.h"
#define MAX_PATH 1000
@ -44,11 +44,8 @@ MySteam::get_instance() {
*/
bool
MySteam::launch_game(AppId_t appID) {
// Print an error if a game is already launched, maybe allow multiple games at the same time in the future?
// GameEmulator* emulator = GameEmulator::get_instance();
// //TODO if
// emulator->init_app(appID);
// Print an error if a game is already launched
// allow multiple games at the same time in the future via new window launching
if (m_ipc_socket != nullptr) {
std::cerr << "I will not launch the game as one is already running" << std::endl;
@ -72,9 +69,6 @@ MySteam::launch_game(AppId_t appID) {
*/
bool
MySteam::quit_game() {
// GameEmulator* emulator = GameEmulator::get_instance();
// return emulator->kill_running_app();
if (m_ipc_socket != nullptr) {
m_ipc_socket->kill_server();
delete m_ipc_socket;
@ -250,9 +244,9 @@ MySteam::refresh_icons() {
// => refresh_icons
std::vector<std::pair<std::string, bool>>
std::vector<Achievement_t>
MySteam::get_achievements() {
std::vector<std::pair<std::string, bool>> achs;
std::vector<Achievement_t> achievements;
std::string response;
const unsigned char * buf;
size_t len;
@ -262,88 +256,23 @@ MySteam::get_achievements() {
exit(0);
}
achs.clear();
achievements.clear();
// Maybe these MySteam functions should be moved to a MyGameClient.cpp?
//TODO encapsulate these into a json generator
// TODO: could push this handle generation handling down into a YAJL
// interfacing object
yajl_gen handle = yajl_gen_alloc(NULL);
yajl_gen_map_open(handle);
if (yajl_gen_string(handle, (const unsigned char *)SAM_ACTION_STR, strlen(SAM_ACTION_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
return achs;
}
if (yajl_gen_string(handle, (const unsigned char *)GET_ACHIEVEMENTS_STR, strlen(GET_ACHIEVEMENTS_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
return achs;
}
if (yajl_gen_map_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
return achs;
}
encode_request(handle, GET_ACHIEVEMENTS_STR);
yajl_gen_get_buf(handle, &buf, &len);
response = m_ipc_socket->request_response(std::string((const char*)buf));
yajl_gen_free(handle);
//parse response
yajl_val node = yajl_tree_parse(response.c_str(), NULL, 0);
achievements = decode_achievements(response);
if (node == NULL) {
std::cerr << "parsing error";
exit(EXIT_FAILURE);
}
const char * path1[] = { SAM_ACK_STR, (const char*)0 };
yajl_val v = yajl_tree_get(node, path1, yajl_t_string);
if (v == NULL || !YAJL_IS_STRING(v)) {
std::cerr << "failed to parse " << SAM_ACK_STR << std::endl;
exit(EXIT_FAILURE);
}
if (std::string(YAJL_GET_STRING(v)) != std::string(SAM_ACK_STR)) {
std::cerr << "failed to receive ack" << std::endl;
exit(EXIT_FAILURE);
}
const char * path2[] = { ACHIEVEMENT_LIST_STR, (const char*)0 };
v = yajl_tree_get(node, path2, yajl_t_array);
if (v == NULL) {
std::cerr << "parsing error" << std::endl;
exit(EXIT_FAILURE);
}
yajl_val *w = YAJL_GET_ARRAY(v)->values;
size_t array_len = YAJL_GET_ARRAY(v)->len;
for(unsigned i = 0; i < array_len; i++) {
const char * path3[] = { ACHIEVEMENT_NAME_STR, (const char*)0 };
const char * path4[] = { ACHIEVED_STR, (const char*)0 };
yajl_val cur_node = w[i];
yajl_val x = yajl_tree_get(cur_node, path3, yajl_t_string);
if (x == NULL) {
std::cerr << "parsing error" << std::endl;
exit(EXIT_FAILURE);
}
// why is bool parsing weird
yajl_val y = yajl_tree_get(cur_node, path4, yajl_t_any);
if (y == NULL) {
std::cerr << "parsing error" << std::endl;
exit(EXIT_FAILURE);
}
if (!YAJL_IS_TRUE(y) && !YAJL_IS_FALSE(y)) {
std::cerr << "bool parsing error" << std::endl;
exit(EXIT_FAILURE);
}
achs.push_back(std::pair<std::string, bool>(YAJL_GET_STRING(x), YAJL_IS_TRUE(y)));
}
return achs;
return achievements;
}
/**

View File

@ -90,7 +90,7 @@ public:
*
* TODO: maybe don't name this the same as GameServer::get_achievements?
*/
std::vector<std::pair<std::string, bool>> get_achievements();
std::vector<Achievement_t> get_achievements();
/**
* Adds a modification to be done on the launched app.

226
src/common/yajlHelpers.cpp Normal file
View File

@ -0,0 +1,226 @@
#include "yajlHelpers.h"
#include <string>
#include <vector>
#include <iostream>
#include <cstring>
#include <yajl/yajl_gen.h>
#include <yajl/yajl_tree.h>
#include "../types/Achievement.h"
#include "../types/Actions.h"
void yajl_gen_string_wrap(yajl_gen handle, const char * a) {
if (yajl_gen_string(handle, (const unsigned char *)a, strlen(a)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
void encode_request(yajl_gen handle, const char * request) {
if (yajl_gen_map_open(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
yajl_gen_string_wrap(handle, SAM_ACTION_STR);
yajl_gen_string_wrap(handle, request);
if (yajl_gen_map_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
std::string decode_request(std::string request) {
yajl_val node = yajl_tree_parse(request.c_str(), NULL, 0);
if (node == NULL) {
std::cerr << "Parsing error";
exit(EXIT_FAILURE);
}
const char * path[] = { SAM_ACTION_STR, (const char*)0 };
yajl_val v = yajl_tree_get(node, path, yajl_t_string);
if (v == NULL || !YAJL_IS_STRING(v)) {
std::cerr << "failed to get" << SAM_ACTION_STR << std::endl;
exit(EXIT_FAILURE);
}
return std::string (YAJL_GET_STRING(v));
}
/**
* Encode an individual achievement into a given YAJL handle
*/
void encode_achievement(yajl_gen handle, Achievement_t achievement) {
yajl_gen_string_wrap(handle, NAME_STR);
yajl_gen_string_wrap(handle, achievement.name.c_str());
yajl_gen_string_wrap(handle, DESC_STR);
yajl_gen_string_wrap(handle, achievement.desc.c_str());
yajl_gen_string_wrap(handle, ID_STR);
yajl_gen_string_wrap(handle, achievement.id.c_str());
yajl_gen_string_wrap(handle, RATE_STR);
if (yajl_gen_double(handle, (double)achievement.global_achieved_rate) != yajl_gen_status_ok) {
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;
}
yajl_gen_string_wrap(handle, HIDDEN_STR);
if (yajl_gen_bool(handle, achievement.hidden) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
/**
* Encode an achievement vector into a given YAJL handle
*/
void encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements) {
yajl_gen_string_wrap(handle, ACHIEVEMENT_LIST_STR);
if (yajl_gen_array_open(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
for (Achievement_t achievement : achievements) {
std::cout << "encoding achievement.id " << achievement.id << std::endl;
if (yajl_gen_map_open(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
encode_achievement(handle, achievement);
if (yajl_gen_map_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
if (yajl_gen_array_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
}
//parsing the array inline would not be nice, so just extract them all here
std::vector<Achievement_t> decode_achievements(std::string response) {
std::vector<Achievement_t> achievements;
//parse response
yajl_val node = yajl_tree_parse(response.c_str(), NULL, 0);
if (node == NULL) {
std::cerr << "parsing error";
exit(EXIT_FAILURE);
}
// TODO: separate out into decode_ack()
const char * ack_path[] = { SAM_ACK_STR, (const char*)0 };
yajl_val v = yajl_tree_get(node, ack_path, yajl_t_string);
if (v == NULL || !YAJL_IS_STRING(v)) {
std::cerr << "failed to parse " << SAM_ACK_STR << std::endl;
}
if (std::string(YAJL_GET_STRING(v)) != std::string(SAM_ACK_STR)) {
std::cerr << "failed to receive ack" << std::endl;
}
// dumb defines for required interface for yajl_tree
const char * list_path[] = { ACHIEVEMENT_LIST_STR, (const char*)0 };
const char * name_path[] = { NAME_STR, (const char*)0 };
const char * 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 };
v = yajl_tree_get(node, list_path, yajl_t_array);
if (v == NULL) {
std::cerr << "parsing error" << std::endl;
}
yajl_val *w = YAJL_GET_ARRAY(v)->values;
size_t array_len = YAJL_GET_ARRAY(v)->len;
achievements.clear();
achievements.resize(array_len);
for(unsigned i = 0; i < array_len; i++) {
yajl_val cur_node = w[i];
yajl_val cur_val;
// verification is done via the type argument to yajl_tree_get
// and via YAJL_IS_* checks if type alone isn't sufficient
cur_val = yajl_tree_get(cur_node, name_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
}
achievements[i].name = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, desc_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
}
achievements[i].desc = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, id_path, yajl_t_string);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
}
achievements[i].id = YAJL_GET_STRING(cur_val);
cur_val = yajl_tree_get(cur_node, rate_path, yajl_t_number);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
}
if (!YAJL_IS_DOUBLE(cur_val)) {
std::cerr << "double float parsing error" << std::endl;
}
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) {
std::cerr << "parsing error" << std::endl;
}
if (!YAJL_IS_TRUE(cur_val) && !YAJL_IS_FALSE(cur_val)) {
std::cerr << "bool parsing error" << std::endl;
}
achievements[i].achieved = YAJL_IS_TRUE(cur_val);
cur_val = yajl_tree_get(cur_node, hidden_path, yajl_t_any);
if (cur_val == NULL) {
std::cerr << "parsing error" << std::endl;
}
if (!YAJL_IS_TRUE(cur_val) && !YAJL_IS_FALSE(cur_val)) {
std::cerr << "bool parsing error" << std::endl;
}
achievements[i].achieved = YAJL_IS_TRUE(cur_val);
}
yajl_tree_free(node);
return achievements;
}

36
src/common/yajlHelpers.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <vector>
#include <yajl/yajl_gen.h>
#include <yajl/yajl_tree.h>
#include "../types/Achievement.h"
#include "../types/Actions.h"
/**
* Encode a string into a YAJL handle and
* handle error and coercion to C types for a string
*/
void yajl_gen_string_wrap(yajl_gen handle, const char * a);
/**
* Encode a request
*/
void encode_request(yajl_gen handle, const char * request);
/**
* Decode a request
*/
std::string decode_request(std::string request);
/**
* Encode an achievement vector into a given YAJL handle
* Encode an array at a time because decoding individual
* achievements in YAJL would be messy
*/
void encode_achievements(yajl_gen handle, std::vector<Achievement_t> achievements);
/**
* Decode the achievement vector from a json response
*/
std::vector<Achievement_t> decode_achievements(std::string response);

View File

@ -1,6 +1,9 @@
#include "GtkAchievementBoxRow.h"
#include "../MySteam.h"
#include "../globals.h"
#include <string>
#include <iostream>
extern "C"
{
@ -29,6 +32,9 @@ extern "C"
}
}
//keep this around until this function is refactored
#define MAX_ACHIEVEMENT_NAME_LENGTH 256
GtkAchievementBoxRow::GtkAchievementBoxRow(const Achievement_t& data)
:
m_data(data)
@ -46,7 +52,12 @@ m_data(data)
sprintf(ach_locked_text, "%s", "Locked");
pressed = FALSE;
}
sprintf(ach_title_text, "<b>%s</b>", data.name);
// remove when this function is refactored
if (data.name.length() > MAX_ACHIEVEMENT_NAME_LENGTH) {
std::cerr << "Overflow going to occur, aborting" << std::endl;
return;
}
sprintf(ach_title_text, "<b>%s</b>", data.name.c_str());
sprintf(ach_player_percent_text, "Achieved by %.1f%% of the players", data.global_achieved_rate);
@ -57,7 +68,7 @@ m_data(data)
GtkWidget *ach_pic = gtk_image_new_from_icon_name("gtk-missing-image", GTK_ICON_SIZE_DIALOG);
GtkWidget *title_desc_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkWidget *title_label = gtk_label_new("");
GtkWidget *desc_label = gtk_label_new(data.desc);
GtkWidget *desc_label = gtk_label_new(data.desc.c_str());
GtkWidget *more_info_button = gtk_menu_button_new();
GtkWidget *more_info_image = gtk_image_new_from_icon_name("gtk-about", GTK_ICON_SIZE_BUTTON);
GtkWidget *lock_unlock_button = gtk_toggle_button_new_with_label(ach_locked_text);

View File

@ -1,7 +1,6 @@
#include "gtk_callbacks.h"
#include <iostream>
#include "MainPickerWindow.h"
#include "../GameEmulator.h"
#include "../MySteam.h"
#include "../globals.h"
@ -27,30 +26,16 @@ extern "C"
std::cerr << "Saving stats and achievements." << std::endl;
const std::map<std::string, bool> pending_achs = g_steam->get_pending_ach_modifications();
const std::map<std::string, double> pending_stats = g_steam->get_pending_stat_modifications();
GameEmulator* emulator = GameEmulator::get_instance();
// Send the number of changes then send that many changes
const unsigned num_to_change = pending_achs.size(); //+ pending_stats.size();
emulator->send_num_changes(num_to_change);
//TODO:
//
//commit changes
//MySteam::commit_changes() //reset pending changes too?
//
// pull out the same game achievement population code from on_game_row_activated
// g_main_gui->reset_achievements_list();
// g_main_gui->confirm_stats_list();
/**
* TODO: Check for failures. But unlocking is done async because
* the son process has to deal with it.
*/
for (auto const& [key, val] : pending_achs) {
if(val) {
std::cout << "Unlocking " << key << std::endl;
emulator->unlock_achievement( key.c_str() );
g_steam->remove_modification_ach(key);
} else {
std::cout << "Relocking " << key << std::endl;
emulator->relock_achievement( key.c_str() );
g_steam->remove_modification_ach(key);
}
}
// Child will inherently udpate the parent after committing changes
emulator->update_data_and_view(); // This is async
}
// => on_store_button_clicked
@ -96,23 +81,14 @@ extern "C"
if( appId != 0 ) {
g_main_gui->switch_to_stats_page();
g_steam->launch_game(appId);
// get_achievements from game server
std::vector<std::pair<std::string, bool>> ach_list = g_steam->get_achievements();
// Get_achievements from game server
std::vector<Achievement_t> achievements = g_steam->get_achievements();
g_main_gui->reset_achievements_list();
//TODO: just pass in the array directly?
for(unsigned i = 0; i < ach_list.size(); i++) {
// For now, for compatibility reasons, just convert to an Achievement_t
// These two types will need to be unified
Achievement_t ach = { 0 };
strncpy(ach.id, ach_list[i].first.c_str(), MAX_ACHIEVEMENT_ID_LENGTH);
// incorrect
strncpy(ach.name, ach_list[i].first.c_str(), MAX_ACHIEVEMENT_NAME_LENGTH);
ach.achieved = ach_list[i].second;
g_main_gui->add_to_achievement_list(ach);
for(Achievement_t achievement : achievements) {
g_main_gui->add_to_achievement_list(achievement);
}
g_main_gui->confirm_stats_list();

View File

@ -2,6 +2,7 @@
#include <yajl/yajl_tree.h>
#include "MyGameSocket.h"
#include "../types/Actions.h"
#include "../common/yajlHelpers.h"
MyGameSocket::MyGameSocket(AppId_t appid) :
MyServerSocket(appid),
@ -13,23 +14,12 @@ m_CallbackUserStatsReceived( this, &MyGameSocket::OnUserStatsReceived )
std::string
MyGameSocket::process_request(std::string request) {
//
//TODO encapsulate these into a json parser?
yajl_val node = yajl_tree_parse(request.c_str(), NULL, 0);
//encoding this response is still tightly coupled to the
// logic in this function, so it's hard to push it to a helper
if (node == NULL) {
std::cerr << "Parsing error";
exit(EXIT_FAILURE);
}
std::string action = decode_request(request);
const char * path[] = { SAM_ACTION_STR, (const char*)0 };
yajl_val v = yajl_tree_get(node, path, yajl_t_string);
if (v == NULL || !YAJL_IS_STRING(v)) {
std::cerr << "failed to get" << SAM_ACTION_STR << std::endl;
exit(EXIT_FAILURE);
}
std::string action(YAJL_GET_STRING(v));
std::string ret;
const unsigned char * buf;
size_t len;
@ -39,53 +29,19 @@ MyGameSocket::process_request(std::string request) {
yajl_gen handle = yajl_gen_alloc(NULL);
yajl_gen_map_open(handle);
if (yajl_gen_string(handle, (const unsigned char *)SAM_ACK_STR, strlen(SAM_ACK_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
if (yajl_gen_string(handle, (const unsigned char *)SAM_ACK_STR, strlen(SAM_ACK_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
// generate ACK with same variable name and content
yajl_gen_string_wrap(handle, SAM_ACK_STR);
yajl_gen_string_wrap(handle, SAM_ACK_STR);
// TODO: change to enums? since it's JSON, using strings is necessary sometime
// switch (request_type) {
if (action == GET_ACHIEVEMENTS_STR) {
//case GET_ACHIEVEMENTS:
std::vector<Achievement_t> achievements = get_achievements();
// Steam api is launched in this context, other possible imlementation: game_utils->get_achievements()
if (yajl_gen_string(handle, (const unsigned char *)ACHIEVEMENT_LIST_STR, strlen(ACHIEVEMENT_LIST_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
if (yajl_gen_array_open(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
// append the achievements to the ack
for (Achievement_t achievement : achievements) {
std::cout << "achievement.id " << achievement.id << std::endl;
yajl_gen_map_open(handle);
if (yajl_gen_string(handle, (const unsigned char *)ACHIEVEMENT_NAME_STR, strlen(ACHIEVEMENT_NAME_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
if (yajl_gen_string(handle, (const unsigned char *)achievement.id, strlen(achievement.id)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
if (yajl_gen_string(handle, (const unsigned char *)ACHIEVED_STR, strlen(ACHIEVED_STR)) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
if (yajl_gen_bool(handle, achievement.achieved) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
yajl_gen_map_close(handle);
}
if (yajl_gen_array_close(handle) != yajl_gen_status_ok) {
std::cerr << "failed to make json" << std::endl;
}
// Steam api is launched in this context, other possible implementation: game_utils->get_achievements()
// Append the achievements to the ack
encode_achievements(handle, achievements);
//break;
} else if (action == STORE_ACHIEVEMENTS_STR) {
@ -114,8 +70,6 @@ MyGameSocket::process_request(std::string request) {
ret = std::string((const char*)buf);
yajl_gen_free(handle);
yajl_tree_free(node);
return ret;
}
@ -160,32 +114,23 @@ MyGameSocket::OnUserStatsReceived(UserStatsReceived_t *callback) {
m_achievement_list.resize(num_ach);
for (unsigned i = 0; i < num_ach ; i++) {
// TODO: strncpy is slow, because it fills the remaining space with NULLs
// This is last stage optimisation but, could have used strcpy, or sprintf,
// making sure strings are NULL terminated
// see "man strncpy" for a possible implementation
strncpy(
m_achievement_list[i].id,
stats_api->GetAchievementName(i),
MAX_ACHIEVEMENT_ID_LENGTH);
strncpy(
m_achievement_list[i].name,
stats_api->GetAchievementDisplayAttribute(m_achievement_list[i].id, "name"),
MAX_ACHIEVEMENT_NAME_LENGTH);
m_achievement_list[i].id = stats_api->GetAchievementName(i);
strncpy(
m_achievement_list[i].desc,
stats_api->GetAchievementDisplayAttribute(m_achievement_list[i].id, "desc"),
MAX_ACHIEVEMENT_DESC_LENGTH);
const char * pchName = m_achievement_list[i].id.c_str();
m_achievement_list[i].name = stats_api->GetAchievementDisplayAttribute(pchName, "name");
m_achievement_list[i].desc = stats_api->GetAchievementDisplayAttribute(pchName, "desc");
// TODO
// https://partner.steamgames.com/doc/api/ISteamUserStats#RequestGlobalAchievementPercentages
//stats_api->GetAchievementAchievedPercent(m_achievement_list[i].id, &(m_achievement_list[i].global_achieved_rate));
m_achievement_list[i].global_achieved_rate = 0;
stats_api->GetAchievement(m_achievement_list[i].id, &(m_achievement_list[i].achieved));
m_achievement_list[i].hidden = (bool)strcmp(stats_api->GetAchievementDisplayAttribute( m_achievement_list[i].id, "hidden" ), "0");
m_achievement_list[i].icon_handle = stats_api->GetAchievementIcon( m_achievement_list[i].id );
stats_api->GetAchievement(pchName, &(m_achievement_list[i].achieved));
m_achievement_list[i].hidden = (bool)strcmp(stats_api->GetAchievementDisplayAttribute(pchName, "hidden" ), "0");
// TODO: incorrect as is
//m_achievement_list[i].icon_handle = stats_api->GetAchievementIcon(pchName);
m_achievement_list[i].icon_handle = 0;
}
m_stats_callback_received = true;

View File

@ -1,21 +1,15 @@
#pragma once
#define MAX_ACHIEVEMENT_ID_LENGTH 256
#define MAX_ACHIEVEMENT_DESC_LENGTH 256
#define MAX_ACHIEVEMENT_NAME_LENGTH 100
#include <string>
/**
* Achievement structure.
* It uses basic C types and has a fixed length, because they need
* to be piped between different process quickly.
* The set limits are arbitrary, we will assume that if the name or
* desription is longer than the given size, the display won't look great,
* so we trim the end.
* Upgraded to use C++ types
*/
struct Achievement_t {
char name[MAX_ACHIEVEMENT_NAME_LENGTH];
char desc[MAX_ACHIEVEMENT_DESC_LENGTH];
char id[MAX_ACHIEVEMENT_ID_LENGTH]; // I have no idea what the length limit of this is. Crossing fingers there's none above 256.
std::string name;
std::string desc;
std::string id;
float global_achieved_rate;
int icon_handle; //0 : incorrect, error occurred, RTFM
bool achieved;

View File

@ -1,3 +1,4 @@
#pragma once
// Message format shall be
// all GET format
@ -5,10 +6,17 @@
#define STORE_ACHIEVEMENTS_STR "STORE_ACHIEVEMENTS"
#define QUIT_GAME_STR "QUIT_GAME"
// Mirroring structure of Achievement_t, should be combined with that
#define SAM_ACTION_STR "SAM_ACTION"
#define ACHIEVEMENT_LIST_STR "ACHIEVEMENT_LIST"
#define ACHIEVEMENT_NAME_STR "ACHIEVEMENT_NAME"
#define NAME_STR "NAME"
#define DESC_STR "DESC"
#define ID_STR "ID"
#define RATE_STR "RATE"
#define ICON_STR "ICON"
#define ACHIEVED_STR "ACHIEVED"
#define HIDDEN_STR "HIDDEN"
#define SAM_ACK_STR "SAM_ACK"
// TODO: Add SAM_START as an action to this too?
@ -20,38 +28,68 @@ enum SAM_ACTION {
INVALID
};
// support for the full achievement type shall be added
// for now just implement achieved/not achieved
/* JSON format shall be
messages are sent as plaintext strings
messages are delimited by the usual string NULL terminator
get all achievements for active game
{
GET_ACHIEVEMENTS_STR
SAM_ACTION_STR: GET_ACHIEVEMENTS_STR
}
response
{
int NUM_ACH //maybe not necessary if we have .length function?
[{
str ACH_NAME
bool ACHIEVED
}]
SAM_ACK: SAM_ACK,
ACHIEVEMENT_LIST_STR:
[
{
NAME_STR: "name"
DESC_STR: "desc"
ID_STR: "ID"
RATE_STR: global_achieved_rate
ICON_STR: icon
ACHIEVED_STR: true/fase
HIDDEN_STR: true/false
},
.
.
.
]
}
store a list of achievement changes
can be reduced to not encode the whole achievement,
only id and achieved status / stats changes
{
int NUM_ACH //maybe not necessary if we have .length function?
[{
str ACH_NAME
bool ACHIEVED
}]
SAM_ACTION_STR: STORE_ACHIEVEMENTS_STR
ACHIEVEMENT_LIST_STR:
[
{
NAME_STR: ""
DESC_STR: ""
ID_STR: "ID"
RATE_STR: 0
ICON_STR: 0
ACHIEVED_STR: true/fase
HIDDEN_STR: false
},
.
.
.
]
}
response
{
SAM_ACK: SAM_ACK,
}
quit active game
{
SAM_QUIT_STR
SAM_ACTION_STR: SAM_QUIT_STR
}
response
{
SAM_ACK: SAM_ACK,
}
*/