Support achievements that are triggered automatically with stats.

The achievements config MUST be generated with the achievements_gen.py script.
This commit is contained in:
Mr_Goldberg 2022-07-12 01:09:27 -04:00
parent 3f8ce69b6d
commit 8695ea2dce
No known key found for this signature in database
GPG key ID: 8597D87419DEF278
5 changed files with 151 additions and 76 deletions

View file

@ -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. 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. 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: 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) 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. 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 type can be: int, float or avgrate
The default value is simply a number that represents the default value for the stat. 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: 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. 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 An example can be found in steam_settings.EXAMPLE

View file

@ -165,6 +165,12 @@ inline std::wstring utf8_decode(const std::string &str)
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
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 // Other libs includes
#include "../json/json.hpp" #include "../json/json.hpp"
#include "../controller/gamepad.h" #include "../controller/gamepad.h"

View file

@ -131,7 +131,7 @@ public:
//stats //stats
std::map<std::string, Stat_config> getStats() { return stats; } std::map<std::string, Stat_config> 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 //subscribed lobby/group ids
std::set<uint64> subscribed_groups; std::set<uint64> subscribed_groups;

View file

@ -27,6 +27,29 @@ struct Steam_Leaderboard {
ELeaderboardDisplayType display_type; 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 : class Steam_User_Stats :
public ISteamUserStats003, public ISteamUserStats003,
public ISteamUserStats004, public ISteamUserStats004,
@ -58,6 +81,7 @@ private:
std::map<std::string, int32> stats_cache_int; std::map<std::string, int32> stats_cache_int;
std::map<std::string, float> stats_cache_float; std::map<std::string, float> stats_cache_float;
std::map<std::string, achievement_trigger> achievement_stat_trigger;
unsigned int find_leaderboard(std::string name) 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"] = false;
user_achievements[name]["earned_time"] = static_cast<uint32>(0); user_achievements[name]["earned_time"] = static_cast<uint32>(0);
} }
achievement_trigger trig;
trig.name = name;
trig.value_operation = static_cast<std::string const&>(it["progress"]["value"]["operation"]);
std::string stat_name = ascii_to_lowercase(static_cast<std::string const&>(it["progress"]["value"]["operand1"]));
trig.min_value = static_cast<std::string const&>(it["progress"]["min_val"]);
trig.max_value = static_cast<std::string const&>(it["progress"]["max_val"]);
achievement_stat_trigger[stat_name] = trig;
} catch (...) {} } catch (...) {}
try { try {
@ -154,14 +186,16 @@ bool GetStat( const char *pchName, int32 *pData )
{ {
PRINT_DEBUG("GetStat int32 %s\n", pchName); PRINT_DEBUG("GetStat int32 %s\n", pchName);
if (!pchName || !pData) return false; if (!pchName || !pData) return false;
std::string stat_name = ascii_to_lowercase(pchName);
std::lock_guard<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> lock(global_mutex);
auto stats_config = settings->getStats(); 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 != stats_config.end()) {
if (stats_data->second.type != Stat_Type::STAT_TYPE_INT) return false; 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)) if (read_data == sizeof(int32))
return true; return true;
@ -177,14 +211,16 @@ bool GetStat( const char *pchName, float *pData )
{ {
PRINT_DEBUG("GetStat float %s\n", pchName); PRINT_DEBUG("GetStat float %s\n", pchName);
if (!pchName || !pData) return false; if (!pchName || !pData) return false;
std::string stat_name = ascii_to_lowercase(pchName);
std::lock_guard<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> lock(global_mutex);
auto stats_config = settings->getStats(); 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 != stats_config.end()) {
if (stats_data->second.type == Stat_Type::STAT_TYPE_INT) return false; 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)) if (read_data == sizeof(float))
return true; return true;
@ -202,14 +238,23 @@ bool SetStat( const char *pchName, int32 nData )
{ {
PRINT_DEBUG("SetStat int32 %s\n", pchName); PRINT_DEBUG("SetStat int32 %s\n", pchName);
if (!pchName) return false; if (!pchName) return false;
std::string stat_name = ascii_to_lowercase(pchName);
std::lock_guard<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> 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 != stats_cache_int.end()) {
if (cached_stat->second == nData) return true; if (cached_stat->second == nData) return true;
} }
if (local_storage->store_data(Local_Storage::stats_storage_folder, pchName, (char* )&nData, sizeof(nData)) == sizeof(nData)) { auto stat_trigger = achievement_stat_trigger.find(stat_name);
stats_cache_int[pchName] = nData; 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; return true;
} }
@ -220,14 +265,23 @@ bool SetStat( const char *pchName, float fData )
{ {
PRINT_DEBUG("SetStat float %s\n", pchName); PRINT_DEBUG("SetStat float %s\n", pchName);
if (!pchName) return false; if (!pchName) return false;
std::string stat_name = ascii_to_lowercase(pchName);
std::lock_guard<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> 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 != stats_cache_float.end()) {
if (cached_stat->second == fData) return true; if (cached_stat->second == fData) return true;
} }
if (local_storage->store_data(Local_Storage::stats_storage_folder, pchName, (char* )&fData, sizeof(fData)) == sizeof(fData)) { auto stat_trigger = achievement_stat_trigger.find(stat_name);
stats_cache_float[pchName] = fData; 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; return true;
} }
@ -237,10 +291,13 @@ bool SetStat( const char *pchName, float fData )
bool UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength ) bool UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength )
{ {
PRINT_DEBUG("UpdateAvgRateStat %s\n", pchName); PRINT_DEBUG("UpdateAvgRateStat %s\n", pchName);
if (!pchName) return false;
std::string stat_name = ascii_to_lowercase(pchName);
std::lock_guard<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> lock(global_mutex);
char data[sizeof(float) + sizeof(float) + sizeof(double)]; 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; float oldcount = 0;
double oldsessionlength = 0; double oldsessionlength = 0;
if (read_data == sizeof(data)) { 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), &oldcount, sizeof(oldcount));
memcpy(data + sizeof(float) * 2, &oldsessionlength, sizeof(oldsessionlength)); 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);
} }

View file

@ -4,14 +4,6 @@ import os
import json 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' language = 'english'
STAT_TYPE_INT = '1' STAT_TYPE_INT = '1'
@ -19,68 +11,84 @@ STAT_TYPE_FLOAT = '2'
STAT_TYPE_AVGRATE = '3' STAT_TYPE_AVGRATE = '3'
STAT_TYPE_BITS = '4' STAT_TYPE_BITS = '4'
achievements_out = [] def generate_stats_achievements(schema, config_directory):
stats_out = [] schema = vdf.binary_loads(schema)
achievements_out = []
stats_out = []
for appid in schema: for appid in schema:
sch = schema[appid] sch = schema[appid]
stat_info = sch['stats'] stat_info = sch['stats']
for s in stat_info: for s in stat_info:
stat = stat_info[s] stat = stat_info[s]
if stat['type'] == STAT_TYPE_BITS: if stat['type'] == STAT_TYPE_BITS:
achs = stat['bits'] achs = stat['bits']
for ach_num in achs: 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 = {} out = {}
ach = achs[ach_num] out['default'] = 0
out["hidden"] = '0' out['name'] = stat['name']
for x in ach['display']: if stat['type'] == STAT_TYPE_INT:
value = ach['display'][x] out['type'] = 'int'
if x == 'name': elif stat['type'] == STAT_TYPE_FLOAT:
x = 'displayName' out['type'] = 'float'
if x == 'desc': elif stat['type'] == STAT_TYPE_AVGRATE:
x = 'description' out['type'] = 'avgrate'
if x == 'Hidden': if 'Default' in stat:
x = 'hidden' out['default'] = stat['Default']
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']
stats_out += [out] stats_out += [out]
# print(stat_info[s]) # print(stat_info[s])
output_ach = json.dumps(achievements_out, indent=4) output_ach = json.dumps(achievements_out, indent=4)
output_stats = "" output_stats = ""
for s in stats_out: for s in stats_out:
output_stats += "{}={}={}\n".format(s['name'], s['type'], s['default']) output_stats += "{}={}={}\n".format(s['name'], s['type'], s['default'])
# print(output_ach) # print(output_ach)
# print(output_stats) # print(output_stats)
config_directory = os.path.join(sys.argv[1] + "_output", "steam_settings") if not os.path.exists(config_directory):
if not os.path.exists(config_directory): os.makedirs(config_directory)
os.makedirs(config_directory)
with open(os.path.join(config_directory, "achievements.json"), 'w') as f: with open(os.path.join(config_directory, "achievements.json"), 'w') as f:
f.write(output_ach) f.write(output_ach)
with open(os.path.join(config_directory, "stats.txt"), 'w') as f: with open(os.path.join(config_directory, "stats.txt"), 'w') as f:
f.write(output_stats) 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"))