diff --git a/build_curl.sh b/build_curl.sh new file mode 100644 index 0000000..6d40296 --- /dev/null +++ b/build_curl.sh @@ -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 diff --git a/curl/CMakeLists.txt b/curl/CMakeLists.txt new file mode 100644 index 0000000..7894dc9 --- /dev/null +++ b/curl/CMakeLists.txt @@ -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}) diff --git a/dll/steam_client.cpp b/dll/steam_client.cpp index a1a678b..ba33cf5 100644 --- a/dll/steam_client.cpp +++ b/dll/steam_client.cpp @@ -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); diff --git a/dll/steam_user_stats.h b/dll/steam_user_stats.h index d889579..57bb57a 100644 --- a/dll/steam_user_stats.h +++ b/dll/steam_user_stats.h @@ -17,13 +17,16 @@ #include "base.h" +#include +#include +#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 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(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(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(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(item["name"]) == pchName; + }); + if (it != achievements.end()) + { + *pbAchieved = it.value()["earned"].get(); + *punUnlockTime = std::time(NULL); + //*punUnlockTime = it.value()["time_earned"].get(); + return true; + } + } + catch (...) + { + + } + *pbAchieved = false; + *punUnlockTime = 0; return true; } @@ -195,6 +294,12 @@ bool StoreStats() PRINT_DEBUG("StoreStats\n"); std::lock_guard 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(item["name"]) == pchName; + }); + if (it != achievements.end()) + { + return it.value()["displayName"].get().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(item["name"]) == pchName; + }); + if (it != achievements.end()) + { + return it.value()["description"].get().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(item["name"]) == pchName; + }); + if (it != achievements.end()) + { + return (it.value()["hidden"].get() ? "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(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 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; diff --git a/generate_game_infos/generate_game_infos.cpp b/generate_game_infos/generate_game_infos.cpp new file mode 100644 index 0000000..509f42c --- /dev/null +++ b/generate_game_infos/generate_game_infos.cpp @@ -0,0 +1,242 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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=&appid= +/* +{ + "game" : { + "gameName" : "", + "availableGameStats" : { + "achievements" : { + ("" : { + "name" : "achievement_name", + "displayName" : "achievement name on screen", + "hidden" : (0|1), + ["description" : "",] + "icon" : "", + "icongray" : "" + }, + ...) + } + } + } +} +*/ +// Get appid infos: http://store.steampowered.com/api/appdetails/?appids=218620 +/* +"appid" : { + "success" : (true|false), + (success == true "data" : { + ... + "name" : "", + "steam_appid" : , + (OPT "dlc" : [, ]), + "header_image" : "" <-- Use this in the overlay ? + (OPT "achievements" : { + "total" : + }), + "background" : "" <-- Use this as the overlay background ? + (OPT "packages" : [, ]) + }) +} +*/ + +#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::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; + } + } +} + \ No newline at end of file