(WIP) achievements support

build is also WIP
merge-requests/24/head
Nemirtingas 2019-08-21 20:52:36 +02:00
parent ced9b77afc
commit ea4588f442
5 changed files with 535 additions and 14 deletions

45
build_curl.sh Normal file
View File

@ -0,0 +1,45 @@
#! /bin/bash
# Build type (Debug or Release)
BUILD_TYPE="$1"
# where to build protobuf, must be win32 or win64
OUT_DIR="$2"
[ "$OUT_DIR" != "win32" -a "$OUT_DIR" != "win64" -a "$OUT_DIR" != "x86" -a "$OUT_DIR" != "x64" ] && echo "The output dir must be 'Win32', 'Win64', 'x86' or 'x64'" && exit 1
[ "$BUILD_TYPE" != "Debug" -a "$BUILD_TYPE" != "Release" ] && echo "The build type must be 'Debug' or 'Release'" && exit 1
# apt install libssl1.0-dev libssl1.0-dev:i386
# My variable to decide if we build x86 or x64 in CMakeLists.txt
if [ "$OUT_DIR" == "win32" -o "$OUT_DIR" == "x86" ]; then
custom_arch_var="-DX86=ON"
else
custom_arch_var="-DX64=ON"
fi
build_type="-DCMAKE_BUILD_TYPE=${BUILD_TYPE}"
build_http="-DHTTP_ONLY=ON"
build_exe="-DBUILD_CURL_EXE=OFF"
build_shared="-DBUILD_SHARED_LIBS=OFF"
build_testing="-DBUILD_TESTING=OFF"
args=()
args+=($build_http)
args+=($build_exe)
args+=($build_shared)
args+=($build_testing)
args+=($build_type)
args+=($custom_arch_var)
# EXTRA_CMAKE_ENV is set by setup_clang_env.sh to build for windows.
# You must run setup_clang_env.sh before calling this script if you build for windows.
rm -rf "curl/$OUT_DIR" &&
mkdir "curl/$OUT_DIR" &&
cd "curl/$OUT_DIR" &&
echo "cmake -G \"Unix Makefiles\" $EXTRA_CMAKE_ENV \"${args[@]}\" .." &&
cmake -G "Unix Makefiles" $EXTRA_CMAKE_ENV "${args[@]}" .. &&
make -j${JOBS-2} || exit 1
exit 0

79
curl/CMakeLists.txt Normal file
View File

@ -0,0 +1,79 @@
#CMAKE_TOOLCHAIN_FILE
project(goldberg_emulator_protobuf)
cmake_minimum_required(VERSION 3.0)
if(WIN32)
# Detect arch on Windows
if( ${CMAKE_SIZEOF_VOID_P} EQUAL 8)
set(X64 ON)
else()
set(X86 ON)
endif()
if(MSVC)
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)
else()
add_compile_options(-std=c++11)
endif()
set(win_libs Iphlpapi ws2_32)
if(X64)
set(STEAM_NAME steam_api64)
elseif(X86)
set(STEAM_NAME steam_api)
else()
message(FATAL_ERROR "Arch unknown")
endif()
elseif(APPLE)
message(FATAL_ERROR "No CMake for Apple")
else()
if(X64)
set(CMAKE_C_FLAGS "-m64")
set(CMAKE_CXX_FLAGS "-m64")
elseif(X86)
set(CMAKE_C_FLAGS "-m32")
set(CMAKE_CXX_FLAGS "-m32")
else()
message(FATAL_ERROR "Arch unknown")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden -fPIC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fPIC")
endif()
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CURL_VERSION "7.65.3")
set(CURL_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(CURL_RELEASE_URL "https://curl.haxx.se/download/curl-${CURL_VERSION}.tar.xz")
set(CURL_SRC curl-src)
if( NOT EXISTS ${CURL_DIR}/${CURL_SRC} )
file(
DOWNLOAD ${CURL_RELEASE_URL} ${CURL_DIR}/curl.tar.xz
SHOW_PROGRESS
EXPECTED_HASH MD5=7bd5b2ebfd3f591034eb8b55314d8c02
)
if( NOT EXISTS ${CURL_DIR}/curl.tar.xz )
message(FATAL_ERROR "Download of curl failed")
endif()
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar -xf curl.tar.xz
WORKING_DIRECTORY ${CURL_DIR}
)
file(REMOVE ${CURL_DIR}/curl.tar.xz)
file(RENAME ${CURL_DIR}/curl-${CURL_VERSION} "${CURL_SRC}")
endif()
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_subdirectory(${CURL_DIR}/${CURL_SRC})

View File

@ -335,6 +335,7 @@ Steam_Client::Steam_Client()
}
}
std::string achievements_db_file_path = (Local_Storage::get_game_settings_path() + "achievements.json");
std::string items_db_file_path = (Local_Storage::get_game_settings_path() + "items.json");
network = new Networking(settings_server->get_local_steam_id(), appid, port, &custom_broadcasts);
@ -352,7 +353,7 @@ Steam_Client::Steam_Client()
steam_utils = new Steam_Utils(settings_client, callback_results_client);
steam_matchmaking = new Steam_Matchmaking(settings_client, network, callback_results_client, callbacks_client, run_every_runcb);
steam_matchmaking_servers = new Steam_Matchmaking_Servers(settings_client, network);
steam_user_stats = new Steam_User_Stats(settings_client, local_storage, callback_results_client, callbacks_client);
steam_user_stats = new Steam_User_Stats(settings_client, local_storage, callback_results_client, callbacks_client, achievements_db_file_path);
steam_apps = new Steam_Apps(settings_client, callback_results_client);
steam_networking = new Steam_Networking(settings_client, network, callbacks_client, run_every_runcb);
steam_remote_storage = new Steam_Remote_Storage(settings_client, local_storage, callback_results_client);

View File

@ -17,13 +17,16 @@
#include "base.h"
#include <iomanip>
#include <fstream>
#include "../json/json.hpp"
struct Steam_Leaderboard {
std::string name;
ELeaderboardSortMethod sort_method;
ELeaderboardDisplayType display_type;
};
class Steam_User_Stats :
public ISteamUserStats003,
public ISteamUserStats004,
@ -41,6 +44,9 @@ public ISteamUserStats
class SteamCallBacks *callbacks;
std::vector<struct Steam_Leaderboard> leaderboards;
std::string db_file_path;
nlohmann::json achievements;
unsigned int find_leaderboard(std::string name)
{
unsigned index = 1;
@ -52,13 +58,41 @@ unsigned int find_leaderboard(std::string name)
return 0;
}
public:
Steam_User_Stats(Settings *settings, Local_Storage *local_storage, class SteamCallResults *callback_results, class SteamCallBacks *callbacks)
void load_achievements()
{
this->local_storage = local_storage;
this->settings = settings;
this->callback_results = callback_results;
this->callbacks = callbacks;
std::ifstream achs_file(db_file_path);
if (achs_file)
{
achs_file.seekg(0, std::ios::end);
size_t size = achs_file.tellg();
std::string buffer(size, '\0');
achs_file.seekg(0);
// Read it entirely, if the .json file gets too big,
// I should look into this and split reads into smaller parts.
achs_file.read(&buffer[0], size);
achs_file.close();
try
{
achievements = nlohmann::json::parse(buffer);
}
catch (std::exception &e)
{
PRINT_DEBUG("(Achievements): Error while parsing json: %s\n", e.what());
}
}
}
public:
Steam_User_Stats(Settings *settings, Local_Storage *local_storage, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, std::string const&achievements_db_file_path):
settings(settings),
local_storage(local_storage),
callback_results(callback_results),
callbacks(callbacks),
db_file_path(achievements_db_file_path)
{
load_achievements();
}
// Ask the server to send down this user's data and achievements for this game
@ -155,19 +189,65 @@ bool GetAchievement( const char *pchName, bool *pbAchieved )
{
//TODO: these achievement functions need to load a list of achievements from somewhere, return false so that kf2 doesn't loop endlessly
PRINT_DEBUG("GetAchievement %s\n", pchName);
*pbAchieved = false;
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName]( nlohmann::json &item ) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
*pbAchieved = it.value()["earned"];
return true;
}
}
catch (...)
{
}
return false;
}
bool SetAchievement( const char *pchName )
{
PRINT_DEBUG("SetAchievement %s\n", pchName);
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName](nlohmann::json& item) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
it.value()["earned"] = 1;
return true;
}
}
catch (...)
{
}
return false;
}
bool ClearAchievement( const char *pchName )
{
PRINT_DEBUG("ClearAchievement %s\n", pchName);
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName](nlohmann::json& item) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
it.value()["earned"] = 0;
return true;
}
}
catch (...)
{
}
return false;
}
@ -178,7 +258,26 @@ bool ClearAchievement( const char *pchName )
bool GetAchievementAndUnlockTime( const char *pchName, bool *pbAchieved, uint32 *punUnlockTime )
{
PRINT_DEBUG("GetAchievementAndUnlockTime\n");
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName](nlohmann::json& item) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
*pbAchieved = it.value()["earned"].get<int>();
*punUnlockTime = std::time(NULL);
//*punUnlockTime = it.value()["time_earned"].get<uint32>();
return true;
}
}
catch (...)
{
}
*pbAchieved = false;
*punUnlockTime = 0;
return true;
}
@ -195,6 +294,12 @@ bool StoreStats()
PRINT_DEBUG("StoreStats\n");
std::lock_guard<std::recursive_mutex> lock(global_mutex);
std::ofstream achiev_file(db_file_path, std::ios::trunc | std::ios::out);
if (achiev_file)
{
achiev_file << std::setw(2) << achievements;
}
UserStatsStored_t data;
data.m_nGameID = settings->get_local_game_id().ToUint64();
data.m_eResult = k_EResultOK;
@ -222,18 +327,56 @@ int GetAchievementIcon( const char *pchName )
const char * GetAchievementDisplayAttribute( const char *pchName, const char *pchKey )
{
PRINT_DEBUG("GetAchievementDisplayAttribute %s %s\n", pchName, pchKey);
return ""; //TODO
if (strcmp (pchKey, "name") == 0) {
return "Achievement Name";
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName](nlohmann::json& item) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
return it.value()["displayName"].get<std::string>().c_str();
}
}
catch (...)
{
}
}
if (strcmp (pchKey, "desc") == 0) {
return "Achievement Description";
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName](nlohmann::json& item) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
return it.value()["description"].get<std::string>().c_str();
}
}
catch (...)
{
}
}
if (strcmp (pchKey, "hidden") == 0) {
return "0";
try
{
auto it = std::find_if(achievements.begin(), achievements.end(), [pchName](nlohmann::json& item) {
return static_cast<std::string const&>(item["name"]) == pchName;
});
if (it != achievements.end())
{
return (it.value()["hidden"].get<int>() ? "1" : "0");
}
}
catch (...)
{
}
}
return "";
@ -253,13 +396,21 @@ bool IndicateAchievementProgress( const char *pchName, uint32 nCurProgress, uint
uint32 GetNumAchievements()
{
PRINT_DEBUG("GetNumAchievements\n");
return 0;
return achievements.size();
}
// Get achievement name iAchievement in [0,GetNumAchievements)
const char * GetAchievementName( uint32 iAchievement )
{
PRINT_DEBUG("GetAchievementName\n");
try
{
return static_cast<std::string const&>(achievements[iAchievement]["name"]).c_str();
}
catch (...)
{
}
return "";
}
@ -276,6 +427,9 @@ SteamAPICall_t RequestUserStats( CSteamID steamIDUser )
PRINT_DEBUG("Steam_User_Stats::RequestUserStats %llu\n", steamIDUser.ConvertToUint64());
std::lock_guard<std::recursive_mutex> lock(global_mutex);
// Enable this to allow hot reload achievements status
//load_achievements();
UserStatsReceived_t data;
data.m_nGameID = settings->get_local_game_id().ToUint64();
data.m_eResult = k_EResultOK;

View File

@ -0,0 +1,242 @@
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <curl/curl.h>
#include <json/json.hpp>
class CurlGlobal
{
bool _init;
CurlGlobal() :_init(false) {}
~CurlGlobal() { cleanup(); }
public:
static CurlGlobal& Inst()
{
static CurlGlobal _this;
return _this;
}
CURLcode init(long flags = CURL_GLOBAL_DEFAULT) { return curl_global_init(flags); }
void cleanup()
{
if (_init)
{
curl_global_cleanup();
_init = false;
}
}
};
class CurlEasy
{
CURL* _me;
bool _init;
std::string _buffer;
static int writer(char* data, size_t size, size_t nmemb,
CurlEasy *_this)
{
if (_this == nullptr)
return 0;
_this->_buffer.append(data, size * nmemb);
return size * nmemb;
}
public:
CurlEasy() :_me(nullptr), _init(false) {}
~CurlEasy() { cleanup(); }
bool init()
{
_init = (_me = curl_easy_init()) != nullptr;
if (_init)
{
if (curl_easy_setopt(_me, CURLOPT_WRITEFUNCTION, writer) != CURLE_OK)
{
cleanup();
return false;
}
if (curl_easy_setopt(_me, CURLOPT_WRITEDATA, this) != CURLE_OK)
{
cleanup();
return false;
}
}
return _init;
}
void cleanup()
{
if (_init)
{
curl_easy_cleanup(_me);
}
}
CURLcode set_url(const std::string& url)
{
return curl_easy_setopt(_me, CURLOPT_URL, url.c_str());
}
CURLcode skip_verifypeer(bool skip = true)
{
return curl_easy_setopt(_me, CURLOPT_SSL_VERIFYPEER, skip ? 0L : 1L);
}
CURLcode skip_verifhost(bool skip = true)
{
return curl_easy_setopt(_me, CURLOPT_SSL_VERIFYHOST, skip ? 0L : 1L);
}
CURLcode connect_only(bool connect = true)
{
return curl_easy_setopt(_me, CURLOPT_CONNECT_ONLY, connect ? 1L : 0L);
}
CURLcode perform()
{
_buffer.clear();
return curl_easy_perform(_me);
}
CURLcode recv(void *buffer, size_t buflen, size_t* read_len)
{
return curl_easy_recv(_me, buffer, buflen, read_len);
}
CURLcode get_html_code(long &code)
{
return curl_easy_getinfo(_me, CURLINFO_RESPONSE_CODE, &code);
}
std::string const& get_answer() const { return _buffer; }
};
// Get all steam appid with their name: http://api.steampowered.com/ISteamApps/GetAppList/v2/
// Steam storefront webapi: https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI
// http://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/?key=<key>&appid=<appid>
/*
{
"game" : {
"gameName" : "<name>",
"availableGameStats" : {
"achievements" : {
("<id>" : {
"name" : "achievement_name",
"displayName" : "achievement name on screen",
"hidden" : (0|1),
["description" : "<desc>",]
"icon" : "<url to icon when achievement is earned>",
"icongray" : "<url to icon when achievement is not earned>"
},
...)
}
}
}
}
*/
// Get appid infos: http://store.steampowered.com/api/appdetails/?appids=218620
/*
"appid" : {
"success" : (true|false),
(success == true "data" : {
...
"name" : "<name>",
"steam_appid" : <appid>,
(OPT "dlc" : [<dlc id>, <dlc id>]),
"header_image" : "<miniature url>" <-- Use this in the overlay ?
(OPT "achievements" : {
"total" : <num of achievements>
}),
"background" : "<background url>" <-- Use this as the overlay background ?
(OPT "packages" : [<package id>, <package id>])
})
}
*/
#ifdef max
#undef max
#endif
int main()
{
CurlGlobal& cglobal = CurlGlobal::Inst();
cglobal.init();
CurlEasy easy;
if (easy.init())
{
std::string url;
std::string steam_apikey;
std::string app_id;
std::cout << "Enter the game appid: ";
std::cin >> app_id;
std::cout << "Enter your webapi key: ";
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cin >> steam_apikey;
url = "http://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/?key=";
url += steam_apikey;
url += "&appid=";
url += app_id;
easy.set_url(url);
easy.perform();
try
{
std::ofstream ach_file("achievements.json", std::ios::trunc | std::ios::out);
nlohmann::json json = nlohmann::json::parse(easy.get_answer());
nlohmann::json output_json = nlohmann::json::array();
bool first = true;
int i = 0;
for (auto& item : json["game"]["availableGameStats"]["achievements"].items())
{
output_json[i]["name"] = item.value()["name"];
output_json[i]["displayName"] = item.value()["displayName"];
output_json[i]["hidden"] = item.value()["hidden"];
try
{
output_json[i]["description"] = item.value()["description"];
}
catch (...)
{
output_json[i]["description"] = "";
}
output_json[i]["icon"] = item.value()["icon"];
output_json[i]["icongray"] = item.value()["icongray"];
output_json[i]["time_earned"] = 0;
output_json[i]["earned"] = 0;
++i;
}
ach_file << std::setw(2) << output_json;
}
catch (std::exception& e)
{
std::cerr << "Failed to get infos: ";
long code;
if (easy.get_html_code(code) == CURLE_OK && code == 403)
{
std::cerr << "Error in webapi key";
}
else
{
std::cerr << "Error while parsing json. Try to go at " << url << " and see what you can do to build your achivements.json";
}
std::cerr << std::endl;
}
}
}