Files
Tyler Wilding d3cc739e43 jakx: Commit existing work from other PRs (#4112)
This attempts to get into master whatever work was done in this PR /
it's earlier PR https://github.com/open-goal/jak-project/pull/3965

I don't want this work to be lost / floating around in massive PRs.

However the changes are:
- switch to ntsc_v1 instead of PAL as the development target, as we have
done for all other games
- remove most of the copied-from-jak2/3 changes as they need to be
confirmed during the decompilation process not just assumed
- avoids committing any changes to `game/kernel/common` as it was not
clear to me if these were changes made in jak x's kernel that were not
properly broken out into it's own functions. We don't want to
accidentally introduce bugs into jak1-3's kernel code.
- in other words, if the change in the kernel only happens in jak x...it
should likely be specific to jak x's kernel, not common.

---------

Co-authored-by: VodBox <dillon@vodbox.io>
Co-authored-by: yodah <greenboyyodah@gmail.com>
2025-12-31 21:08:44 -05:00

962 lines
38 KiB
C++

#include "kmachine_extras.h"
#include <bitset>
#include <regex>
#include "kscheme.h"
#include "common/symbols.h"
#include "common/util/font/font_utils.h"
#include "game/external/discord_jakx.h"
#include "game/kernel/common/Symbol4.h"
#include "game/kernel/common/kmachine.h"
#include "game/kernel/common/kscheme.h"
#include "game/overlord/jakx/iso_cd.h"
namespace jakx {
namespace kmachine_extras {
AutoSplitterBlock g_auto_splitter_block_jakx;
void update_discord_rpc(u32 discord_info) {
if (gDiscordRpcEnabled) {
DiscordRichPresence rpc;
char state[128];
char large_image_key[128];
char large_image_text[128];
char small_image_key[128];
char small_image_text[128];
auto info = discord_info ? Ptr<DiscordInfo>(discord_info).c() : NULL;
if (info) {
// Get the data from GOAL
int orbs = (int)info->orb_count;
int gems = (int)info->gem_count;
// convert encodings
std::string status = get_font_bank(GameTextVersion::JAKX)
->convert_game_to_utf8(Ptr<String>(info->status).c()->data());
// get rid of special encodings like <COLOR_WHITE>
std::regex r("<.*?>");
while (std::regex_search(status, r)) {
status = std::regex_replace(status, r, "");
}
char* level = Ptr<String>(info->level).c()->data();
auto cutscene = Ptr<Symbol4<u32>>(info->cutscene)->value();
float time = info->time_of_day;
float percent_completed = info->percent_completed;
std::bitset<64> focus_status = info->focus_status;
auto vehicle = static_cast<VehicleType>(info->current_vehicle);
char* task = Ptr<String>(info->task).c()->data();
// Construct the DiscordRPC Object
const char* full_level_name =
get_full_level_name(level_names, level_name_remap, Ptr<String>(info->level).c()->data());
memset(&rpc, 0, sizeof(rpc));
// if we have an active task, set the mission specific image for it
if (strcmp(task, "unknown") != 0) {
strcpy(large_image_key, task);
} else {
// if we are in an outdoors level, use the picture for the corresponding time of day
if (!indoors(indoor_levels, level)) {
char level_with_tod[128];
strcpy(level_with_tod, level);
strcat(level_with_tod, "-");
strcat(level_with_tod, time_of_day_str(time));
strcpy(large_image_key, level_with_tod);
} else {
strcpy(large_image_key, level);
}
}
strcpy(large_image_text, full_level_name);
if (!strcmp(full_level_name, "unknown")) {
strcpy(large_image_key, full_level_name);
strcpy(large_image_text, level);
}
rpc.largeImageKey = large_image_key;
if (cutscene != offset_of_s7()) {
strcpy(state, "Watching a cutscene");
// temporarily move these counters to the large image tooltip during a cutscene
strcat(large_image_text,
fmt::format(" | {:.0f}% | Orbs: {} | Gems: {} | {}", percent_completed,
std::to_string(orbs), std::to_string(gems), get_time_of_day(time))
.c_str());
} else {
strcpy(state, fmt::format("{:.0f}% | Orbs: {} | Gems: {} | {}", percent_completed,
std::to_string(orbs), std::to_string(gems), get_time_of_day(time))
.c_str());
}
rpc.largeImageText = large_image_text;
rpc.state = state;
// check for any special conditions to display for the small image
if (FOCUS_TEST(focus_status, FocusStatus::Board)) {
strcpy(small_image_key, "focus-status-board");
strcpy(small_image_text, "On the JET-Board");
} else if (FOCUS_TEST(focus_status, FocusStatus::Mech)) {
strcpy(small_image_key, "focus-status-mech");
strcpy(small_image_text, "Controlling a Dark Maker bot");
} else if (FOCUS_TEST(focus_status, FocusStatus::Pilot)) {
// TODO vehicle images
strcpy(small_image_key, "focus-status-pilot");
auto vehicle_name = VehicleTypeToString(vehicle);
if (!strcmp(task, "comb-travel") || !strcmp(task, "comb-wild-ride")) {
strcpy(small_image_text, "Driving the Catacombs Rail Rider");
} else if (!strcmp(task, "desert-glide")) {
strcpy(small_image_text, "Flying the Glider");
} else if (!strcmp(task, "factory-sky-battle")) {
strcpy(small_image_text, "Flying the Hellcat");
} else {
if (vehicle_name != "Unknown") {
strcpy(small_image_text, fmt::format("Driving the {}", vehicle_name).c_str());
} else {
strcpy(small_image_key, "");
strcpy(small_image_text, "");
}
}
} else if (FOCUS_TEST(focus_status, FocusStatus::Indax)) {
strcpy(small_image_key, "focus-status-indax");
strcpy(small_image_text, "Playing as Daxter");
} else if (FOCUS_TEST(focus_status, FocusStatus::Dark)) {
strcpy(small_image_key, "focus-status-dark");
strcpy(small_image_text, "Dark Jak");
} else if (FOCUS_TEST(focus_status, FocusStatus::Light)) {
strcpy(small_image_key, "focus-status-light");
strcpy(small_image_text, "Light Jak");
} else if (FOCUS_TEST(focus_status, FocusStatus::Turret)) {
strcpy(small_image_key, "focus-status-turret");
strcpy(small_image_text, "In a Gunpod");
} else if (FOCUS_TEST(focus_status, FocusStatus::Gun)) {
strcpy(small_image_key, "focus-status-gun");
strcpy(small_image_text, "Using a Gun");
} else {
strcpy(small_image_key, "");
strcpy(small_image_text, "");
}
rpc.smallImageKey = small_image_key;
rpc.smallImageText = small_image_text;
rpc.startTimestamp = gStartTime;
rpc.details = status.c_str();
rpc.partySize = 0;
rpc.partyMax = 0;
Discord_UpdatePresence(&rpc);
}
} else {
Discord_ClearPresence();
}
}
void pc_set_levels(u32 lev_list) {
if (!Gfx::GetCurrentRenderer()) {
return;
}
std::vector<std::string> levels;
for (int i = 0; i < LEVEL_MAX; i++) {
u32 lev = *Ptr<u32>(lev_list + i * 4);
std::string ls = Ptr<String>(lev).c()->data();
if (ls != "none" && ls != "#f" && ls != "") {
levels.push_back(ls);
}
}
Gfx::GetCurrentRenderer()->set_levels(levels);
}
void pc_set_active_levels(u32 lev_list) {
if (!Gfx::GetCurrentRenderer()) {
return;
}
std::vector<std::string> levels;
for (int i = 0; i < LEVEL_MAX; i++) {
u32 lev = *Ptr<u32>(lev_list + i * 4);
std::string ls = Ptr<String>(lev).c()->data();
if (ls != "none" && ls != "#f" && ls != "") {
levels.push_back(ls);
}
}
Gfx::GetCurrentRenderer()->set_active_levels(levels);
}
static std::string unpack_vag_name_jakx(u64 compressed) {
const char* char_map = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
u32 chars = compressed & 0x1fffff;
std::array<char, 9> buf{};
buf.fill(0);
for (int i = 0; i < 8; i++) {
if (i == 4) {
chars = (compressed >> 21) & 0x1fffff;
}
buf[7 - i] = char_map[chars % 38];
chars /= 38;
}
return {buf.data()};
}
u32 alloc_vagdir_names(u32 heap_sym) {
auto alloced_heap = (Ptr<u64>)alloc_heap_memory(heap_sym, g_VagDir.num_entries * 8 + 8);
if (alloced_heap.offset) {
*alloced_heap = g_VagDir.num_entries;
// use entry -1 to get the amount
alloced_heap = alloced_heap + 8;
for (size_t i = 0; i < g_VagDir.num_entries; ++i) {
char vagname_temp[9];
u64 packed = *(u64*)g_VagDir.entries[i].words;
auto name = unpack_vag_name_jakx(packed);
memcpy(vagname_temp, name.data(), 8);
for (int j = 0; j < 8; ++j) {
vagname_temp[j] = tolower(vagname_temp[j]);
}
vagname_temp[8] = 0;
u64 vagname_val;
memcpy(&vagname_val, vagname_temp, 8);
*(alloced_heap + i * 8) = vagname_val;
}
return alloced_heap.offset;
}
return s7.offset;
}
inline u64 bool_to_symbol(const bool val) {
return val ? static_cast<u64>(s7.offset) + true_symbol_offset(g_game_version) : s7.offset;
}
inline bool symbol_to_bool(const u32 symptr) {
return symptr != s7.offset;
}
void init_autosplit_struct() {
g_auto_splitter_block_jakx.pointer_to_symbol =
(u64)g_ee_main_mem + (u64)intern_from_c(-1, 0, "*autosplit-info-jakx*")->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::JAKX)->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::JAKX)->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::JAKX)->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::JAKX)->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::JAKX)->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::JAKX)->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 jakx