jak3: speedrunner mode (#3761)

Base implementation of the popup menu and speedrunner mode in Jak 3.
Autosplitter is untested because I'm on Linux.

Also a couple of other misc changes:

- Model replacements can now have custom bone weights. Needs the "Use
Custom Bone Weights" property (provided by the OpenGOAL Blender plugin)
enabled in Blender.
- Better error message for lump syntax errors in custom level JSON
files.
This commit is contained in:
Hat Kid
2024-11-17 06:45:34 +01:00
committed by GitHub
parent 5e3cb8faa6
commit 7543acfb8a
28 changed files with 3400 additions and 49 deletions
-2
View File
@@ -11,8 +11,6 @@ void pc_set_levels(u32 lev_list);
void pc_set_active_levels(u32 lev_list);
u32 alloc_vagdir_names(u32 heap_sym);
inline u64 bool_to_symbol(const bool val);
// TODO - move to common
void encode_utf8_string(u32 src_str_ptr, u32 str_dest_ptr);
void init_autosplit_struct();
void callback_fetch_external_speedrun_times(bool success,
const std::string& cache_id,
+41 -1
View File
@@ -369,7 +369,8 @@ void InitMachine_PCPort() {
make_function_symbol_from_c("__pc-set-active-levels",
(void*)kmachine_extras::pc_set_active_levels);
make_function_symbol_from_c("__pc-get-tex-remap", (void*)lookup_jak3_texture_dest_offset);
// make_function_symbol_from_c("pc-init-autosplitter-struct", (void*)init_autosplit_struct);
make_function_symbol_from_c("pc-init-autosplitter-struct",
(void*)kmachine_extras::init_autosplit_struct);
// discord rich presence
make_function_symbol_from_c("pc-discord-rpc-update", (void*)kmachine_extras::update_discord_rpc);
@@ -395,6 +396,45 @@ void InitMachine_PCPort() {
(void*)pc_get_num_external_highscores);
*/
// speedrunning stuff
make_function_symbol_from_c("pc-sr-mode-get-practice-entries-amount",
(void*)kmachine_extras::pc_sr_mode_get_practice_entries_amount);
make_function_symbol_from_c("pc-sr-mode-get-practice-entry-name",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_name);
make_function_symbol_from_c("pc-sr-mode-get-practice-entry-continue-point",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_continue_point);
make_function_symbol_from_c(
"pc-sr-mode-get-practice-entry-history-success",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_history_success);
make_function_symbol_from_c(
"pc-sr-mode-get-practice-entry-history-attempts",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_history_attempts);
make_function_symbol_from_c(
"pc-sr-mode-get-practice-entry-session-success",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_session_success);
make_function_symbol_from_c(
"pc-sr-mode-get-practice-entry-session-attempts",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_session_attempts);
make_function_symbol_from_c("pc-sr-mode-get-practice-entry-avg-time",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_avg_time);
make_function_symbol_from_c("pc-sr-mode-get-practice-entry-fastest-time",
(void*)kmachine_extras::pc_sr_mode_get_practice_entry_fastest_time);
make_function_symbol_from_c("pc-sr-mode-record-practice-entry-attempt!",
(void*)kmachine_extras::pc_sr_mode_record_practice_entry_attempt);
make_function_symbol_from_c("pc-sr-mode-init-practice-info!",
(void*)kmachine_extras::pc_sr_mode_init_practice_info);
make_function_symbol_from_c("pc-sr-mode-get-custom-category-amount",
(void*)kmachine_extras::pc_sr_mode_get_custom_category_amount);
make_function_symbol_from_c("pc-sr-mode-get-custom-category-name",
(void*)kmachine_extras::pc_sr_mode_get_custom_category_name);
make_function_symbol_from_c(
"pc-sr-mode-get-custom-category-continue-point",
(void*)kmachine_extras::pc_sr_mode_get_custom_category_continue_point);
make_function_symbol_from_c("pc-sr-mode-init-custom-category-info!",
(void*)kmachine_extras::pc_sr_mode_init_custom_category_info);
make_function_symbol_from_c("pc-sr-mode-dump-new-custom-category",
(void*)kmachine_extras::pc_sr_mode_dump_new_custom_category);
// setup string constants
auto user_dir_path = file_util::get_user_config_dir();
intern_from_c(-1, 0, "*pc-user-dir-base-path*")->value() =
+733 -1
View File
@@ -16,7 +16,7 @@
namespace jak3 {
namespace kmachine_extras {
using namespace jak3;
AutoSplitterBlock g_auto_splitter_block_jak3;
void update_discord_rpc(u32 discord_info) {
if (gDiscordRpcEnabled) {
@@ -225,5 +225,737 @@ inline bool symbol_to_bool(const u32 symptr) {
return symptr != s7.offset;
}
void init_autosplit_struct() {
g_auto_splitter_block_jak3.pointer_to_symbol =
(u64)g_ee_main_mem + (u64)intern_from_c(-1, 0, "*autosplit-info-jak3*")->value();
}
// TODO - currently using a single mutex for all background task synchronization
std::mutex background_task_lock;
std::string last_rpc_error;
// TODO - add a TTL to this
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
external_speedrun_time_cache = {};
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
external_race_time_cache = {};
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
external_highscores_cache = {};
// clang-format off
// TODO - eventually don't depend on SRC
const std::unordered_map<std::string, std::string> external_speedrun_lookup_urls = {
{"any", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/category/9d8p1qkn?embed=players&max=200"},
{"nooob", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/category/5dwj0n0k?embed=players&max=200"},
{"allmissions", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/category/xd1r98k8?embed=players&max=200"},
{"100", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/category/zd30nndn?embed=players&max=200"},
{"anyorbs", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/category/jdzw79vd?embed=players&max=200"},
{"anyhero", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/category/9kvp50kg?embed=players&max=200"}};
const std::unordered_map<std::string, std::string> external_race_lookup_urls = {
{"time-trial", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/level/kwjvyzwg/jdr8onk6?embed=players&max=200"},
{"rally", "https://www.speedrun.com/api/v1/leaderboards/nj1nww1p/level/owo3kyw6/jdr8onk6?embed=players&max=200"}};
const std::unordered_map<std::string, std::string> external_highscores_lookup_urls = {
{"was-pre-game", "https://api.jakspeedruns.workers.dev/v1/highscores/9"},
{"air-time", "https://api.jakspeedruns.workers.dev/v1/highscores/10"},
{"total-air-time", "https://api.jakspeedruns.workers.dev/v1/highscores/11"},
{"jump-distance", "https://api.jakspeedruns.workers.dev/v1/highscores/12"},
{"total-jump-distance", "https://api.jakspeedruns.workers.dev/v1/highscores/13"},
{"roll-count", "https://api.jakspeedruns.workers.dev/v1/highscores/14"},
{"wascity-gungame", "https://api.jakspeedruns.workers.dev/v1/highscores/15"},
{"jetboard", "https://api.jakspeedruns.workers.dev/v1/highscores/16"},
{"gungame-yellow-2", "https://api.jakspeedruns.workers.dev/v1/highscores/17"},
{"gungame-red-2", "https://api.jakspeedruns.workers.dev/v1/highscores/18"},
{"gungame-ratchet", "https://api.jakspeedruns.workers.dev/v1/highscores/19"},
{"gungame-clank", "https://api.jakspeedruns.workers.dev/v1/highscores/20"},
{"power-game", "https://api.jakspeedruns.workers.dev/v1/highscores/21"},
{"destroy-interceptors", "https://api.jakspeedruns.workers.dev/v1/highscores/22"}};
// clang-format on
void callback_fetch_external_speedrun_times(bool success,
const std::string& cache_id,
std::optional<std::string> result) {
std::scoped_lock lock{background_task_lock};
if (!success) {
intern_from_c(-1, 0, "*pc-rpc-error?*")->value() = bool_to_symbol(true);
if (result) {
last_rpc_error = result.value();
} else {
last_rpc_error = "Unexpected Error Occurred";
}
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
// TODO - might be nice to have an error if we get an unexpected payload
if (!result) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
// Parse the response
const auto data = safe_parse_json(result.value());
if (!data || !data->contains("data") || !data->at("data").contains("players") ||
!data->at("data").at("players").contains("data") || !data->at("data").contains("runs")) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
auto& players = data->at("data").at("players").at("data");
auto& runs = data->at("data").at("runs");
std::vector<std::pair<std::string, float>> times = {};
for (const auto& run_info : runs) {
std::pair<std::string, float> time_info;
if (players.size() > times.size() && players.at(times.size()).contains("names") &&
players.at(times.size()).at("names").contains("international")) {
time_info.first = players.at(times.size()).at("names").at("international");
} else if (players.size() > times.size() && players.at(times.size()).contains("name")) {
time_info.first = players.at(times.size()).at("name");
} else {
time_info.first = "Unknown";
}
if (run_info.contains("run") && run_info.at("run").contains("times") &&
run_info.at("run").at("times").contains("primary_t")) {
time_info.second = run_info.at("run").at("times").at("primary_t");
times.push_back(time_info);
}
}
external_speedrun_time_cache[cache_id] = times;
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
}
// TODO - duplicate code, put it in a function
void callback_fetch_external_race_times(bool success,
const std::string& cache_id,
std::optional<std::string> result) {
std::scoped_lock lock{background_task_lock};
if (!success) {
intern_from_c(-1, 0, "*pc-rpc-error?*")->value() = bool_to_symbol(true);
if (result) {
last_rpc_error = result.value();
} else {
last_rpc_error = "Unexpected Error Occurred";
}
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
// TODO - might be nice to have an error if we get an unexpected payload
if (!result) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
// Parse the response
const auto data = safe_parse_json(result.value());
if (!data || !data->contains("data") || !data->at("data").contains("players") ||
!data->at("data").at("players").contains("data") || !data->at("data").contains("runs")) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
auto& players = data->at("data").at("players").at("data");
auto& runs = data->at("data").at("runs");
std::vector<std::pair<std::string, float>> times = {};
for (const auto& run_info : runs) {
std::pair<std::string, float> time_info;
if (players.size() > times.size() && players.at(times.size()).contains("names") &&
players.at(times.size()).at("names").contains("international")) {
time_info.first = players.at(times.size()).at("names").at("international");
} else if (players.size() > times.size() && players.at(times.size()).contains("name")) {
time_info.first = players.at(times.size()).at("name");
} else {
time_info.first = "Unknown";
}
if (run_info.contains("run") && run_info.at("run").contains("times") &&
run_info.at("run").at("times").contains("primary_t")) {
time_info.second = run_info.at("run").at("times").at("primary_t");
times.push_back(time_info);
}
}
external_race_time_cache[cache_id] = times;
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
}
// TODO - duplicate code, put it in a function
void callback_fetch_external_highscores(bool success,
const std::string& cache_id,
std::optional<std::string> result) {
std::scoped_lock lock{background_task_lock};
if (!success) {
intern_from_c(-1, 0, "*pc-rpc-error?*")->value() = bool_to_symbol(true);
if (result) {
last_rpc_error = result.value();
} else {
last_rpc_error = "Unexpected Error Occurred";
}
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
// TODO - might be nice to have an error if we get an unexpected payload
if (!result) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
return;
}
// Parse the response
const auto data = safe_parse_json(result.value());
std::vector<std::pair<std::string, float>> times = {};
for (const auto& highscore_info : data.value()) {
if (highscore_info.contains("playerName") && highscore_info.contains("score")) {
std::pair<std::string, float> time_info;
time_info.first = highscore_info.at("playerName");
time_info.second = highscore_info.at("score");
times.push_back(time_info);
}
}
external_highscores_cache[cache_id] = times;
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
}
void pc_fetch_external_speedrun_times(u32 speedrun_id_ptr) {
std::scoped_lock lock{background_task_lock};
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
if (external_speedrun_lookup_urls.find(speedrun_id) == external_speedrun_lookup_urls.end()) {
lg::error("No URL for speedrun_id: '{}'", speedrun_id);
return;
}
// First check to see if we've already retrieved this info
if (external_speedrun_time_cache.find(speedrun_id) == external_speedrun_time_cache.end()) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true);
intern_from_c(-1, 0, "*pc-rpc-error?*")->value() = bool_to_symbol(false);
// otherwise, hit the URL
WebRequestJobPayload req;
req.callback = callback_fetch_external_speedrun_times;
req.url = external_speedrun_lookup_urls.at(speedrun_id);
req.cache_id = speedrun_id;
g_background_worker.enqueue_webrequest(req);
}
}
void pc_fetch_external_race_times(u32 race_id_ptr) {
std::scoped_lock lock{background_task_lock};
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
if (external_race_lookup_urls.find(race_id) == external_race_lookup_urls.end()) {
lg::error("No URL for race_id: '{}'", race_id);
return;
}
// First check to see if we've already retrieved this info
if (external_race_time_cache.find(race_id) == external_race_time_cache.end()) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true);
intern_from_c(-1, 0, "*pc-rpc-error?*")->value() = bool_to_symbol(false);
// otherwise, hit the URL
WebRequestJobPayload req;
req.callback = callback_fetch_external_race_times;
req.url = external_race_lookup_urls.at(race_id);
req.cache_id = race_id;
g_background_worker.enqueue_webrequest(req);
}
}
void pc_fetch_external_highscores(u32 highscore_id_ptr) {
std::scoped_lock lock{background_task_lock};
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
if (external_highscores_lookup_urls.find(highscore_id) == external_highscores_lookup_urls.end()) {
lg::error("No URL for highscore_id: '{}'", highscore_id);
return;
}
// First check to see if we've already retrieved this info
if (external_highscores_cache.find(highscore_id) == external_highscores_cache.end()) {
intern_from_c(-1, 0, "*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true);
intern_from_c(-1, 0, "*pc-rpc-error?*")->value() = bool_to_symbol(false);
// otherwise, hit the URL
WebRequestJobPayload req;
req.callback = callback_fetch_external_highscores;
req.url = external_highscores_lookup_urls.at(highscore_id);
req.cache_id = highscore_id;
g_background_worker.enqueue_webrequest(req);
}
}
void pc_get_external_speedrun_time(u32 speedrun_id_ptr,
s32 index,
u32 name_dest_ptr,
u32 time_dest_ptr) {
std::scoped_lock lock{background_task_lock};
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
if (external_speedrun_time_cache.find(speedrun_id) != external_speedrun_time_cache.end()) {
const auto& runs = external_speedrun_time_cache.at(speedrun_id);
if (index < (int)runs.size()) {
const auto& run_info = external_speedrun_time_cache.at(speedrun_id).at(index);
std::string converted =
get_font_bank(GameTextVersion::JAK3)->convert_utf8_to_game(run_info.first);
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
} else {
std::string converted = get_font_bank(GameTextVersion::JAK3)->convert_utf8_to_game("");
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
}
}
}
void pc_get_external_race_time(u32 race_id_ptr, s32 index, u32 name_dest_ptr, u32 time_dest_ptr) {
std::scoped_lock lock{background_task_lock};
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
if (external_race_time_cache.find(race_id) != external_race_time_cache.end()) {
const auto& runs = external_race_time_cache.at(race_id);
if (index < (int)runs.size()) {
const auto& run_info = external_race_time_cache.at(race_id).at(index);
std::string converted =
get_font_bank(GameTextVersion::JAK3)->convert_utf8_to_game(run_info.first);
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
} else {
std::string converted = get_font_bank(GameTextVersion::JAK3)->convert_utf8_to_game("");
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
}
}
}
void pc_get_external_highscore(u32 highscore_id_ptr,
s32 index,
u32 name_dest_ptr,
u32 time_dest_ptr) {
std::scoped_lock lock{background_task_lock};
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
if (external_highscores_cache.find(highscore_id) != external_highscores_cache.end()) {
const auto& runs = external_highscores_cache.at(highscore_id);
if (index < (int)runs.size()) {
const auto& run_info = external_highscores_cache.at(highscore_id).at(index);
std::string converted =
get_font_bank(GameTextVersion::JAK3)->convert_utf8_to_game(run_info.first);
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
} else {
std::string converted = get_font_bank(GameTextVersion::JAK3)->convert_utf8_to_game("");
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
}
}
}
s32 pc_get_num_external_speedrun_times(u32 speedrun_id_ptr) {
std::scoped_lock lock{background_task_lock};
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
if (external_speedrun_time_cache.find(speedrun_id) != external_speedrun_time_cache.end()) {
return external_speedrun_time_cache.at(speedrun_id).size();
}
return 0;
}
s32 pc_get_num_external_race_times(u32 race_id_ptr) {
std::scoped_lock lock{background_task_lock};
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
if (external_race_time_cache.find(race_id) != external_race_time_cache.end()) {
return external_race_time_cache.at(race_id).size();
}
return 0;
}
s32 pc_get_num_external_highscores(u32 highscore_id_ptr) {
std::scoped_lock lock{background_task_lock};
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
if (external_highscores_cache.find(highscore_id) != external_highscores_cache.end()) {
return external_highscores_cache.at(highscore_id).size();
}
return 0;
}
void to_json(json& j, const SpeedrunPracticeEntryHistoryAttempt& obj) {
if (obj.time) {
j["time"] = obj.time.value();
} else {
j["time"] = nullptr;
}
}
void from_json(const json& j, SpeedrunPracticeEntryHistoryAttempt& obj) {
if (j["time"].is_null()) {
obj.time = {};
} else {
obj.time = j["time"];
}
}
void to_json(json& j, const SpeedrunPracticeEntry& obj) {
json_serialize(name);
json_serialize(continue_point_name);
json_serialize(flags);
json_serialize(completed_task);
json_serialize(features);
json_serialize(secrets);
json_serialize(vehicles);
json_serialize(starting_position);
json_serialize(starting_rotation);
json_serialize(starting_camera_position);
json_serialize(starting_camera_rotation);
json_serialize(start_zone_v1);
json_serialize(start_zone_v2);
json_serialize_optional(end_zone_v1);
json_serialize_optional(end_zone_v2);
json_serialize_optional(end_task);
json_serialize(history);
}
void from_json(const json& j, SpeedrunPracticeEntry& obj) {
json_deserialize_if_exists(name);
json_deserialize_if_exists(continue_point_name);
json_deserialize_if_exists(flags);
json_deserialize_if_exists(completed_task);
json_deserialize_if_exists(features);
json_deserialize_if_exists(secrets);
json_deserialize_if_exists(vehicles);
json_deserialize_if_exists(starting_position);
json_deserialize_if_exists(starting_rotation);
json_deserialize_if_exists(starting_camera_position);
json_deserialize_if_exists(starting_camera_rotation);
json_deserialize_if_exists(start_zone_v1);
json_deserialize_if_exists(start_zone_v2);
json_deserialize_optional_if_exists(end_zone_v1);
json_deserialize_optional_if_exists(end_zone_v2);
json_deserialize_optional_if_exists(end_task);
json_deserialize_if_exists(history);
}
void to_json(json& j, const SpeedrunCustomCategoryEntry& obj) {
json_serialize(name);
json_serialize(secrets);
json_serialize(features);
json_serialize(vehicles);
json_serialize(forbidden_features);
json_serialize(cheats);
json_serialize(continue_point_name);
json_serialize(completed_task);
}
void from_json(const json& j, SpeedrunCustomCategoryEntry& obj) {
json_deserialize_if_exists(name);
json_deserialize_if_exists(secrets);
json_deserialize_if_exists(features);
json_deserialize_if_exists(vehicles);
json_deserialize_if_exists(forbidden_features);
json_deserialize_if_exists(cheats);
json_deserialize_if_exists(continue_point_name);
json_deserialize_if_exists(completed_task);
}
std::vector<SpeedrunPracticeEntry> g_speedrun_practice_entries;
std::unordered_map<int, SpeedrunPracticeState> g_speedrun_practice_state;
s32 pc_sr_mode_get_practice_entries_amount() {
// load practice entries from the file
const auto file_path =
file_util::get_user_features_dir(g_game_version) / "speedrun-practice.json";
if (!file_util::file_exists(file_path.string())) {
lg::info("speedrun-practice.json not found, no entries to return!");
return 0;
}
const auto file_contents = safe_parse_json(file_util::read_text_file(file_path));
if (!file_contents) {
lg::error("speedrun-practice.json could not be parsed!");
return 0;
}
g_speedrun_practice_entries = *file_contents;
for (size_t i = 0; i < g_speedrun_practice_entries.size(); i++) {
const auto& entry = g_speedrun_practice_entries.at(i);
s32 last_session_id = -1;
s32 total_attempts = 0;
s32 total_successes = 0;
s32 session_attempts = 0;
s32 session_successes = 0;
double total_time = 0;
float average_time = 0;
float fastest_time = 0;
for (const auto& [history_session, times] : entry.history) {
s32 session_id = stoi(history_session);
if (session_id > last_session_id) {
last_session_id = session_id;
}
for (const auto& time : times) {
total_attempts++;
if (time.time) {
total_successes++;
total_time += *time.time;
if (fastest_time == 0 || *time.time < fastest_time) {
fastest_time = *time.time;
}
}
}
}
if (total_successes != 0) {
average_time = total_time / total_successes;
}
g_speedrun_practice_state[i] = {last_session_id + 1, total_attempts, total_successes,
session_attempts, session_successes, total_time,
average_time, fastest_time};
}
return g_speedrun_practice_entries.size();
}
void pc_sr_mode_get_practice_entry_name(s32 entry_index, u32 name_str_ptr) {
std::string name;
if (entry_index < (int)g_speedrun_practice_entries.size()) {
name = g_speedrun_practice_entries.at(entry_index).name;
}
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
}
void pc_sr_mode_get_practice_entry_continue_point(s32 entry_index, u32 name_str_ptr) {
std::string name;
if (entry_index < (int)g_speedrun_practice_entries.size()) {
name = g_speedrun_practice_entries.at(entry_index).continue_point_name;
}
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
}
s32 pc_sr_mode_get_practice_entry_history_success(s32 entry_index) {
return g_speedrun_practice_state.at(entry_index).total_successes;
}
s32 pc_sr_mode_get_practice_entry_history_attempts(s32 entry_index) {
return g_speedrun_practice_state.at(entry_index).total_attempts;
}
s32 pc_sr_mode_get_practice_entry_session_success(s32 entry_index) {
return g_speedrun_practice_state.at(entry_index).session_successes;
}
s32 pc_sr_mode_get_practice_entry_session_attempts(s32 entry_index) {
return g_speedrun_practice_state.at(entry_index).session_attempts;
}
void pc_sr_mode_get_practice_entry_avg_time(s32 entry_index, u32 time_str_ptr) {
const auto time = fmt::format("{:.2f}", g_speedrun_practice_state.at(entry_index).average_time);
strcpy(Ptr<String>(time_str_ptr).c()->data(), time.c_str());
}
void pc_sr_mode_get_practice_entry_fastest_time(s32 entry_index, u32 time_str_ptr) {
const auto time = fmt::format("{:.2f}", g_speedrun_practice_state.at(entry_index).fastest_time);
strcpy(Ptr<String>(time_str_ptr).c()->data(), time.c_str());
}
u64 pc_sr_mode_record_practice_entry_attempt(s32 entry_index, u32 success_bool, u32 time_ptr) {
auto& state = g_speedrun_practice_state.at(entry_index);
const auto was_successful = symbol_to_bool(success_bool);
state.total_attempts++;
state.session_attempts++;
bool ret = false;
SpeedrunPracticeEntryHistoryAttempt new_history_entry;
if (was_successful) {
auto time = Ptr<float>(time_ptr).c();
new_history_entry.time = *time;
state.total_successes++;
state.session_successes++;
state.total_time += *time;
state.average_time = state.total_time / state.total_successes;
if (*time < state.fastest_time) {
state.fastest_time = *time;
ret = true;
}
}
// persist to file
const auto file_path =
file_util::get_user_features_dir(g_game_version) / "speedrun-practice.json";
if (!file_util::file_exists(file_path.string())) {
lg::info("speedrun-practice.json not found, not persisting!");
} else {
auto& history = g_speedrun_practice_entries.at(entry_index).history;
if (history.find(fmt::format("{}", state.current_session_id)) == history.end()) {
history[fmt::format("{}", state.current_session_id)] = {};
}
history[fmt::format("{}", state.current_session_id)].push_back(new_history_entry);
json data = g_speedrun_practice_entries;
file_util::write_text_file(file_path, data.dump(2));
}
// return
return bool_to_symbol(ret);
}
void pc_sr_mode_init_practice_info(s32 entry_index, u32 speedrun_practice_obj_ptr) {
if (entry_index >= (int)g_speedrun_practice_entries.size()) {
return;
}
auto objective = speedrun_practice_obj_ptr
? Ptr<SpeedrunPracticeObjective>(speedrun_practice_obj_ptr).c()
: NULL;
if (objective) {
const auto& json_info = g_speedrun_practice_entries.at(entry_index);
objective->index = entry_index;
objective->flags = json_info.flags;
objective->completed_task = json_info.completed_task;
objective->features = json_info.features;
objective->vehicles = json_info.vehicles;
objective->secrets = json_info.secrets;
auto starting_position =
objective->starting_position ? Ptr<Vector>(objective->starting_position).c() : NULL;
if (starting_position) {
for (int i = 0; i < 4; i++) {
starting_position->data[i] = json_info.starting_position.at(i) * METER_LENGTH;
}
}
auto starting_rotation =
objective->starting_rotation ? Ptr<Vector>(objective->starting_rotation).c() : NULL;
if (starting_rotation) {
for (int i = 0; i < 4; i++) {
starting_rotation->data[i] = json_info.starting_rotation.at(i);
}
}
auto starting_camera_position = objective->starting_camera_position
? Ptr<Vector>(objective->starting_camera_position).c()
: NULL;
if (starting_camera_position) {
for (int i = 0; i < 4; i++) {
starting_camera_position->data[i] = json_info.starting_camera_position.at(i) * 4096.0;
}
}
auto starting_camera_rotation = objective->starting_camera_rotation
? Ptr<Vector>(objective->starting_camera_rotation).c()
: NULL;
if (starting_camera_rotation) {
for (int i = 0; i < 16; i++) {
starting_camera_rotation->data[i] = json_info.starting_camera_rotation.at(i);
}
}
if (json_info.end_task) {
objective->end_task = *json_info.end_task;
} else {
objective->end_task = 0;
}
auto starting_zone = objective->start_zone_init_params
? Ptr<ObjectiveZoneInitParams>(objective->start_zone_init_params).c()
: NULL;
if (starting_zone) {
starting_zone->v1[0] = json_info.start_zone_v1.at(0) * METER_LENGTH;
starting_zone->v1[1] = json_info.start_zone_v1.at(1) * METER_LENGTH;
starting_zone->v1[2] = json_info.start_zone_v1.at(2) * METER_LENGTH;
starting_zone->v1[3] = json_info.start_zone_v1.at(3) * METER_LENGTH;
starting_zone->v2[0] = json_info.start_zone_v2.at(0) * METER_LENGTH;
starting_zone->v2[1] = json_info.start_zone_v2.at(1) * METER_LENGTH;
starting_zone->v2[2] = json_info.start_zone_v2.at(2) * METER_LENGTH;
starting_zone->v2[3] = json_info.start_zone_v2.at(3) * METER_LENGTH;
}
if (json_info.end_zone_v1 && json_info.end_zone_v2) {
auto ending_zone = objective->end_zone_init_params
? Ptr<ObjectiveZoneInitParams>(objective->end_zone_init_params).c()
: NULL;
if (ending_zone) {
ending_zone->v1[0] = json_info.end_zone_v1->at(0) * METER_LENGTH;
ending_zone->v1[1] = json_info.end_zone_v1->at(1) * METER_LENGTH;
ending_zone->v1[2] = json_info.end_zone_v1->at(2) * METER_LENGTH;
ending_zone->v1[3] = json_info.end_zone_v1->at(3) * METER_LENGTH;
ending_zone->v2[0] = json_info.end_zone_v2->at(0) * METER_LENGTH;
ending_zone->v2[1] = json_info.end_zone_v2->at(1) * METER_LENGTH;
ending_zone->v2[2] = json_info.end_zone_v2->at(2) * METER_LENGTH;
ending_zone->v2[3] = json_info.end_zone_v2->at(3) * METER_LENGTH;
}
}
}
}
std::vector<SpeedrunCustomCategoryEntry> g_speedrun_custom_categories;
s32 pc_sr_mode_get_custom_category_amount() {
// load practice entries from the file
const auto file_path =
file_util::get_user_features_dir(g_game_version) / "speedrun-categories.json";
if (!file_util::file_exists(file_path.string())) {
lg::info("speedrun-categories.json not found, no entries to return!");
return 0;
}
const auto file_contents = safe_parse_json(file_util::read_text_file(file_path));
if (!file_contents) {
lg::error("speedrun-categories.json could not be parsed!");
return 0;
}
g_speedrun_custom_categories = *file_contents;
return g_speedrun_custom_categories.size();
}
void pc_sr_mode_get_custom_category_name(s32 entry_index, u32 name_str_ptr) {
std::string name;
if (entry_index < (int)g_speedrun_custom_categories.size()) {
name = g_speedrun_custom_categories.at(entry_index).name;
}
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
}
void pc_sr_mode_get_custom_category_continue_point(s32 entry_index, u32 name_str_ptr) {
std::string name;
if (entry_index < (int)g_speedrun_custom_categories.size()) {
name = g_speedrun_custom_categories.at(entry_index).continue_point_name;
}
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
}
void pc_sr_mode_init_custom_category_info(s32 entry_index, u32 speedrun_custom_category_ptr) {
if (entry_index >= (int)g_speedrun_custom_categories.size()) {
return;
}
auto category = speedrun_custom_category_ptr
? Ptr<SpeedrunCustomCategory>(speedrun_custom_category_ptr).c()
: NULL;
if (category) {
const auto& json_info = g_speedrun_custom_categories.at(entry_index);
category->index = entry_index;
category->secrets = json_info.secrets;
category->features = json_info.features;
category->vehicles = json_info.vehicles;
category->forbidden_features = json_info.forbidden_features;
category->cheats = json_info.cheats;
category->completed_task = json_info.completed_task;
}
}
void pc_sr_mode_dump_new_custom_category(u32 speedrun_custom_category_ptr) {
const auto file_path =
file_util::get_user_features_dir(g_game_version) / "speedrun-categories.json";
if (file_util::file_exists(file_path.string())) {
// read current categories from file
const auto file_contents = safe_parse_json(file_util::read_text_file(file_path));
if (file_contents) {
g_speedrun_custom_categories = *file_contents;
}
}
auto category = speedrun_custom_category_ptr
? Ptr<SpeedrunCustomCategory>(speedrun_custom_category_ptr).c()
: NULL;
if (category) {
SpeedrunCustomCategoryEntry new_category;
new_category.name = fmt::format("custom-category-{}", g_speedrun_custom_categories.size());
new_category.secrets = category->secrets;
new_category.features = category->features;
new_category.vehicles = category->vehicles;
new_category.forbidden_features = category->forbidden_features;
new_category.cheats = category->cheats;
new_category.completed_task = category->completed_task;
new_category.continue_point_name = "";
g_speedrun_custom_categories.push_back(new_category);
// convert to json and write file
json data = g_speedrun_custom_categories;
file_util::write_text_file(file_path, data.dump(2));
}
}
} // namespace kmachine_extras
} // namespace jak3
+146 -2
View File
@@ -12,8 +12,47 @@ void pc_set_levels(u32 lev_list);
void pc_set_active_levels(u32 lev_list);
u32 alloc_vagdir_names(u32 heap_sym);
inline u64 bool_to_symbol(const bool val);
// TODO - move to common
void encode_utf8_string(u32 src_str_ptr, u32 str_dest_ptr);
void init_autosplit_struct();
void callback_fetch_external_speedrun_times(bool success,
const std::string& cache_id,
std::optional<std::string> result);
void callback_fetch_external_race_times(bool success,
const std::string& cache_id,
std::optional<std::string> result);
void callback_fetch_external_highscores(bool success,
const std::string& cache_id,
std::optional<std::string> result);
void pc_fetch_external_speedrun_times(u32 speedrun_id_ptr);
void pc_fetch_external_race_times(u32 race_id_ptr);
void pc_fetch_external_highscores(u32 highscore_id_ptr);
void pc_get_external_speedrun_time(u32 speedrun_id_ptr,
s32 index,
u32 name_dest_ptr,
u32 time_dest_ptr);
void pc_get_external_race_time(u32 race_id_ptr, s32 index, u32 name_dest_ptr, u32 time_dest_ptr);
void pc_get_external_highscore(u32 highscore_id_ptr,
s32 index,
u32 name_dest_ptr,
u32 time_dest_ptr);
s32 pc_get_num_external_speedrun_times(u32 speedrun_id_ptr);
s32 pc_get_num_external_race_times(u32 race_id_ptr);
s32 pc_get_num_external_highscores(u32 highscore_id_ptr);
s32 pc_sr_mode_get_practice_entries_amount();
void pc_sr_mode_get_practice_entry_name(s32 entry_index, u32 name_str_ptr);
void pc_sr_mode_get_practice_entry_continue_point(s32 entry_index, u32 name_str_ptr);
s32 pc_sr_mode_get_practice_entry_history_success(s32 entry_index);
s32 pc_sr_mode_get_practice_entry_history_attempts(s32 entry_index);
s32 pc_sr_mode_get_practice_entry_session_success(s32 entry_index);
s32 pc_sr_mode_get_practice_entry_session_attempts(s32 entry_index);
void pc_sr_mode_get_practice_entry_avg_time(s32 entry_index, u32 time_str_ptr);
void pc_sr_mode_get_practice_entry_fastest_time(s32 entry_index, u32 time_str_ptr);
u64 pc_sr_mode_record_practice_entry_attempt(s32 entry_index, u32 success_bool, u32 time);
void pc_sr_mode_init_practice_info(s32 entry_index, u32 speedrun_practice_obj_ptr);
s32 pc_sr_mode_get_custom_category_amount();
void pc_sr_mode_get_custom_category_name(s32 entry_index, u32 name_str_ptr);
void pc_sr_mode_get_custom_category_continue_point(s32 entry_index, u32 name_str_ptr);
void pc_sr_mode_init_custom_category_info(s32 entry_index, u32 speedrun_custom_category_ptr);
void pc_sr_mode_dump_new_custom_category(u32 speedrun_custom_category_ptr);
struct DiscordInfo {
float orb_count; // float
@@ -113,5 +152,110 @@ enum class FocusStatus : u64 {
#define FOCUS_TEST(status, foc) (status.test(static_cast<size_t>(foc)))
// To speed up finding the auto-splitter block in GOAL memory
// all this has is a marker for LiveSplit to find, and then the pointer
// to the symbol
struct AutoSplitterBlock {
const char marker[20] = "UnLiStEdStRaTs_JaK3";
u64 pointer_to_symbol = 0;
};
extern AutoSplitterBlock g_auto_splitter_block_jak3;
struct SpeedrunPracticeEntryHistoryAttempt {
std::optional<float> time;
};
void to_json(json& j, const SpeedrunPracticeEntryHistoryAttempt& obj);
void from_json(const json& j, SpeedrunPracticeEntryHistoryAttempt& obj);
struct SpeedrunPracticeEntry {
std::string name;
std::string continue_point_name;
u64 flags;
u64 completed_task;
u64 features;
u64 secrets;
u64 vehicles;
std::vector<float> starting_position;
std::vector<float> starting_rotation;
std::vector<float> starting_camera_position;
std::vector<float> starting_camera_rotation;
std::vector<float> start_zone_v1;
std::vector<float> start_zone_v2;
std::optional<std::vector<float>> end_zone_v1;
std::optional<std::vector<float>> end_zone_v2;
std::optional<u64> end_task;
std::map<std::string, std::vector<SpeedrunPracticeEntryHistoryAttempt>> history;
};
void to_json(json& j, const SpeedrunPracticeEntry& obj);
void from_json(const json& j, SpeedrunPracticeEntry& obj);
struct SpeedrunPracticeState {
s32 current_session_id;
s32 total_attempts;
s32 total_successes;
s32 session_attempts;
s32 session_successes;
double total_time;
float average_time;
float fastest_time;
};
struct ObjectiveZoneInitParams {
float v1[4];
float v2[4];
};
struct Vector {
float data[4];
};
struct Matrix {
float data[16];
};
struct SpeedrunPracticeObjective {
s32 index;
u8 pad1[4];
u64 flags;
u8 completed_task;
u8 pad2[7];
u64 features;
u64 secrets;
u64 vehicles;
u32 starting_position; // Vector
u32 starting_rotation; // Vector
u32 starting_camera_position; // Vector
u32 starting_camera_rotation; // Matrix
u8 end_task;
u32 start_zone_init_params; // ObjectiveZoneInitParams
u32 start_zone; // irrelevant for cpp
u32 end_zone_init_params; // ObjectiveZoneInitParams
u32 end_zone; // irrelevant for cpp
};
struct SpeedrunCustomCategoryEntry {
std::string name;
u64 secrets;
u64 features;
u64 vehicles;
u64 forbidden_features;
u64 cheats;
std::string continue_point_name;
u64 completed_task;
};
void to_json(json& j, const SpeedrunCustomCategoryEntry& obj);
void from_json(const json& j, SpeedrunCustomCategoryEntry& obj);
struct SpeedrunCustomCategory {
s32 index;
u64 secrets;
u64 features;
u64 vehicles;
u64 forbidden_features;
u64 cheats;
u8 completed_task;
};
} // namespace kmachine_extras
} // namespace jak3