Support achievements that are triggered automatically with stats.
The achievements config MUST be generated with the achievements_gen.py script.merge-requests/49/merge
parent
3f8ce69b6d
commit
8695ea2dce
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
Loading…
Reference in New Issue