mirror of
https://github.com/open-goal/jak-project
synced 2026-05-23 06:54:31 -04:00
7543acfb8a
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.
962 lines
38 KiB
C++
962 lines
38 KiB
C++
#include "kmachine_extras.h"
|
|
|
|
#include <bitset>
|
|
#include <regex>
|
|
|
|
#include "kscheme.h"
|
|
|
|
#include "common/symbols.h"
|
|
#include "common/util/FontUtils.h"
|
|
|
|
#include "game/external/discord_jak3.h"
|
|
#include "game/kernel/common/Symbol4.h"
|
|
#include "game/kernel/common/kmachine.h"
|
|
#include "game/kernel/common/kscheme.h"
|
|
#include "game/overlord/jak3/iso_cd.h"
|
|
|
|
namespace jak3 {
|
|
namespace kmachine_extras {
|
|
AutoSplitterBlock g_auto_splitter_block_jak3;
|
|
|
|
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::JAK3)
|
|
->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_jak3(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_jak3(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_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
|