From 8695ea2dce30530d2f2453242b64d107ac71b687 Mon Sep 17 00:00:00 2001 From: Mr_Goldberg Date: Tue, 12 Jul 2022 01:09:27 -0400 Subject: [PATCH] Support achievements that are triggered automatically with stats. The achievements config MUST be generated with the achievements_gen.py script. --- Readme_release.txt | 4 + dll/common_includes.h | 6 + dll/settings.h | 2 +- dll/steam_user_stats.h | 81 +++++++++-- .../achievements_gen.py | 134 ++++++++++-------- 5 files changed, 151 insertions(+), 76 deletions(-) diff --git a/Readme_release.txt b/Readme_release.txt index 3346785..105f005 100644 --- a/Readme_release.txt +++ b/Readme_release.txt @@ -88,6 +88,8 @@ You can use https://steamdb.info/ to list items and attributes they have and put Keep in mind that some item are not valid to have in your inventory. For example, in PayDay2 all items below item_id 50000 will make your game crash. items.json should contain all the item definitions for the game, default_items.json is the quantity of each item that you want a user to have initially in their inventory. By default the user will have no items. +You can use the scripts\stats_schema_achievement_gen\achievements_gen.py script in the emu source code repo to generate a achievements config from a steam: appcache\stats\UserGameStatsSchema_{appid}.bin file. + Leaderboards: By default the emulator assumes all leaderboards queried by the game (FindLeaderboard()) exist and creates them with the most common options (sort method descending, display type numeric) In some games this default behavior doesn't work and so you may need to tweak which leaderboards the game sees. @@ -105,6 +107,8 @@ The format is: STAT_NAME=type=default value The type can be: int, float or avgrate The default value is simply a number that represents the default value for the stat. +You can use the scripts\stats_schema_achievement_gen\achievements_gen.py script in the emu source code repo to generate a stats config from a steam: appcache\stats\UserGameStatsSchema_{appid}.bin file. + Build id: Add a steam_settings\build_id.txt with the build id if the game doesn't show the correct build id and you want the emu to give it the correct one. An example can be found in steam_settings.EXAMPLE diff --git a/dll/common_includes.h b/dll/common_includes.h index f68e161..0bf7978 100644 --- a/dll/common_includes.h +++ b/dll/common_includes.h @@ -165,6 +165,12 @@ inline std::wstring utf8_decode(const std::string &str) #include #include +inline std::string ascii_to_lowercase(std::string data) { + std::transform(data.begin(), data.end(), data.begin(), + [](unsigned char c){ return std::tolower(c); }); + return data; +} + // Other libs includes #include "../json/json.hpp" #include "../controller/gamepad.h" diff --git a/dll/settings.h b/dll/settings.h index b54815e..03c956d 100644 --- a/dll/settings.h +++ b/dll/settings.h @@ -131,7 +131,7 @@ public: //stats std::map getStats() { return stats; } - void setStatDefiniton(std::string name, struct Stat_config stat_config) {stats[name] = stat_config; } + void setStatDefiniton(std::string name, struct Stat_config stat_config) {stats[ascii_to_lowercase(name)] = stat_config; } //subscribed lobby/group ids std::set subscribed_groups; diff --git a/dll/steam_user_stats.h b/dll/steam_user_stats.h index cbecf14..cc5f81e 100644 --- a/dll/steam_user_stats.h +++ b/dll/steam_user_stats.h @@ -27,6 +27,29 @@ struct Steam_Leaderboard { ELeaderboardDisplayType display_type; }; +struct achievement_trigger { + std::string name; + std::string value_operation; + std::string min_value; + std::string max_value; + + bool check_triggered(float stat) { + try { + if (std::stof(max_value) <= stat) return true; + } catch (...) {} + + return false; + } + + bool check_triggered(int32 stat) { + try { + if (std::stoi(max_value) <= stat) return true; + } catch (...) {} + + return false; + } +}; + class Steam_User_Stats : public ISteamUserStats003, public ISteamUserStats004, @@ -58,6 +81,7 @@ private: std::map stats_cache_int; std::map stats_cache_float; + std::map achievement_stat_trigger; unsigned int find_leaderboard(std::string name) { @@ -118,6 +142,14 @@ Steam_User_Stats(Settings *settings, Local_Storage *local_storage, class SteamCa user_achievements[name]["earned"] = false; user_achievements[name]["earned_time"] = static_cast(0); } + + achievement_trigger trig; + trig.name = name; + trig.value_operation = static_cast(it["progress"]["value"]["operation"]); + std::string stat_name = ascii_to_lowercase(static_cast(it["progress"]["value"]["operand1"])); + trig.min_value = static_cast(it["progress"]["min_val"]); + trig.max_value = static_cast(it["progress"]["max_val"]); + achievement_stat_trigger[stat_name] = trig; } catch (...) {} try { @@ -154,14 +186,16 @@ bool GetStat( const char *pchName, int32 *pData ) { PRINT_DEBUG("GetStat int32 %s\n", pchName); if (!pchName || !pData) return false; + std::string stat_name = ascii_to_lowercase(pchName); + std::lock_guard lock(global_mutex); auto stats_config = settings->getStats(); - auto stats_data = stats_config.find(pchName); + auto stats_data = stats_config.find(stat_name); if (stats_data != stats_config.end()) { if (stats_data->second.type != Stat_Type::STAT_TYPE_INT) return false; } - int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, pchName, (char* )pData, sizeof(*pData)); + int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )pData, sizeof(*pData)); if (read_data == sizeof(int32)) return true; @@ -177,14 +211,16 @@ bool GetStat( const char *pchName, float *pData ) { PRINT_DEBUG("GetStat float %s\n", pchName); if (!pchName || !pData) return false; + std::string stat_name = ascii_to_lowercase(pchName); + std::lock_guard lock(global_mutex); auto stats_config = settings->getStats(); - auto stats_data = stats_config.find(pchName); + auto stats_data = stats_config.find(stat_name); if (stats_data != stats_config.end()) { if (stats_data->second.type == Stat_Type::STAT_TYPE_INT) return false; } - int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, pchName, (char* )pData, sizeof(*pData)); + int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )pData, sizeof(*pData)); if (read_data == sizeof(float)) return true; @@ -202,14 +238,23 @@ bool SetStat( const char *pchName, int32 nData ) { PRINT_DEBUG("SetStat int32 %s\n", pchName); if (!pchName) return false; + std::string stat_name = ascii_to_lowercase(pchName); + std::lock_guard lock(global_mutex); - auto cached_stat = stats_cache_int.find(pchName); + auto cached_stat = stats_cache_int.find(stat_name); if (cached_stat != stats_cache_int.end()) { if (cached_stat->second == nData) return true; } - if (local_storage->store_data(Local_Storage::stats_storage_folder, pchName, (char* )&nData, sizeof(nData)) == sizeof(nData)) { - stats_cache_int[pchName] = nData; + auto stat_trigger = achievement_stat_trigger.find(stat_name); + if (stat_trigger != achievement_stat_trigger.end()) { + if (stat_trigger->second.check_triggered(nData)) { + SetAchievement(stat_trigger->second.name.c_str()); + } + } + + if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char* )&nData, sizeof(nData)) == sizeof(nData)) { + stats_cache_int[stat_name] = nData; return true; } @@ -220,14 +265,23 @@ bool SetStat( const char *pchName, float fData ) { PRINT_DEBUG("SetStat float %s\n", pchName); if (!pchName) return false; + std::string stat_name = ascii_to_lowercase(pchName); + std::lock_guard lock(global_mutex); - auto cached_stat = stats_cache_float.find(pchName); + auto cached_stat = stats_cache_float.find(stat_name); if (cached_stat != stats_cache_float.end()) { if (cached_stat->second == fData) return true; } - if (local_storage->store_data(Local_Storage::stats_storage_folder, pchName, (char* )&fData, sizeof(fData)) == sizeof(fData)) { - stats_cache_float[pchName] = fData; + auto stat_trigger = achievement_stat_trigger.find(stat_name); + if (stat_trigger != achievement_stat_trigger.end()) { + if (stat_trigger->second.check_triggered(fData)) { + SetAchievement(stat_trigger->second.name.c_str()); + } + } + + if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char* )&fData, sizeof(fData)) == sizeof(fData)) { + stats_cache_float[stat_name] = fData; return true; } @@ -237,10 +291,13 @@ bool SetStat( const char *pchName, float fData ) bool UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength ) { PRINT_DEBUG("UpdateAvgRateStat %s\n", pchName); + if (!pchName) return false; + std::string stat_name = ascii_to_lowercase(pchName); + std::lock_guard lock(global_mutex); char data[sizeof(float) + sizeof(float) + sizeof(double)]; - int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, pchName, (char* )data, sizeof(*data)); + int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )data, sizeof(*data)); float oldcount = 0; double oldsessionlength = 0; if (read_data == sizeof(data)) { @@ -256,7 +313,7 @@ bool UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dS memcpy(data + sizeof(float), &oldcount, sizeof(oldcount)); memcpy(data + sizeof(float) * 2, &oldsessionlength, sizeof(oldsessionlength)); - return local_storage->store_data(Local_Storage::stats_storage_folder, pchName, data, sizeof(data)) == sizeof(data); + return local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, data, sizeof(data)) == sizeof(data); } diff --git a/scripts/stats_schema_achievement_gen/achievements_gen.py b/scripts/stats_schema_achievement_gen/achievements_gen.py index b1bf91f..a6c5c65 100644 --- a/scripts/stats_schema_achievement_gen/achievements_gen.py +++ b/scripts/stats_schema_achievement_gen/achievements_gen.py @@ -4,14 +4,6 @@ import os import json -if len(sys.argv) < 2: - print("format: {} UserGameStatsSchema_480.bin".format(sys.argv[0])) - exit(0) - - -with open(sys.argv[1], 'rb') as f: - schema = vdf.binary_loads(f.read()) - language = 'english' STAT_TYPE_INT = '1' @@ -19,68 +11,84 @@ STAT_TYPE_FLOAT = '2' STAT_TYPE_AVGRATE = '3' STAT_TYPE_BITS = '4' -achievements_out = [] -stats_out = [] +def generate_stats_achievements(schema, config_directory): + schema = vdf.binary_loads(schema) + achievements_out = [] + stats_out = [] -for appid in schema: - sch = schema[appid] - stat_info = sch['stats'] - for s in stat_info: - stat = stat_info[s] - if stat['type'] == STAT_TYPE_BITS: - achs = stat['bits'] - for ach_num in achs: + for appid in schema: + sch = schema[appid] + stat_info = sch['stats'] + for s in stat_info: + stat = stat_info[s] + if stat['type'] == STAT_TYPE_BITS: + achs = stat['bits'] + for ach_num in achs: + out = {} + ach = achs[ach_num] + out["hidden"] = '0' + for x in ach['display']: + value = ach['display'][x] + if x == 'name': + x = 'displayName' + if x == 'desc': + x = 'description' + if x == 'Hidden': + x = 'hidden' + if type(value) is dict: + if language in value: + value = value[language] + else: + value = '' + out[x] = value + out['name'] = ach['name'] + if 'progress' in ach: + out['progress'] = ach['progress'] + achievements_out += [out] + else: out = {} - ach = achs[ach_num] - out["hidden"] = '0' - for x in ach['display']: - value = ach['display'][x] - if x == 'name': - x = 'displayName' - if x == 'desc': - x = 'description' - if x == 'Hidden': - x = 'hidden' - if type(value) is dict: - if language in value: - value = value[language] - else: - value = '' - out[x] = value - out['name'] = ach['name'] - achievements_out += [out] - else: - out = {} - out['default'] = 0 - out['name'] = stat['name'] - if stat['type'] == STAT_TYPE_INT: - out['type'] = 'int' - elif stat['type'] == STAT_TYPE_FLOAT: - out['type'] = 'float' - elif stat['type'] == STAT_TYPE_AVGRATE: - out['type'] = 'avgrate' - if 'Default' in stat: - out['default'] = stat['Default'] + out['default'] = 0 + out['name'] = stat['name'] + if stat['type'] == STAT_TYPE_INT: + out['type'] = 'int' + elif stat['type'] == STAT_TYPE_FLOAT: + out['type'] = 'float' + elif stat['type'] == STAT_TYPE_AVGRATE: + out['type'] = 'avgrate' + if 'Default' in stat: + out['default'] = stat['Default'] - stats_out += [out] - # print(stat_info[s]) + stats_out += [out] + # print(stat_info[s]) -output_ach = json.dumps(achievements_out, indent=4) -output_stats = "" -for s in stats_out: - output_stats += "{}={}={}\n".format(s['name'], s['type'], s['default']) + output_ach = json.dumps(achievements_out, indent=4) + output_stats = "" + for s in stats_out: + output_stats += "{}={}={}\n".format(s['name'], s['type'], s['default']) -# print(output_ach) -# print(output_stats) + # print(output_ach) + # print(output_stats) -config_directory = os.path.join(sys.argv[1] + "_output", "steam_settings") -if not os.path.exists(config_directory): - os.makedirs(config_directory) + if not os.path.exists(config_directory): + os.makedirs(config_directory) -with open(os.path.join(config_directory, "achievements.json"), 'w') as f: - f.write(output_ach) + with open(os.path.join(config_directory, "achievements.json"), 'w') as f: + f.write(output_ach) -with open(os.path.join(config_directory, "stats.txt"), 'w') as f: - f.write(output_stats) + with open(os.path.join(config_directory, "stats.txt"), 'w') as f: + f.write(output_stats) + + return (output_ach, output_stats) + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("format: {} UserGameStatsSchema_480.bin".format(sys.argv[0])) + exit(0) + + + with open(sys.argv[1], 'rb') as f: + schema = f.read() + + generate_stats_achievements(schema, os.path.join("{}".format( "{}_output".format(sys.argv[1])), "steam_settings"))