diff --git a/common/util/FileUtil.cpp b/common/util/FileUtil.cpp index 0d7527599..c88bf9907 100644 --- a/common/util/FileUtil.cpp +++ b/common/util/FileUtil.cpp @@ -98,6 +98,13 @@ fs::path get_user_misc_dir(GameVersion game_version) { return get_user_config_dir() / game_version_name / "misc"; } +fs::path get_user_features_dir(GameVersion game_version) { + auto game_version_name = game_version_names[game_version]; + auto path = get_user_config_dir() / game_version_name / "features"; + file_util::create_dir_if_needed(path); + return path; +} + struct { bool initialized = false; fs::path path_to_data; diff --git a/common/util/FileUtil.h b/common/util/FileUtil.h index 3b1c50de1..e33e1be09 100644 --- a/common/util/FileUtil.h +++ b/common/util/FileUtil.h @@ -33,6 +33,7 @@ fs::path get_user_settings_dir(GameVersion game_version); fs::path get_user_memcard_dir(GameVersion game_version); fs::path get_user_screenshots_dir(GameVersion game_version); fs::path get_user_misc_dir(GameVersion game_version); +fs::path get_user_features_dir(GameVersion game_version); fs::path get_jak_project_dir(); bool create_dir_if_needed(const fs::path& path); diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index b2ed91b6a..675c53e4b 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -114,6 +114,7 @@ set(RUNTIME_SOURCE kernel/jak2/klink.cpp kernel/jak2/klisten.cpp kernel/jak2/kmachine.cpp + kernel/jak2/kmachine_extras.cpp kernel/jak2/kmalloc.cpp kernel/jak2/kprint.cpp kernel/jak2/kscheme.cpp diff --git a/game/kernel/jak2/kmachine.cpp b/game/kernel/jak2/kmachine.cpp index 13e4b24fe..aa11bc7fd 100644 --- a/game/kernel/jak2/kmachine.cpp +++ b/game/kernel/jak2/kmachine.cpp @@ -33,6 +33,7 @@ #include "game/kernel/jak2/kdgo.h" #include "game/kernel/jak2/klink.h" #include "game/kernel/jak2/klisten.h" +#include "game/kernel/jak2/kmachine_extras.h" #include "game/kernel/jak2/kmalloc.h" #include "game/kernel/jak2/kscheme.h" #include "game/kernel/jak2/ksound.h" @@ -362,8 +363,6 @@ void InitIOP() { printf("InitIOP OK\n"); } -AutoSplitterBlock g_auto_splitter_block_jak2; - int InitMachine() { // heap_start = malloc(0x10); // set up global heap (modified, the default size in the release game is 32 MB in all cases) @@ -524,532 +523,6 @@ u64 kopen(u64 fs, u64 name, u64 mode) { return fs; } -/*! - * PC port functions START - */ - -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(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::JAK2) - ->convert_game_to_utf8(Ptr(info->status).c()->data()); - - // get rid of special encodings like - std::regex r("<.*?>"); - while (std::regex_search(status, r)) { - status = std::regex_replace(status, r, ""); - } - - char* level = Ptr(info->level).c()->data(); - auto cutscene = Ptr>(info->cutscene)->value(); - float time = info->time_of_day; - float percent_completed = info->percent_completed; - std::bitset<32> focus_status; - focus_status = info->focus_status; - char* task = Ptr(info->task).c()->data(); - - // Construct the DiscordRPC Object - const char* full_level_name = - get_full_level_name(level_names, level_name_remap, Ptr(info->level).c()->data()); - memset(&rpc, 0, sizeof(rpc)); - // if we have an active task, set the mission specific image for it - // also small hack to prevent oracle image from showing up while inside levels - // like hideout, onintent, etc. - if (strcmp(task, "unknown") != 0 && strcmp(task, "city-oracle") != 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, "In the Titan Suit"); - } else if (FOCUS_TEST(focus_status, FocusStatus::Pilot)) { - strcpy(small_image_key, "focus-status-pilot"); - strcpy(small_image_text, "Driving a Zoomer"); - } 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::Disable) && - FOCUS_TEST(focus_status, FocusStatus::Grabbed)) { - // being in a turret sets disable and grabbed flags - 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 levels; - for (int i = 0; i < LEVEL_MAX; i++) { - u32 lev = *Ptr(lev_list + i * 4); - std::string ls = Ptr(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 levels; - for (int i = 0; i < LEVEL_MAX; i++) { - u32 lev = *Ptr(lev_list + i * 4); - std::string ls = Ptr(lev).c()->data(); - if (ls != "none" && ls != "#f" && ls != "") { - levels.push_back(ls); - } - } - - Gfx::GetCurrentRenderer()->set_active_levels(levels); -} - -void init_autosplit_struct() { - g_auto_splitter_block_jak2.pointer_to_symbol = - (u64)g_ee_main_mem + (u64)intern_from_c("*autosplit-info-jak2*")->value(); -} - -u32 alloc_vagdir_names(u32 heap_sym) { - auto alloced_heap = (Ptr)alloc_heap_memory(heap_sym, gVagDir.count * 8 + 8); - if (alloced_heap.offset) { - *alloced_heap = gVagDir.count; - // use entry -1 to get the amount - alloced_heap = alloced_heap + 8; - for (size_t i = 0; i < gVagDir.count; ++i) { - char vagname_temp[9]; - memcpy(vagname_temp, gVagDir.vag[i].name, 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(s7.offset) + true_symbol_offset(g_game_version) : s7.offset; -} - -// TODO - move to common -void encode_utf8_string(u32 src_str_ptr, u32 str_dest_ptr) { - auto str = std::string(Ptr(src_str_ptr).c()->data()); - std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(str); - strcpy(Ptr(str_dest_ptr).c()->data(), converted.c_str()); -} - -// 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>> - external_speedrun_time_cache = {}; -std::unordered_map>> - external_race_time_cache = {}; -std::unordered_map>> - external_highscores_cache = {}; - -// clang-format off -// TODO - eventually don't depend on SRC -const std::unordered_map external_speedrun_lookup_urls = { - {"any", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/n2y6y4ed?embed=players&max=200"}, - {"anyhoverless", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/7kjyn5gk?embed=players&max=200"}, - {"allmissions", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/xk96myxk?embed=players&max=200"}, - {"100", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/z27exp5k?embed=players&max=200"}, - {"anyorbs", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/zdn3vm72?embed=players&max=200"}, - {"anyhero", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/q25pv0wd?embed=players&max=200"}}; -const std::unordered_map external_race_lookup_urls = { - {"class3", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/y9m7qmx9/jdr0mg0d?embed=players&max=200"}, - {"class2", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/5wk5zmpw/jdr0mg0d?embed=players&max=200"}, - {"class1", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/5922g639/jdr0mg0d?embed=players&max=200"}, - {"class3rev", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/29v4e8l9/jdr0mg0d?embed=players&max=200"}, - {"class2rev", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/xd4475rd/jdr0mg0d?embed=players&max=200"}, - {"class1rev", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/xd0mre4w/jdr0mg0d?embed=players&max=200"}, - {"erol", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/rw68p7gd/jdr0mg0d?embed=players&max=200"}, - {"port", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/n93v5xzd/jdr0mg0d?embed=players&max=200"}}; -const std::unordered_map external_highscores_lookup_urls = { - {"scatter", "https://api.jakspeedruns.workers.dev/v1/highscores/2"}, - {"blaster", "https://api.jakspeedruns.workers.dev/v1/highscores/3"}, - {"vulcan", "https://api.jakspeedruns.workers.dev/v1/highscores/4"}, - {"peacemaker", "https://api.jakspeedruns.workers.dev/v1/highscores/5"}, - {"jetboard", "https://api.jakspeedruns.workers.dev/v1/highscores/6"}, - {"onin", "https://api.jakspeedruns.workers.dev/v1/highscores/7"}, - {"mash", "https://api.jakspeedruns.workers.dev/v1/highscores/8"}}; -// clang-format on - -void callback_fetch_external_speedrun_times(bool success, - const std::string& cache_id, - std::optional result) { - std::scoped_lock lock{background_task_lock}; - - if (!success) { - intern_from_c("*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("*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("*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("*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> times = {}; - for (const auto& run_info : runs) { - std::pair 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("*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 result) { - std::scoped_lock lock{background_task_lock}; - - if (!success) { - intern_from_c("*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("*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("*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("*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> times = {}; - for (const auto& run_info : runs) { - std::pair 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("*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 result) { - std::scoped_lock lock{background_task_lock}; - - if (!success) { - intern_from_c("*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("*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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false); - return; - } - - // Parse the response - const auto data = safe_parse_json(result.value()); - std::vector> times = {}; - for (const auto& highscore_info : data.value()) { - if (highscore_info.contains("playerName") && highscore_info.contains("score")) { - std::pair 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("*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(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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true); - intern_from_c("*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(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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true); - intern_from_c("*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(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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true); - intern_from_c("*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(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::JAK2)->convert_utf8_to_game(run_info.first); - strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); - *(Ptr(time_dest_ptr).c()) = run_info.second; - } else { - std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(""); - strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); - *(Ptr(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(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::JAK2)->convert_utf8_to_game(run_info.first); - strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); - *(Ptr(time_dest_ptr).c()) = run_info.second; - } else { - std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(""); - strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); - *(Ptr(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(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::JAK2)->convert_utf8_to_game(run_info.first); - strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); - *(Ptr(time_dest_ptr).c()) = run_info.second; - } else { - std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(""); - strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); - *(Ptr(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(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(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(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 InitMachine_PCPort() { // PC Port added functions init_common_pc_port_functions( @@ -1062,33 +535,78 @@ void InitMachine_PCPort() { }, make_string_from_c); - make_function_symbol_from_c("__pc-set-levels", (void*)pc_set_levels); - make_function_symbol_from_c("__pc-set-active-levels", (void*)pc_set_active_levels); + make_function_symbol_from_c("__pc-set-levels", (void*)kmachine_extras::pc_set_levels); + 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_jak2_texture_dest_offset); - make_function_symbol_from_c("pc-init-autosplitter-struct", (void*)init_autosplit_struct); - make_function_symbol_from_c("pc-encode-utf8-string", (void*)encode_utf8_string); + make_function_symbol_from_c("pc-init-autosplitter-struct", + (void*)kmachine_extras::init_autosplit_struct); + make_function_symbol_from_c("pc-encode-utf8-string", (void*)kmachine_extras::encode_utf8_string); // discord rich presence - make_function_symbol_from_c("pc-discord-rpc-update", (void*)update_discord_rpc); + make_function_symbol_from_c("pc-discord-rpc-update", (void*)kmachine_extras::update_discord_rpc); // debugging tools - make_function_symbol_from_c("alloc-vagdir-names", (void*)alloc_vagdir_names); + make_function_symbol_from_c("alloc-vagdir-names", (void*)kmachine_extras::alloc_vagdir_names); // external RPCs make_function_symbol_from_c("pc-fetch-external-speedrun-times", - (void*)pc_fetch_external_speedrun_times); - make_function_symbol_from_c("pc-fetch-external-race-times", (void*)pc_fetch_external_race_times); - make_function_symbol_from_c("pc-fetch-external-highscores", (void*)pc_fetch_external_highscores); + (void*)kmachine_extras::pc_fetch_external_speedrun_times); + make_function_symbol_from_c("pc-fetch-external-race-times", + (void*)kmachine_extras::pc_fetch_external_race_times); + make_function_symbol_from_c("pc-fetch-external-highscores", + (void*)kmachine_extras::pc_fetch_external_highscores); make_function_symbol_from_c("pc-get-external-speedrun-time", - (void*)pc_get_external_speedrun_time); - make_function_symbol_from_c("pc-get-external-race-time", (void*)pc_get_external_race_time); - make_function_symbol_from_c("pc-get-external-highscore", (void*)pc_get_external_highscore); + (void*)kmachine_extras::pc_get_external_speedrun_time); + make_function_symbol_from_c("pc-get-external-race-time", + (void*)kmachine_extras::pc_get_external_race_time); + make_function_symbol_from_c("pc-get-external-highscore", + (void*)kmachine_extras::pc_get_external_highscore); make_function_symbol_from_c("pc-get-num-external-speedrun-times", - (void*)pc_get_num_external_speedrun_times); + (void*)kmachine_extras::pc_get_num_external_speedrun_times); make_function_symbol_from_c("pc-get-num-external-race-times", - (void*)pc_get_num_external_race_times); + (void*)kmachine_extras::pc_get_num_external_race_times); make_function_symbol_from_c("pc-get-num-external-highscores", - (void*)pc_get_num_external_highscores); + (void*)kmachine_extras::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(); diff --git a/game/kernel/jak2/kmachine.h b/game/kernel/jak2/kmachine.h index 0d39a5558..e740f2aa2 100644 --- a/game/kernel/jak2/kmachine.h +++ b/game/kernel/jak2/kmachine.h @@ -53,64 +53,4 @@ struct MouseInfo { // (speedy float :offset 108) }; -enum class FocusStatus : u32 { - Disable = 0, - Dead = 1, - Ignore = 2, - Inactive = 3, - Dangerous = 4, - InAir = 5, - Hit = 6, - Grabbed = 7, - InHead = 8, - TouchWater = 9, - OnWater = 10, - UnderWater = 11, - EdgeGrab = 12, - Pole = 13, - PilotRiding = 14, - Flut = 15, - Tube = 16, - Ice = 17, - Board = 18, - Gun = 19, - Pilot = 20, - Mech = 21, - Dark = 22, - Rail = 23, - Halfpipe = 24, - Carry = 25, - Super = 26, - Shooting = 27, - Indax = 28, - Arrestable = 29, - Teleporting = 30, - FS31 = 31, - Max = 32 -}; - -#define FOCUS_TEST(status, foc) (status.test(static_cast(foc))) - -struct DiscordInfo { - float orb_count; // float - float gem_count; // float - u32 death_count; // int32 - u32 status; // string - u32 level; // string - u32 cutscene; // symbol - bool - float time_of_day; // float - float percent_completed; // float - u32 focus_status; // uint32 - u32 task; // string -}; -// To speedup 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_JaK2"; - u64 pointer_to_symbol = 0; -}; - -extern AutoSplitterBlock g_auto_splitter_block_jak2; - } // namespace jak2 diff --git a/game/kernel/jak2/kmachine_extras.cpp b/game/kernel/jak2/kmachine_extras.cpp new file mode 100644 index 000000000..186c2a38b --- /dev/null +++ b/game/kernel/jak2/kmachine_extras.cpp @@ -0,0 +1,931 @@ +#include "kmachine_extras.h" + +#include +#include + +#include "kscheme.h" + +#include "common/symbols.h" +#include "common/util/FontUtils.h" + +#include "game/external/discord.h" +#include "game/external/discord_jak1.h" +#include "game/external/discord_jak2.h" +#include "game/kernel/common/Symbol4.h" +#include "game/kernel/common/kmachine.h" +#include "game/kernel/common/kscheme.h" +#include "game/overlord/jak2/iso.h" + +namespace kmachine_extras { +using namespace jak2; + +AutoSplitterBlock g_auto_splitter_block_jak2; + +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(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::JAK2) + ->convert_game_to_utf8(Ptr(info->status).c()->data()); + + // get rid of special encodings like + std::regex r("<.*?>"); + while (std::regex_search(status, r)) { + status = std::regex_replace(status, r, ""); + } + + char* level = Ptr(info->level).c()->data(); + auto cutscene = Ptr>(info->cutscene)->value(); + float time = info->time_of_day; + float percent_completed = info->percent_completed; + std::bitset<32> focus_status; + focus_status = info->focus_status; + char* task = Ptr(info->task).c()->data(); + + // Construct the DiscordRPC Object + const char* full_level_name = + get_full_level_name(level_names, level_name_remap, Ptr(info->level).c()->data()); + memset(&rpc, 0, sizeof(rpc)); + // if we have an active task, set the mission specific image for it + // also small hack to prevent oracle image from showing up while inside levels + // like hideout, onintent, etc. + if (strcmp(task, "unknown") != 0 && strcmp(task, "city-oracle") != 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, "In the Titan Suit"); + } else if (FOCUS_TEST(focus_status, FocusStatus::Pilot)) { + strcpy(small_image_key, "focus-status-pilot"); + strcpy(small_image_text, "Driving a Zoomer"); + } 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::Disable) && + FOCUS_TEST(focus_status, FocusStatus::Grabbed)) { + // being in a turret sets disable and grabbed flags + 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 levels; + for (int i = 0; i < LEVEL_MAX; i++) { + u32 lev = *Ptr(lev_list + i * 4); + std::string ls = Ptr(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 levels; + for (int i = 0; i < LEVEL_MAX; i++) { + u32 lev = *Ptr(lev_list + i * 4); + std::string ls = Ptr(lev).c()->data(); + if (ls != "none" && ls != "#f" && ls != "") { + levels.push_back(ls); + } + } + + Gfx::GetCurrentRenderer()->set_active_levels(levels); +} + +u32 alloc_vagdir_names(u32 heap_sym) { + auto alloced_heap = (Ptr)alloc_heap_memory(heap_sym, gVagDir.count * 8 + 8); + if (alloced_heap.offset) { + *alloced_heap = gVagDir.count; + // use entry -1 to get the amount + alloced_heap = alloced_heap + 8; + for (size_t i = 0; i < gVagDir.count; ++i) { + char vagname_temp[9]; + memcpy(vagname_temp, gVagDir.vag[i].name, 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(s7.offset) + true_symbol_offset(g_game_version) : s7.offset; +} + +inline bool symbol_to_bool(const u32 symptr) { + return symptr != s7.offset; +} + +// TODO - move to common +void encode_utf8_string(u32 src_str_ptr, u32 str_dest_ptr) { + auto str = std::string(Ptr(src_str_ptr).c()->data()); + std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(str); + strcpy(Ptr(str_dest_ptr).c()->data(), converted.c_str()); +} + +void init_autosplit_struct() { + g_auto_splitter_block_jak2.pointer_to_symbol = + (u64)g_ee_main_mem + (u64)intern_from_c("*autosplit-info-jak2*")->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>> + external_speedrun_time_cache = {}; +std::unordered_map>> + external_race_time_cache = {}; +std::unordered_map>> + external_highscores_cache = {}; + +// clang-format off +// TODO - eventually don't depend on SRC +const std::unordered_map external_speedrun_lookup_urls = { + {"any", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/n2y6y4ed?embed=players&max=200"}, + {"anyhoverless", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/7kjyn5gk?embed=players&max=200"}, + {"allmissions", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/xk96myxk?embed=players&max=200"}, + {"100", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/z27exp5k?embed=players&max=200"}, + {"anyorbs", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/zdn3vm72?embed=players&max=200"}, + {"anyhero", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/category/q25pv0wd?embed=players&max=200"}}; +const std::unordered_map external_race_lookup_urls = { + {"class3", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/y9m7qmx9/jdr0mg0d?embed=players&max=200"}, + {"class2", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/5wk5zmpw/jdr0mg0d?embed=players&max=200"}, + {"class1", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/5922g639/jdr0mg0d?embed=players&max=200"}, + {"class3rev", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/29v4e8l9/jdr0mg0d?embed=players&max=200"}, + {"class2rev", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/xd4475rd/jdr0mg0d?embed=players&max=200"}, + {"class1rev", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/xd0mre4w/jdr0mg0d?embed=players&max=200"}, + {"erol", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/rw68p7gd/jdr0mg0d?embed=players&max=200"}, + {"port", "https://www.speedrun.com/api/v1/leaderboards/3dxk47y1/level/n93v5xzd/jdr0mg0d?embed=players&max=200"}}; +const std::unordered_map external_highscores_lookup_urls = { + {"scatter", "https://api.jakspeedruns.workers.dev/v1/highscores/2"}, + {"blaster", "https://api.jakspeedruns.workers.dev/v1/highscores/3"}, + {"vulcan", "https://api.jakspeedruns.workers.dev/v1/highscores/4"}, + {"peacemaker", "https://api.jakspeedruns.workers.dev/v1/highscores/5"}, + {"jetboard", "https://api.jakspeedruns.workers.dev/v1/highscores/6"}, + {"onin", "https://api.jakspeedruns.workers.dev/v1/highscores/7"}, + {"mash", "https://api.jakspeedruns.workers.dev/v1/highscores/8"}}; +// clang-format on + +void callback_fetch_external_speedrun_times(bool success, + const std::string& cache_id, + std::optional result) { + std::scoped_lock lock{background_task_lock}; + + if (!success) { + intern_from_c("*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("*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("*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("*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> times = {}; + for (const auto& run_info : runs) { + std::pair 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("*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 result) { + std::scoped_lock lock{background_task_lock}; + + if (!success) { + intern_from_c("*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("*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("*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("*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> times = {}; + for (const auto& run_info : runs) { + std::pair 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("*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 result) { + std::scoped_lock lock{background_task_lock}; + + if (!success) { + intern_from_c("*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("*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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false); + return; + } + + // Parse the response + const auto data = safe_parse_json(result.value()); + std::vector> times = {}; + for (const auto& highscore_info : data.value()) { + if (highscore_info.contains("playerName") && highscore_info.contains("score")) { + std::pair 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("*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(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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true); + intern_from_c("*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(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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true); + intern_from_c("*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(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("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(true); + intern_from_c("*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(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::JAK2)->convert_utf8_to_game(run_info.first); + strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); + *(Ptr(time_dest_ptr).c()) = run_info.second; + } else { + std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(""); + strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); + *(Ptr(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(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::JAK2)->convert_utf8_to_game(run_info.first); + strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); + *(Ptr(time_dest_ptr).c()) = run_info.second; + } else { + std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(""); + strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); + *(Ptr(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(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::JAK2)->convert_utf8_to_game(run_info.first); + strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); + *(Ptr(time_dest_ptr).c()) = run_info.second; + } else { + std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(""); + strcpy(Ptr(name_dest_ptr).c()->data(), converted.c_str()); + *(Ptr(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(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(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(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(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(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(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(forbidden_features); + json_deserialize_if_exists(cheats); + json_deserialize_if_exists(continue_point_name); + json_deserialize_if_exists(completed_task); +} + +std::vector g_speedrun_practice_entries; +std::unordered_map 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 (int 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 (!g_speedrun_practice_entries.size() <= entry_index) { + name = g_speedrun_practice_entries.at(entry_index).name; + } + strcpy(Ptr(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 (!g_speedrun_practice_entries.size() <= entry_index) { + name = g_speedrun_practice_entries.at(entry_index).continue_point_name; + } + strcpy(Ptr(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(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(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(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 >= g_speedrun_practice_entries.size()) { + return; + } + + auto objective = speedrun_practice_obj_ptr + ? Ptr(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->secrets = json_info.secrets; + auto starting_position = + objective->starting_position ? Ptr(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) * 4096.0; + } + } + auto starting_rotation = + objective->starting_rotation ? Ptr(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(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(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(objective->start_zone_init_params).c() + : NULL; + if (starting_zone) { + starting_zone->v1[0] = json_info.start_zone_v1.at(0) * 4096.0; + starting_zone->v1[1] = json_info.start_zone_v1.at(1) * 4096.0; + starting_zone->v1[2] = json_info.start_zone_v1.at(2) * 4096.0; + starting_zone->v1[3] = json_info.start_zone_v1.at(3) * 4096.0; + starting_zone->v2[0] = json_info.start_zone_v2.at(0) * 4096.0; + starting_zone->v2[1] = json_info.start_zone_v2.at(1) * 4096.0; + starting_zone->v2[2] = json_info.start_zone_v2.at(2) * 4096.0; + starting_zone->v2[3] = json_info.start_zone_v2.at(3) * 4096.0; + } + + if (json_info.end_zone_v1 && json_info.end_zone_v2) { + auto ending_zone = objective->end_zone_init_params + ? Ptr(objective->end_zone_init_params).c() + : NULL; + if (ending_zone) { + ending_zone->v1[0] = json_info.end_zone_v1->at(0) * 4096.0; + ending_zone->v1[1] = json_info.end_zone_v1->at(1) * 4096.0; + ending_zone->v1[2] = json_info.end_zone_v1->at(2) * 4096.0; + ending_zone->v1[3] = json_info.end_zone_v1->at(3) * 4096.0; + ending_zone->v2[0] = json_info.end_zone_v2->at(0) * 4096.0; + ending_zone->v2[1] = json_info.end_zone_v2->at(1) * 4096.0; + ending_zone->v2[2] = json_info.end_zone_v2->at(2) * 4096.0; + ending_zone->v2[3] = json_info.end_zone_v2->at(3) * 4096.0; + } + } + } +} + +std::vector 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 (!g_speedrun_custom_categories.size() <= entry_index) { + name = g_speedrun_custom_categories.at(entry_index).name; + } + strcpy(Ptr(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 (!g_speedrun_custom_categories.size() <= entry_index) { + name = g_speedrun_custom_categories.at(entry_index).continue_point_name; + } + strcpy(Ptr(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 >= g_speedrun_custom_categories.size()) { + return; + } + + auto category = speedrun_custom_category_ptr + ? Ptr(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->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(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.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)); + } + return; +} + +} // namespace kmachine_extras diff --git a/game/kernel/jak2/kmachine_extras.h b/game/kernel/jak2/kmachine_extras.h new file mode 100644 index 000000000..f10c13d5a --- /dev/null +++ b/game/kernel/jak2/kmachine_extras.h @@ -0,0 +1,210 @@ +#pragma once +#include +#include + +#include "common/common_types.h" +#include "common/util/json_util.h" + +namespace kmachine_extras { +void update_discord_rpc(u32 discord_info); +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 result); +void callback_fetch_external_race_times(bool success, + const std::string& cache_id, + std::optional result); +void callback_fetch_external_highscores(bool success, + const std::string& cache_id, + std::optional 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 + float gem_count; // float + u32 death_count; // int32 + u32 status; // string + u32 level; // string + u32 cutscene; // symbol - bool + float time_of_day; // float + float percent_completed; // float + u32 focus_status; // uint32 + u32 task; // string +}; + +enum class FocusStatus : u32 { + Disable = 0, + Dead = 1, + Ignore = 2, + Inactive = 3, + Dangerous = 4, + InAir = 5, + Hit = 6, + Grabbed = 7, + InHead = 8, + TouchWater = 9, + OnWater = 10, + UnderWater = 11, + EdgeGrab = 12, + Pole = 13, + PilotRiding = 14, + Flut = 15, + Tube = 16, + Ice = 17, + Board = 18, + Gun = 19, + Pilot = 20, + Mech = 21, + Dark = 22, + Rail = 23, + Halfpipe = 24, + Carry = 25, + Super = 26, + Shooting = 27, + Indax = 28, + Arrestable = 29, + Teleporting = 30, + FS31 = 31, + Max = 32 +}; + +#define FOCUS_TEST(status, foc) (status.test(static_cast(foc))) + +// To speedup 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_JaK2"; + u64 pointer_to_symbol = 0; +}; + +extern AutoSplitterBlock g_auto_splitter_block_jak2; + +struct SpeedrunPracticeEntryHistoryAttempt { + std::optional 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; + std::vector starting_position; + std::vector starting_rotation; + std::vector starting_camera_position; + std::vector starting_camera_rotation; + std::vector start_zone_v1; + std::vector start_zone_v2; + std::optional> end_zone_v1; + std::optional> end_zone_v2; + std::optional end_task; + std::map> 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; + u32 secrets; + 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; + u32 secrets; + u64 features; + 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; + u32 secrets; + u64 features; + u64 forbidden_features; + u64 cheats; + u8 completed_task; +}; + +} // namespace kmachine_extras diff --git a/goal_src/goal-lib.gc b/goal_src/goal-lib.gc index 3afa850af..5674421ef 100644 --- a/goal_src/goal-lib.gc +++ b/goal_src/goal-lib.gc @@ -1016,6 +1016,15 @@ ) ) +(defmacro bitfield->string (enum input) + "return the name of an bitfield enum value, assumes `input` is the bit position (with 0 being the right-most bit)" + + `(case (shl 1 ,input) + ,@(apply (lambda (x) `(((,enum ,(car x) )) ,(symbol->string (car x) ) )) (reverse (get-enum-vals enum))) + (else "*unknown*") + ) + ) + (defmacro bit-enum->string (enum input stream) "print the enum bits in input to stream" diff --git a/goal_src/jak2/engine/debug/default-menu.gc b/goal_src/jak2/engine/debug/default-menu.gc index 76a58f07a..07b146b3e 100644 --- a/goal_src/jak2/engine/debug/default-menu.gc +++ b/goal_src/jak2/engine/debug/default-menu.gc @@ -3218,6 +3218,8 @@ (flag "level-select" 256 dm-game-secret-toggle-pick-func) (flag "scrap-book-1" 512 dm-game-secret-toggle-pick-func) (flag "scrap-book-2" 1024 dm-game-secret-toggle-pick-func) + ;; ;; og:preserve-this they missed one! + (flag "scrap-book-3" 2048 dm-game-secret-toggle-pick-func) (flag "gungame-blue" 4096 dm-game-secret-toggle-pick-func) (flag "gungame-dark" 8192 dm-game-secret-toggle-pick-func) (flag "reverse-races" 16384 dm-game-secret-toggle-pick-func) diff --git a/goal_src/jak2/engine/game/main.gc b/goal_src/jak2/engine/game/main.gc index 1ee2e2774..b5ccc113b 100644 --- a/goal_src/jak2/engine/game/main.gc +++ b/goal_src/jak2/engine/game/main.gc @@ -250,7 +250,9 @@ ) ) (('menu) - (set-master-mode (cond + ;; og:preserve-this Let the popup menu code handle inputs instead of the code written for the original debug menu + (when (not *popup-menu-open*) + (set-master-mode (cond ((and *debug-segment* (cpad-hold? 0 l3) (cpad-pressed? 0 select start)) 'menu ) @@ -270,7 +272,7 @@ *master-mode* ) ) - ) + )) (set! *pause-lock* #f) ) (('pause) @@ -922,9 +924,11 @@ ;; draw and update menus (with-profiler 'menu-hook *profile-menu-hook-color* - (*menu-hook*) - (when *speedrun-menu* - (draw! (-> *speedrun-menu* 0))) + ;; og:preserve-this Let the popup menu code handle inputs instead of the code written for the original debug menu + (when (not *popup-menu-open*) + (*menu-hook*)) + (when *speedrun-manager* + (draw-menu (-> *speedrun-manager* 0))) ) ;; load text files as needed from the menu update diff --git a/goal_src/jak2/engine/target/board/target-board.gc b/goal_src/jak2/engine/target/board/target-board.gc index af195c0f3..d969759d7 100644 --- a/goal_src/jak2/engine/target/board/target-board.gc +++ b/goal_src/jak2/engine/target/board/target-board.gc @@ -105,7 +105,6 @@ (set-width! font-ctx 332) (set-scale! font-ctx 0.35) (print-game-text *temp-string* font-ctx #f 44 (bucket-id debug-no-zbuf1)) - ;; Add points (+! (-> font-ctx origin y) 36.0) (set-scale! font-ctx 0.5) diff --git a/goal_src/jak2/kernel-defs.gc b/goal_src/jak2/kernel-defs.gc index 661e62970..52b80e858 100644 --- a/goal_src/jak2/kernel-defs.gc +++ b/goal_src/jak2/kernel-defs.gc @@ -252,6 +252,27 @@ (define-extern pc-get-num-external-speedrun-times (function string int)) (define-extern pc-get-num-external-highscores (function string int)) +;; Speedrunner Mode Stuff +(define-extern pc-sr-mode-get-practice-entries-amount (function int)) +(define-extern pc-sr-mode-get-practice-entry-name (function int string none)) +(define-extern pc-sr-mode-get-practice-entry-continue-point (function int string none)) +(define-extern pc-sr-mode-get-practice-entry-history-success (function int int)) +(define-extern pc-sr-mode-get-practice-entry-history-attempts (function int int)) +(define-extern pc-sr-mode-get-practice-entry-session-success (function int int)) +(define-extern pc-sr-mode-get-practice-entry-session-attempts (function int int)) +(define-extern pc-sr-mode-get-practice-entry-avg-time (function int string none)) +(define-extern pc-sr-mode-get-practice-entry-fastest-time (function int string none)) +(define-extern pc-sr-mode-record-practice-entry-attempt! (function int symbol (pointer float) symbol)) +(declare-type speedrun-practice-objective structure) +(define-extern pc-sr-mode-init-practice-info! (function int speedrun-practice-objective none)) +;; TODO - a menu to dump out the 3 numbers with a pre-generated name to the file +(define-extern pc-sr-mode-get-custom-category-amount (function int)) +(define-extern pc-sr-mode-get-custom-category-name (function int string none)) +(define-extern pc-sr-mode-get-custom-category-continue-point (function int string none)) +(declare-type speedrun-custom-category structure) +(define-extern pc-sr-mode-init-custom-category-info! (function int speedrun-custom-category none)) +(define-extern pc-sr-mode-dump-new-custom-category (function speedrun-custom-category none)) + (define-extern file-stream-open (function file-stream string symbol file-stream)) (define-extern file-stream-close (function file-stream file-stream)) (define-extern file-stream-length (function file-stream int)) diff --git a/goal_src/jak2/pc/features/speedruns-h.gc b/goal_src/jak2/pc/features/speedruns-h.gc index e980c66f8..ed0013b67 100644 --- a/goal_src/jak2/pc/features/speedruns-h.gc +++ b/goal_src/jak2/pc/features/speedruns-h.gc @@ -1,6 +1,81 @@ ;;-*-Lisp-*- (in-package goal) +;; TEST - safe with malformed entries + +(deftype speedrun-timer (process) + ((draw? symbol) + (started? symbol) + (stopped? symbol) + (start-time time-frame) + (end-time time-frame) + (recorded-time float)) + (:methods + (draw-timer (_type_) none :behavior speedrun-timer) + (start! (_type_) none :behavior speedrun-timer) + (reset! (_type_) none :behavior speedrun-timer) + (stop! (_type_) float :behavior speedrun-timer)) + (:state-methods + idle)) + +(defbehavior speedrun-timer-init speedrun-timer () + (set! (-> self draw?) #f) + (set! (-> self started?) #f) + (set! (-> self start-time) 0) + (set! (-> self end-time) 0) + (set! (-> self recorded-time) 0.0) + (go-virtual idle) + (none)) + +(defstate idle (speedrun-timer) + :virtual #t + :code (behavior () + (until #f + (when (-> self draw?) (draw-timer self)) + (suspend)) + (none))) + +;; TODO - put in util +(deftype objective-zone (process) + ((start? symbol) + (v1 vector :inline) + (v2 vector :inline) + (on-enter (function none)) + (on-exit (function none))) + (:methods + (draw-zone (_type_) none)) + (:state-methods + waiting-for-player + player-inside)) + +(deftype objective-zone-init-params (structure) + ((v1 vector :inline) + (v2 vector :inline))) + +(defenum speedrun-practice-flags + :type uint64 + (none 0)) + +;; reset method +(deftype speedrun-practice-objective (structure) + ((index int32 :offset-assert 0) + (flags speedrun-practice-flags :offset-assert 8) + (completed-task game-task :offset-assert 16) + (features game-feature :offset-assert 24) + (secrets game-secrets :offset-assert 32) + (starting-position vector :offset-assert 36) + (starting-rotation vector :offset-assert 40) + (starting-camera-position vector :offset-assert 44) + (starting-camera-rotation matrix :offset-assert 48) + (end-task game-task :offset-assert 52) + (start-zone-init-params objective-zone-init-params :offset-assert 56) + (start-zone (pointer objective-zone) :offset-assert 60) + (end-zone-init-params objective-zone-init-params :offset-assert 64) + (end-zone (pointer objective-zone) :offset-assert 68)) + (:methods + (draw-info (_type_) none) + (reset! (_type_) none))) + (defenum speedrun-category :type uint32 @@ -13,18 +88,31 @@ ;; ie. removing mars tomb skip if you pick "all missions" ;; Random one for experimentation (all-cheats-allowed 999) - ) + (custom 9999)) + +(deftype speedrun-custom-category (structure) + ((index int32 :offset-assert 0) + (secrets game-secrets :offset-assert 4) + (features game-feature :offset-assert 8) + (forbidden-features game-feature :offset-assert 16) + (pc-cheats pc-cheats :offset-assert 24) + (completed-task game-task :offset-assert 32))) (deftype speedrun-info (structure) ((category speedrun-category) - (display-run-info? symbol)) + (active-custom-category speedrun-custom-category) + (dump-custom-category speedrun-custom-category) + (display-run-info? symbol) + (practicing? symbol) + (active-practice-objective speedrun-practice-objective) + (waiting-to-record-practice-attempt? symbol) + (run-started-at time-frame)) (:methods (set-category! (_type_ speedrun-category) none) (start-run! (_type_) none) (enforce-settings! (_type_) none) - (hide-run-info! (_type_) none) (update! (_type_) none) - (draw-run-info! (_type_) none))) + (draw-run-info (_type_) none))) (define-extern *speedrun-info* speedrun-info) @@ -33,16 +121,14 @@ (reset 0) (exit 1)) -(deftype speedrun-menu (process-drawable) - ((popup-menu popup-menu) - (draw-menu? symbol) +(deftype speedrun-manager (process) + ((popup-menu (pointer popup-menu)) (ignore-menu-toggle? symbol) - (opened-with-start? symbol)) + (opened-with-start? symbol) + (timer (pointer speedrun-timer))) (:methods - (draw! (_type_) none)) - (:states + (draw-menu (_type_) none)) + (:state-methods idle)) -(define-extern *speedrun-popup-menu* popup-menu) -(define-extern *speedrun-menu* (pointer speedrun-menu)) -(define-extern speedrun-menu-init (function none :behavior speedrun-menu)) +(define-extern *speedrun-manager* (pointer speedrun-manager)) diff --git a/goal_src/jak2/pc/features/speedruns.gc b/goal_src/jak2/pc/features/speedruns.gc index fc1047ab1..50a2c1cdf 100644 --- a/goal_src/jak2/pc/features/speedruns.gc +++ b/goal_src/jak2/pc/features/speedruns.gc @@ -1,7 +1,64 @@ ;;-*-Lisp-*- (in-package goal) +;; TODO later - customize menu open keybind + +(define-extern task-close! (function string symbol)) + (define *speedrun-info* (new 'static 'speedrun-info)) +(set! (-> *speedrun-info* active-custom-category) (new 'static 'speedrun-custom-category)) +(set! (-> *speedrun-info* dump-custom-category) (new 'static 'speedrun-custom-category)) +(set! (-> *speedrun-info* active-practice-objective) (new 'static 'speedrun-practice-objective)) +(set! (-> *speedrun-info* active-practice-objective starting-position) (new 'static 'vector)) +(set! (-> *speedrun-info* active-practice-objective starting-rotation) (new 'static 'vector)) +(set! (-> *speedrun-info* active-practice-objective starting-camera-position) (new 'static 'vector)) +(set! (-> *speedrun-info* active-practice-objective starting-camera-rotation) (new 'static 'matrix)) +(set! (-> *speedrun-info* active-practice-objective start-zone-init-params) (new 'static 'objective-zone-init-params)) +(set! (-> *speedrun-info* active-practice-objective end-zone-init-params) (new 'static 'objective-zone-init-params)) + +(defmethod draw-timer ((this speedrun-timer)) + (clear *temp-string*) + (clear *pc-encoded-temp-string*) + (cond + ((-> this started?) + (format *temp-string* "~,,2fs~%" (* (the float (- (current-time) (-> this start-time))) 0.0033333334))) + ((and (!= 0 (-> this end-time))) + (format *temp-string* "~,,2fs~%" (* (the float (- (-> this end-time) (-> this start-time))) 0.0033333334))) + (else + (format *temp-string* "0.0s~%"))) + (when *target* + (format *temp-string* "~,,2M~%" (-> *target* control ctrl-xz-vel))) + (pc-encode-utf8-string *temp-string* *pc-encoded-temp-string*) + (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) (bucket-id debug-no-zbuf1)) + ;; reset bucket settings prior to drawing - font won't do this for us, and + ;; draw-raw-image can sometimes mess them up. (intro sequence) + (dma-buffer-add-gs-set-flusha buf (alpha-1 (new 'static 'gs-alpha :b #x1 :d #x1)) (tex1-1 (new 'static 'gs-tex1 :mmag #x1 :mmin #x1))) + (let ((font-ctx (new 'stack 'font-context *font-default-matrix* 256 350 0.0 (font-color default) (font-flags middle shadow kerning large)))) + (set! (-> font-ctx scale) 0.325) + (draw-string-adv *pc-encoded-temp-string* buf font-ctx))) + (none)) + +(defmethod start! ((this speedrun-timer)) + (set! (-> this started?) #t) + (set! (-> this stopped?) #f) + (set! (-> this start-time) (current-time)) + (set! (-> this end-time) 0) + (none)) + +(defmethod reset! ((this speedrun-timer)) + (set! (-> this started?) #f) + (set! (-> this stopped?) #f) + (set! (-> this start-time) 0) + (set! (-> this end-time) 0) + (none)) + +(defmethod stop! ((this speedrun-timer)) + (when (not (-> this stopped?)) + (set! (-> this started?) #f) + (set! (-> this stopped?) #t) + (set! (-> this end-time) (current-time)) + (set! (-> this recorded-time) (* (the float (- (-> this end-time) (-> this start-time))) 0.0033333334))) + (-> this recorded-time)) (defmethod set-category! ((this speedrun-info) (category speedrun-category)) (set! (-> this category) category) @@ -12,6 +69,7 @@ (reset! *autosplit-info-jak2*) ;; turn on speedrun verification display (set! (-> this display-run-info?) #t) + (send-event (ppointer->process *speedrun-manager*) 'start-run) ;; ensure any required settings are enabled (enforce-settings! this) ;; finalize any category specific setup code @@ -21,8 +79,20 @@ (((speedrun-category newgame-heromode)) (initialize! *game-info* 'game (the-as game-save #f) "game-start-hero")) (((speedrun-category all-cheats-allowed)) - (initialize! *game-info* 'game (the-as game-save #f) "game-start"))) - + (initialize! *game-info* 'game (the-as game-save #f) "game-start")) + (((speedrun-category custom)) + (set-master-mode 'game) + (send-event (ppointer->process (-> *speedrun-manager* 0 popup-menu)) 'close-menu) + (process-spawn-function process (lambda :behavior process () + (clear *temp-string*) + (pc-sr-mode-get-custom-category-continue-point (-> *speedrun-info* active-custom-category index) *temp-string*) + (if (string= *temp-string* "") + (initialize! *game-info* 'game (the-as game-save #f) "game-start") + (initialize! *game-info* 'game (the-as game-save #f) *temp-string*)) + (until (and *target* (= (-> *target* next-state name) 'target-stance)) + (suspend)) + (when (nonzero? (-> *speedrun-info* active-custom-category completed-task)) + (task-resolution-close! (-> *speedrun-info* active-custom-category completed-task))))))) (if (!= -1 (-> *game-info* auto-save-which)) (set! (-> *setting-control* user-default auto-save) #t)) @@ -35,122 +105,394 @@ ;; - If you are playing a category that requires cheats (ie. a turbo jetboard one) you'd ;; probably like the game to automatically set the appropriate ones for you ;; - If you are playing a category that forbids cheats, you wouldn't want your run invalidated because you forgot - ;; - ;; However, the pc-settings stores a backup of your cheats whenever you manually modify them (NYI - no menus yet) - ;; and when speedrunner mode is first enabled. They are restored when speedrunner mode is disabled. - (when (!= (-> this category) (speedrun-category all-cheats-allowed)) - ;; disable any active cheats - (set! (-> *pc-settings* cheats) (the-as pc-cheats #x0))) + (case (-> this category) + (((speedrun-category newgame-normal) (speedrun-category newgame-heromode)) + ;; disable any active cheats + (set! (-> *pc-settings* cheats) (the-as pc-cheats #x0))) + (((speedrun-category custom)) + (set! (-> *game-info* secrets) (-> *speedrun-info* active-custom-category secrets)) + (logior! (-> *game-info* features) (-> *speedrun-info* active-custom-category features)) + (logclear! (-> *game-info* features) (-> *speedrun-info* active-custom-category forbidden-features)) + (set! (-> *pc-settings* cheats) (-> *speedrun-info* active-custom-category pc-cheats)))) (none)) -(defmethod hide-run-info! ((this speedrun-info)) - (set! (-> this display-run-info?) #f) +(defmethod draw-zone ((this objective-zone)) + (add-debug-box + #t + (bucket-id debug2) + (-> this v1) + (-> this v2) + (if (-> this start?) + (new 'static 'rgba :r #xff :g #xff :b #x00 :a #x80) + (new 'static 'rgba :r #xff :g #x00 :b #xff :a #x80))) + (none)) + +(defstate waiting-for-player (objective-zone) + :virtual #t + :event (behavior ((proc process) (arg1 int) (event-type symbol) (event event-message-block)) + (the-as object 0)) + :trans (behavior () + ;; Check to see if we have entered the zone + (let ((min-point-x (fmin (-> self v1 x) (-> self v2 x))) + (min-point-y (fmin (-> self v1 y) (-> self v2 y))) + (min-point-z (fmin (-> self v1 z) (-> self v2 z))) + (max-point-x (fmax (-> self v1 x) (-> self v2 x))) + (max-point-y (fmax (-> self v1 y) (-> self v2 y))) + (max-point-z (fmax (-> self v1 z) (-> self v2 z))) + (pos (target-pos 0))) + (when (and (and (<= min-point-x (-> pos x)) + (<= (-> pos x) max-point-x)) + (and (<= min-point-y (-> pos y)) + (<= (-> pos y) max-point-y)) + (and (<= min-point-z (-> pos z)) + (<= (-> pos z) max-point-z))) + (when (nonzero? (-> self on-enter)) + ((-> self on-enter))) + (go-virtual player-inside))) + (none)) + :code (behavior () + (until #f + (draw-zone self) + (suspend)) + (none)) + :post (behavior () + (none))) + +(defstate player-inside (objective-zone) + :virtual #t + :trans (behavior () + ;; Check to see if we have entered the zone + (let ((min-point-x (fmin (-> self v1 x) (-> self v2 x))) + (min-point-y (fmin (-> self v1 y) (-> self v2 y))) + (min-point-z (fmin (-> self v1 z) (-> self v2 z))) + (max-point-x (fmax (-> self v1 x) (-> self v2 x))) + (max-point-y (fmax (-> self v1 y) (-> self v2 y))) + (max-point-z (fmax (-> self v1 z) (-> self v2 z))) + (pos (target-pos 0))) + (when (not (and (and (<= min-point-x (-> pos x)) + (<= (-> pos x) max-point-x)) + (and (<= min-point-y (-> pos y)) + (<= (-> pos y) max-point-y)) + (and (<= min-point-z (-> pos z)) + (<= (-> pos z) max-point-z)))) + (when (nonzero? (-> self on-exit)) + ((-> self on-exit))) + (go-virtual waiting-for-player))) + (none)) + :code (behavior () + (until #f + (draw-zone self) + (suspend)) + (none))) + +(defbehavior objective-zone-init objective-zone ((start? symbol) (params objective-zone-init-params)) + (set! (-> self start?) start?) + (set! (-> self v1 quad) (-> params v1 quad)) + (set! (-> self v2 quad) (-> params v2 quad)) + (go-virtual waiting-for-player) + (none)) + +(defmethod draw-info ((this speedrun-practice-objective)) + (clear *temp-string*) + (clear *pc-encoded-temp-string*) + (pc-sr-mode-get-practice-entry-name (-> this index) *pc-encoded-temp-string*) + (format *temp-string* "Practicing: ~S~%" *pc-encoded-temp-string*) + (if (> (pc-sr-mode-get-practice-entry-history-attempts (-> this index)) 0) + (format *temp-string* "History: ~D/~D (~,,2f%)~%" + (pc-sr-mode-get-practice-entry-history-success (-> this index)) + (pc-sr-mode-get-practice-entry-history-attempts (-> this index)) + (* 100.0 (/ (the float (pc-sr-mode-get-practice-entry-history-success (-> this index))) + (the float (pc-sr-mode-get-practice-entry-history-attempts (-> this index)))))) + (format *temp-string* "History: --~%")) + (if (> (pc-sr-mode-get-practice-entry-session-attempts (-> this index)) 0) + (format *temp-string* "Session: ~D/~D (~,,2f%)~%" + (pc-sr-mode-get-practice-entry-session-success (-> this index)) + (pc-sr-mode-get-practice-entry-session-attempts (-> this index)) + (* 100.0 (/ (the float (pc-sr-mode-get-practice-entry-session-success (-> this index))) + (the float (pc-sr-mode-get-practice-entry-session-attempts (-> this index)))))) + (format *temp-string* "Session: --~%")) + (pc-sr-mode-get-practice-entry-avg-time (-> this index) *pc-encoded-temp-string*) + (format *temp-string* "Average Time: ~Ss~%" *pc-encoded-temp-string*) + (pc-sr-mode-get-practice-entry-fastest-time (-> this index) *pc-encoded-temp-string*) + (format *temp-string* "Fastest Time: ~Ss~%" *pc-encoded-temp-string*) + (format *temp-string* "\c91 L3: Reset~%") + (pc-encode-utf8-string *temp-string* *pc-encoded-temp-string*) + (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) (bucket-id debug-no-zbuf2)) + ;; reset bucket settings prior to drawing - font won't do this for us, and + ;; draw-raw-image can sometimes mess them up. (intro sequence) + (dma-buffer-add-gs-set-flusha buf (alpha-1 (new 'static 'gs-alpha :b #x1 :d #x1)) (tex1-1 (new 'static 'gs-tex1 :mmag #x1 :mmin #x1))) + (let ((font-ctx (new 'stack 'font-context *font-default-matrix* 510 20 0.0 (font-color default) (font-flags right shadow kerning large)))) + (set! (-> font-ctx scale) 0.325) + (draw-string-adv *pc-encoded-temp-string* buf font-ctx))) + (none)) + +(defmethod reset! ((this speedrun-practice-objective)) + ;; record attempt if attempt was started + (when (-> *speedrun-info* waiting-to-record-practice-attempt?) + (pc-sr-mode-record-practice-entry-attempt! (-> this index) #f (&-> (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer))) recorded-time))) + ;; TODO - load checkpoint if not already in that checkpoint + ;; TODO - set features / cheats / completed-task / etc + ;; Update player position + (vector-copy! (-> *target* root trans) (-> this starting-position)) + (vector-copy! (-> *target* root quat) (-> this starting-rotation)) + ;; - get off jetboard and reset speed + (vector-copy! (-> *target* control transv) *zero-vector*) + (send-event *target* 'change-mode 'normal) + ;; Update camera position and rotation + (vector-copy! (-> *camera-combiner* trans) (-> this starting-camera-position)) + (matrix-identity! (-> *camera-combiner* inv-camera-rot)) + (matrix-copy! (-> *camera-combiner* inv-camera-rot) (-> this starting-camera-rotation)) + (process-spawn-function process + (lambda :behavior process () + (suspend) + (send-event *camera* 'teleport) + (deactivate self))) + (cam-master-activate-slave #f) + (none)) + +(define *speedrun-popup-menu-entries* + (new 'static 'boxed-array :type popup-menu-entry + (new 'static 'popup-menu-button :label "Reset" + :on-confirm (lambda () (send-event (ppointer->process *speedrun-manager*) 'invoke (speedrun-menu-command reset)))) + (new 'static 'popup-menu-submenu :label "Built-in category select" + :entries (new 'static 'boxed-array :type popup-menu-entry + (new 'static 'popup-menu-flag :label "Normal" + :on-confirm (lambda () (set-category! *speedrun-info* (speedrun-category newgame-normal))) + :is-toggled? (lambda () (= (-> *speedrun-info* category) (speedrun-category newgame-normal)))) + (new 'static 'popup-menu-flag :label "Hero mode" + :on-confirm (lambda () (set-category! *speedrun-info* (speedrun-category newgame-heromode))) + :is-toggled? (lambda () (= (-> *speedrun-info* category) (speedrun-category newgame-heromode)))) + (new 'static 'popup-menu-flag :label "All cheats allowed" + :on-confirm (lambda () (set-category! *speedrun-info* (speedrun-category all-cheats-allowed))) + :is-toggled? (lambda () (= (-> *speedrun-info* category) (speedrun-category all-cheats-allowed)))))) + (new 'static 'popup-menu-dynamic-submenu :label "Custom category select" + :get-length (lambda () (pc-sr-mode-get-custom-category-amount)) + :get-entry-label (lambda ((index int) (str-dest string)) (pc-sr-mode-get-custom-category-name index str-dest)) + :on-entry-confirm (lambda ((index int)) + ;; hydrate from cpp + (pc-sr-mode-init-custom-category-info! index (-> *speedrun-info* active-custom-category)) + (set-category! *speedrun-info* (speedrun-category custom))) + :entry-selected? (lambda ((index int)) + (and (= (-> *speedrun-info* category) (speedrun-category custom)) + (= index (-> *speedrun-info* active-custom-category index))))) + ;; TODO - disabled until finalized + ;; (new 'static 'popup-menu-dynamic-submenu :label "Practice select" + ;; :entry-disabled? (lambda () (not (-> *speedrun-info* practicing?))) + ;; :get-length (lambda () (pc-sr-mode-get-practice-entries-amount)) + ;; :get-entry-label (lambda ((index int) (str-dest string)) (pc-sr-mode-get-practice-entry-name index str-dest)) + ;; :on-entry-confirm (lambda ((index int)) + ;; ;; turn on timer + ;; (set! (-> (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer))) draw?) #t) + ;; ;; tear down old processes + ;; (when (nonzero? (-> *speedrun-info* active-practice-objective start-zone)) + ;; (deactivate (-> *speedrun-info* active-practice-objective start-zone 0))) + ;; (when (nonzero? (-> *speedrun-info* active-practice-objective end-zone)) + ;; (deactivate (-> *speedrun-info* active-practice-objective end-zone 0))) + ;; ;; init from cpp + ;; (pc-sr-mode-init-practice-info! index (-> *speedrun-info* active-practice-objective)) + ;; ;; startup new processes + ;; (set! (-> *speedrun-info* active-practice-objective start-zone) + ;; (the-as (pointer objective-zone) (process-spawn objective-zone :init objective-zone-init #t (-> *speedrun-info* active-practice-objective start-zone-init-params)))) + ;; (set! (-> *speedrun-info* active-practice-objective start-zone 0 on-exit) + ;; (lambda () + ;; (start! (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer)))) + ;; (set! (-> *speedrun-info* waiting-to-record-practice-attempt?) #t) + ;; (none))) + ;; (set! (-> *speedrun-info* active-practice-objective start-zone 0 on-enter) + ;; (lambda () + ;; (when (and *target* (>= (-> *target* control ctrl-xz-vel) (meters 30.0))) + ;; (vector-copy! (-> *target* control transv) *zero-vector*)) + ;; (set! (-> *speedrun-info* waiting-to-record-practice-attempt?) #f) + ;; (reset! (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer)))) + ;; (none))) + + ;; (when (= 0 (-> *speedrun-info* active-practice-objective end-task)) + ;; (set! (-> *speedrun-info* active-practice-objective end-zone) + ;; (the-as (pointer objective-zone) (process-spawn objective-zone :init objective-zone-init #f (-> *speedrun-info* active-practice-objective end-zone-init-params)))) + ;; (set! (-> *speedrun-info* active-practice-objective end-zone 0 on-enter) + ;; (lambda () + ;; (when (-> *speedrun-info* waiting-to-record-practice-attempt?) + ;; (stop! (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer)))) + ;; (if (pc-sr-mode-record-practice-entry-attempt! (-> *speedrun-info* active-practice-objective index) + ;; #t + ;; (&-> (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer))) recorded-time)) + ;; (sound-play "skill-pickup") + ;; (sound-play "menu-pick")) + ;; (set! (-> *speedrun-info* waiting-to-record-practice-attempt?) #f)) + ;; (none)))) + ;; (set! (-> *speedrun-info* practicing?) #t) + ;; (reset! (-> *speedrun-info* active-practice-objective)) + ;; (set-master-mode 'game) + ;; (send-event (ppointer->process (-> *speedrun-manager* 0 popup-menu)) 'close-menu)) + ;; :entry-selected? (lambda ((index int)) (and (-> *speedrun-info* practicing?) (= index (-> *speedrun-info* active-practice-objective index))))) + ;; (new 'static 'popup-menu-button :label "Stop practicing" + ;; :entry-disabled? (lambda () (not (-> *speedrun-info* practicing?))) + ;; :on-confirm (lambda () + ;; (when (-> *speedrun-info* practicing?) + ;; (when (nonzero? (-> *speedrun-info* active-practice-objective start-zone)) + ;; (deactivate (-> *speedrun-info* active-practice-objective start-zone 0))) + ;; (when (nonzero? (-> *speedrun-info* active-practice-objective end-zone)) + ;; (deactivate (-> *speedrun-info* active-practice-objective end-zone 0)))) + ;; (set! (-> *speedrun-info* practicing?) #f) + ;; (set! (-> (the-as speedrun-timer (ppointer->process (-> *speedrun-manager* 0 timer))) draw?) #f))) + (new 'static 'popup-menu-submenu :label "Tools" + :entries (new 'static 'boxed-array :type popup-menu-entry + (new 'static 'popup-menu-submenu :label "Create custom category" + :entries (new 'static 'boxed-array :type popup-menu-entry + (new 'static 'popup-menu-dynamic-submenu :label "Select secrets" + :get-length (lambda () 18) + :get-entry-label (lambda ((index int) (str-dest string)) (copy-string<-string str-dest (bitfield->string game-secrets index))) + :on-entry-confirm (lambda ((index int)) (logxor! (-> *speedrun-info* dump-custom-category secrets) (shl 1 index))) + :entry-selected? (lambda ((index int)) (logtest? (-> *speedrun-info* dump-custom-category secrets) (shl 1 index))) + :on-reset (lambda () (set! (-> *speedrun-info* dump-custom-category secrets) (game-secrets)))) + (new 'static 'popup-menu-dynamic-submenu :label "Select features" + :get-length (lambda () 27) + :get-entry-label (lambda ((index int) (str-dest string)) (copy-string<-string str-dest (bitfield->string game-feature index))) + :on-entry-confirm (lambda ((index int)) (logxor! (-> *speedrun-info* dump-custom-category features) (shl 1 index))) + :entry-selected? (lambda ((index int)) (logtest? (-> *speedrun-info* dump-custom-category features) (shl 1 index))) + :on-reset (lambda () (set! (-> *speedrun-info* dump-custom-category features) (game-feature)))) + (new 'static 'popup-menu-dynamic-submenu :label "Forbid features" + :get-length (lambda () 27) + :get-entry-label (lambda ((index int) (str-dest string)) (copy-string<-string str-dest (bitfield->string game-feature index))) + :on-entry-confirm (lambda ((index int)) (logxor! (-> *speedrun-info* dump-custom-category forbidden-features) (shl 1 index))) + :entry-selected? (lambda ((index int)) (logtest? (-> *speedrun-info* dump-custom-category forbidden-features) (shl 1 index))) + :on-reset (lambda () (set! (-> *speedrun-info* dump-custom-category forbidden-features) (game-feature)))) + (new 'static 'popup-menu-dynamic-submenu :label "Select cheats" + :get-length (lambda () 20) + :get-entry-label (lambda ((index int) (str-dest string)) (copy-string<-string str-dest (bitfield->string pc-cheats index))) + :on-entry-confirm (lambda ((index int)) (logxor! (-> *speedrun-info* dump-custom-category pc-cheats) (shl 1 index))) + :entry-selected? (lambda ((index int)) (logtest? (-> *speedrun-info* dump-custom-category pc-cheats) (shl 1 index))) + :on-reset (lambda () (set! (-> *speedrun-info* dump-custom-category pc-cheats) (pc-cheats)))) + (new 'static 'popup-menu-dynamic-submenu :label "Select completed task" + :get-length (lambda () (dec (the int (game-task max)))) + :get-entry-label (lambda ((index int) (str-dest string)) (copy-string<-string str-dest (enum->string game-task index))) + :on-entry-confirm (lambda ((index int)) (set! (-> *speedrun-info* dump-custom-category completed-task) (the game-task index))) + :entry-selected? (lambda ((index int)) (= (-> *speedrun-info* dump-custom-category completed-task) (the game-task index))) + :on-reset (lambda () (set! (-> *speedrun-info* dump-custom-category completed-task) (game-task none)))) + (new 'static 'popup-menu-button :label "Save new category to file" + :on-confirm (lambda () (pc-sr-mode-dump-new-custom-category (-> *speedrun-info* dump-custom-category)))))))) + (new 'static 'popup-menu-button :label "Exit" + :on-confirm (lambda () (send-event (ppointer->process *speedrun-manager*) 'invoke (speedrun-menu-command exit)))) + )) + +(define *speedrun-manager* (the-as (pointer speedrun-manager) #f)) + +(defbehavior speedrun-manager-init speedrun-manager () + (set! *speedrun-manager* (the-as (pointer speedrun-manager) (process->ppointer self))) + (set! (-> *speedrun-manager* 0 popup-menu) + (the-as (pointer popup-menu) (process-spawn popup-menu :init popup-menu-init "Speedrun Menu" *speedrun-popup-menu-entries*))) + (set! (-> *speedrun-manager* 0 timer) + (the-as (pointer speedrun-timer) (process-spawn speedrun-timer :init speedrun-timer-init))) + (set! (-> *speedrun-manager* 0 ignore-menu-toggle?) #f) + (set! (-> *speedrun-manager* 0 opened-with-start?) #f) + (set! (-> *speedrun-info* practicing?) #f) + (set! (-> *speedrun-info* waiting-to-record-practice-attempt?) #f) + (go-virtual idle) (none)) (defmethod update! ((this speedrun-info)) "A per frame update for speedrunning related stuff" ;; Ensure the speedrunner menu process is enabled or destroyed (when (and (-> *pc-settings* speedrunner-mode?) - (not *speedrun-menu*)) - (process-spawn speedrun-menu :init speedrun-menu-init #f :to *entity-pool*)) + (not *speedrun-manager*)) + (process-spawn speedrun-manager :init speedrun-manager-init #f :to *entity-pool*)) (when (and (not (-> *pc-settings* speedrunner-mode?)) - *speedrun-menu*) - (deactivate (-> *speedrun-menu* 0))) + *speedrun-manager*) + (deactivate (-> *speedrun-manager* 0))) ;; do speedrunner mode things (when (-> *pc-settings* speedrunner-mode?) ;; Update auto-splitter struct (update! *autosplit-info-jak2*) ;; see if we should stop drawing the run info (when you escape the fortress!) - (when (task-complete? *game-info* (game-task fortress-escape)) + (when (and (!= (-> this category) (speedrun-category custom)) + (task-complete? *game-info* (game-task fortress-escape))) (set! (-> this display-run-info?) #f)) - (when (-> this display-run-info?) - ;; Draw info to the screen - (draw-run-info! this)) + ;; Draw info to the screen + (when (and (not (-> *speedrun-info* practicing?)) (-> this display-run-info?)) + (draw-run-info this)) ;; enforce settings even if they've changed them - (enforce-settings! this)) + (enforce-settings! this) + ;; draw objective info if practicing + (when (-> *speedrun-info* practicing?) + (draw-info (-> this active-practice-objective)))) (none)) -(defmethod draw-run-info! ((this speedrun-info)) +(defmethod draw-run-info ((this speedrun-info)) "Draw speedrun related settings in the bottom left corner" (when (and (-> *pc-settings* speedrunner-mode?) (-> this display-run-info?)) (clear *temp-string*) (clear *pc-encoded-temp-string*) - (format *temp-string* "Category: ~S~%PC Cheats: ~D~%Frame Rate: ~D~%PS2 Actor Vis?: ~S~%Version: ~S~%" - (enum->string speedrun-category (-> this category)) - (the-as int (-> *pc-settings* cheats)) - (-> *pc-settings* target-fps) - (if (-> *pc-settings* ps2-actor-vis?) "true" "false") - *pc-settings-built-sha*) + (clear *pc-cpp-temp-string*) + (cond + ((= (-> this category) (speedrun-category custom)) + (pc-sr-mode-get-custom-category-name (-> this active-custom-category index) *pc-cpp-temp-string*) + (format *temp-string* + "Category: ~S~%Secrets: ~D~%Features: ~D~%Forbidden Features: ~D~%Cheats: ~D~%Version: ~S~%" + *pc-cpp-temp-string* + (-> this active-custom-category secrets) + (-> this active-custom-category features) + (-> this active-custom-category forbidden-features) + (-> this active-custom-category pc-cheats) + *pc-settings-built-sha*)) + (else + (format *temp-string* + "Category: ~S~%PC Cheats: ~D~%Frame Rate: ~D~%PS2 Actor Vis?: ~S~%Version: ~S~%" + (enum->string speedrun-category (-> this category)) + (the-as int (-> *pc-settings* cheats)) + (-> *pc-settings* target-fps) + (if (-> *pc-settings* ps2-actor-vis?) "true" "false") + *pc-settings-built-sha*))) (pc-encode-utf8-string *temp-string* *pc-encoded-temp-string*) - (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) (bucket-id debug-no-zbuf1)) + (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) (bucket-id debug-no-zbuf2)) ;; reset bucket settings prior to drawing - font won't do this for us, and ;; draw-raw-image can sometimes mess them up. (intro sequence) (dma-buffer-add-gs-set-flusha buf (alpha-1 (new 'static 'gs-alpha :b #x1 :d #x1)) (tex1-1 (new 'static 'gs-tex1 :mmag #x1 :mmin #x1))) - (let ((font-ctx (new 'stack 'font-context *font-default-matrix* 510 365 0.0 (font-color default) (font-flags right shadow kerning large)))) + (let ((font-ctx (new 'stack 'font-context *font-default-matrix* 510 (if (= (-> this category) (speedrun-category custom)) 355 365) 0.0 (font-color default) (font-flags right shadow kerning large)))) (set! (-> font-ctx scale) 0.325) (draw-string-adv *pc-encoded-temp-string* buf font-ctx)))) (none)) ;; Speedrun Menu -(define *speedrun-popup-menu* - (new 'static 'popup-menu - :entries (new 'static 'boxed-array :type popup-menu-entry - (new 'static 'popup-menu-button :label "Reset" :on-press (lambda () (send-event (ppointer->process *speedrun-menu*) 'invoke (speedrun-menu-command reset)))) - (new 'static 'popup-menu-label :label "Categories") - (new 'static 'popup-menu-flag :label "Normal" - :on-press (lambda () (set-category! *speedrun-info* (speedrun-category newgame-normal))) - :is-toggled? (lambda () (= (-> *speedrun-info* category) (speedrun-category newgame-normal)))) - (new 'static 'popup-menu-flag :label "Hero Mode" - :on-press (lambda () (set-category! *speedrun-info* (speedrun-category newgame-heromode))) - :is-toggled? (lambda () (= (-> *speedrun-info* category) (speedrun-category newgame-heromode)))) - (new 'static 'popup-menu-flag :label "All Cheats Allowed" - :on-press (lambda () (set-category! *speedrun-info* (speedrun-category all-cheats-allowed))) - :is-toggled? (lambda () (= (-> *speedrun-info* category) (speedrun-category all-cheats-allowed)))) - (new 'static 'popup-menu-button :label "Exit" :on-press (lambda () (send-event (ppointer->process *speedrun-menu*) 'invoke (speedrun-menu-command exit)))) - ) - ) - ) - -(define *speedrun-menu* (the-as (pointer speedrun-menu) #f)) - -(defbehavior speedrun-menu-init speedrun-menu () - (set! *speedrun-menu* (the-as (pointer speedrun-menu) (process->ppointer self))) - (set! (-> *speedrun-menu* 0 popup-menu) *speedrun-popup-menu*) - (set! (-> *speedrun-menu* 0 draw-menu?) #f) - (set! (-> *speedrun-menu* 0 ignore-menu-toggle?) #f) - (set! (-> *speedrun-menu* 0 opened-with-start?) #f) - (go idle) +(defmethod deactivate ((this speedrun-manager)) + (set! *speedrun-manager* (the-as (pointer speedrun-manager) #f)) + ((method-of-type process deactivate) this) (none)) -(defmethod deactivate ((this speedrun-menu)) - (set! *speedrun-menu* (the-as (pointer speedrun-menu) #f)) - ((method-of-type process-drawable deactivate) this) - (none)) - -(defstate idle (speedrun-menu) +(defstate idle (speedrun-manager) + :virtual #t :event (behavior ((proc process) (arg1 int) (event-type symbol) (event event-message-block)) (case event-type + (('start-run) + (set-time! (-> *speedrun-info* run-started-at))) (('invoke) (case (-> event param 0) (((speedrun-menu-command reset)) (start-run! *speedrun-info*)) (((speedrun-menu-command exit)) (set-master-mode 'game) - (set! (-> self draw-menu?) #f)) + (send-event (ppointer->process (-> self popup-menu)) 'close-menu)) (else (format 0 "nyi: invoke ~D~%" (-> event param 0)))))) (the-as object 0)) :trans (behavior () (none)) :code (behavior () - (until #f (suspend) ) + (until #f + (when (and (-> *speedrun-info* practicing?) (cpad-pressed? 0 l3)) + (reset! (-> *speedrun-info* active-practice-objective))) + (when (and (-> *speedrun-info* display-run-info?) + (= (-> *speedrun-info* category) (speedrun-category custom)) + (time-elapsed? (-> *speedrun-info* run-started-at) (seconds 15))) + (set! (-> *speedrun-info* display-run-info?) #f)) + (suspend)) (none)) :post (behavior () (none))) -(defmethod draw! ((this speedrun-menu)) +(defmethod draw-menu ((this speedrun-manager)) + ;; handle opening and closing the menu (cond ((!= (-> *pc-settings* speedrunner-mode-custom-bind) 0) ;; the user has let go of the keybind completely or partially, allow the bind to trigger again @@ -165,10 +507,10 @@ (cond ((= *master-mode* 'game) (set-master-mode 'menu) - (set! (-> this draw-menu?) #t)) + (send-event (ppointer->process (-> this popup-menu)) 'open-menu)) ((= *master-mode* 'menu) (set-master-mode 'game) - (set! (-> this draw-menu?) #f))) + (send-event (ppointer->process (-> this popup-menu)) 'close-menu))) (logclear! (cpad-hold 0) (-> *pc-settings* speedrunner-mode-custom-bind)) (logclear! (cpad-pressed 0) (-> *pc-settings* speedrunner-mode-custom-bind)) (set! (-> this ignore-menu-toggle?) #t))) @@ -183,10 +525,10 @@ (cond ((= *master-mode* 'game) (set-master-mode 'menu) - (set! (-> this draw-menu?) #t)) + (send-event (ppointer->process (-> this popup-menu)) 'open-menu)) ((= *master-mode* 'menu) (set-master-mode 'game) - (set! (-> this draw-menu?) #f))) + (send-event (ppointer->process (-> this popup-menu)) 'close-menu))) (cpad-clear! 0 l1 r1) (cond ((cpad-hold? 0 select) @@ -196,18 +538,6 @@ (cpad-clear! 0 start) (set! (-> this opened-with-start?) #t))) (set! (-> this ignore-menu-toggle?) #t)))) - ;; render the menu - (when (-> this draw-menu?) - ;; handle any inputs for within the menu - (cond - ((cpad-pressed? 0 triangle select circle) - (set! (-> this draw-menu?) #f)) - ((cpad-pressed? 0 up) - (move-up! (-> this popup-menu))) - ((cpad-pressed? 0 down) - (move-down! (-> this popup-menu))) - ((cpad-pressed? 0 x) - (press! (-> this popup-menu)))) - ;; draw it - (draw! (-> this popup-menu))) + ;; render menu / handle inputs + (update-menu! (the-as popup-menu (ppointer->process (-> this popup-menu)))) (none)) diff --git a/goal_src/jak2/pc/pckernel-impl.gc b/goal_src/jak2/pc/pckernel-impl.gc index 930ea989f..95ae7c54d 100644 --- a/goal_src/jak2/pc/pckernel-impl.gc +++ b/goal_src/jak2/pc/pckernel-impl.gc @@ -82,7 +82,6 @@ (cheats-purchased pc-cheats) (cheats-unlocked pc-cheats) (cheats-mask pc-cheats) - (cheats-backup pc-cheats) ;; backup for 'cheats', persisted to disk to be restored when disabling speedrunner mode TODO use mask instead ;; music (music-unlocked bit-array) (flava-unlocked symbol 6) diff --git a/goal_src/jak2/pc/pckernel.gc b/goal_src/jak2/pc/pckernel.gc index fa3a198eb..7721bb18c 100644 --- a/goal_src/jak2/pc/pckernel.gc +++ b/goal_src/jak2/pc/pckernel.gc @@ -739,7 +739,7 @@ (("cheats-revealed") (set! (-> obj cheats-revealed) (the-as pc-cheats (file-stream-read-int file)))) (("cheats-purchased") (set! (-> obj cheats-purchased) (the-as pc-cheats (file-stream-read-int file)))) (("cheats-unlocked") (set! (-> obj cheats-unlocked) (the-as pc-cheats (file-stream-read-int file)))) - (("cheats-backup") (set! (-> obj cheats-backup) (the-as pc-cheats (file-stream-read-int file)))) + (("cheats-backup") (file-stream-read-int file)) ;; TODO - Don't remove this, parsing code can't handle unexpected keys (("music-unlocked") (dotimes (i (/ (align64 (-> obj music-unlocked length)) 64)) (bit-array<-int64 (-> obj music-unlocked) (* i 64) (file-stream-read-int file)) @@ -790,7 +790,6 @@ (format file " (cheats-revealed #x~x)~%" (-> obj cheats-revealed)) (format file " (cheats-purchased #x~x)~%" (-> obj cheats-purchased)) (format file " (cheats-unlocked #x~x)~%" (-> obj cheats-unlocked)) - (format file " (cheats-backup #x~x)~%" (-> obj cheats-backup)) (format file " (music-unlocked") (dotimes (i (/ (align64 (-> obj music-unlocked length)) 64)) @@ -836,7 +835,7 @@ (defun draw-build-revision () (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) - (bucket-id debug-no-zbuf1)) + (bucket-id debug-no-zbuf2)) ;; reset bucket settings prior to drawing - font won't do this for us, and ;; draw-raw-image can sometimes mess them up. (dma-buffer-add-gs-set-flusha buf @@ -846,8 +845,8 @@ (clear *temp-string*) (format *temp-string* "~S" *pc-settings-built-sha*) (pc-encode-utf8-string *temp-string* *pc-encoded-temp-string*) - (let ((font-ctx (new 'stack 'font-context *font-default-matrix* 2 403 0.0 (font-color default) (font-flags shadow kerning large)))) - (set! (-> font-ctx scale) 0.325) + (let ((font-ctx (new 'stack 'font-context *font-default-matrix* 2 406 0.0 (font-color default) (font-flags shadow kerning large)))) + (set! (-> font-ctx scale) 0.25) (draw-string-adv *pc-encoded-temp-string* buf font-ctx)))) diff --git a/goal_src/jak2/pc/progress/progress-generic-draw-pc.gc b/goal_src/jak2/pc/progress/progress-generic-draw-pc.gc index eaf996fb2..66d3e46d6 100644 --- a/goal_src/jak2/pc/progress/progress-generic-draw-pc.gc +++ b/goal_src/jak2/pc/progress/progress-generic-draw-pc.gc @@ -174,7 +174,7 @@ (if (= (get-aspect-ratio) 'aspect16x9) (set! (-> font-ctx origin y) (+ 86.0 margin-top-bottom)) (set! (-> font-ctx origin y) (+ 112.0 margin-top-bottom))) - + ;; do scrolling. if we notice we need to scroll too much, we just snap immediately instead of smoothly stepping. (cond ((> (-> *progress-pc-generic-store* current-menu-scroll-index) (-> *progress-pc-generic-store* current-menu-hover-index)) @@ -186,7 +186,7 @@ (set! (-> *progress-pc-generic-store* current-menu-scroll-index) (the float (- (-> *progress-pc-generic-store* current-menu-hover-index) (1- max-page-size)))) (seek-ease! (-> *progress-pc-generic-store* current-menu-scroll-index) (the float (- (-> *progress-pc-generic-store* current-menu-hover-index) (1- max-page-size))) (* 0.125 (-> PP clock time-adjust-ratio)) 0.3 (* 0.00125 (-> PP clock time-adjust-ratio))))) ) - + ;; render items (let* ((start-index (the int (-> *progress-pc-generic-store* current-menu-scroll-index))) ;; we add 1 because the scroll effect will reveal 1 extra diff --git a/goal_src/jak2/pc/progress/progress-static-pc.gc b/goal_src/jak2/pc/progress/progress-static-pc.gc index 7da0fa6f2..e06a972c0 100644 --- a/goal_src/jak2/pc/progress/progress-static-pc.gc +++ b/goal_src/jak2/pc/progress/progress-static-pc.gc @@ -81,9 +81,6 @@ This gives us more freedom to write code how we want. ) ) -;; TODO - there is a bug where if you restore default binds and that changes your `X` bind, -;; the next X input is ignored, figure this out eventually / make an issue for it. - ;; TODO - this is a gross misuse of macros, instead if we want to hide a very small amount of options in one menu versus another ;; it's a clear indication of a missing feature (add a lambda that determines visibility, or just the use disabled one) (defmacro game-options-pc-input-options () @@ -358,11 +355,6 @@ This gives us more freedom to write code how we want. :get-value-fn (lambda () (-> *pc-settings* speedrunner-mode?)) :on-confirm (lambda ((val symbol)) (set! (-> *pc-settings* speedrunner-mode?) val) - ;; store and restore pc-settings cheats - ;; TODO - when cheats menus are actually added, update the backup whenever one is changed - (if (-> *pc-settings* speedrunner-mode?) - (set! (-> *pc-settings* cheats-backup) (-> *pc-settings* cheats)) - (set! (-> *pc-settings* cheats) (-> *pc-settings* cheats-backup))) (pc-settings-save)))) (defmacro misc-options-pc-fast-progress () diff --git a/goal_src/jak2/pc/util/popup-menu-h.gc b/goal_src/jak2/pc/util/popup-menu-h.gc index a81d5877e..eac5ddbb1 100644 --- a/goal_src/jak2/pc/util/popup-menu-h.gc +++ b/goal_src/jak2/pc/util/popup-menu-h.gc @@ -3,28 +3,58 @@ ;; A debug-menu style popup menu, a lightweight way to make a context menu that doesn't involve the progress code ;; and isn't debug-only -;; -;; Currently only supports a single 1-level menu of buttons, add more features as required + +(define *popup-menu-open* #f) (deftype popup-menu-entry (basic) ((label string) - (on-press (function none))) + (entry-disabled? (function symbol)) + (on-confirm (function none))) (:methods - (draw! (_type_ font-context dma-buffer) none))) + (draw-entry (_type_ font-context dma-buffer symbol) none))) -(deftype popup-menu-label (popup-menu-entry) ()) +;; (deftype popup-menu-label (popup-menu-entry) ()) (deftype popup-menu-button (popup-menu-entry) ()) (deftype popup-menu-flag (popup-menu-entry) ((is-toggled? (function symbol)))) -(deftype popup-menu (basic) - ((entries (array popup-menu-entry)) - (curr-entry-index int32)) +(deftype popup-menu-submenu (popup-menu-entry) + ((entries (array popup-menu-entry)))) + +(deftype popup-menu-dynamic-submenu (popup-menu-entry) + ((get-length (function int)) + (get-entry-label (function int string none)) + (on-entry-confirm (function int none)) + (entry-selected? (function int symbol)) + (on-reset (function none)))) + +(deftype popup-menu-state (structure) + ((title string) + (entries (array popup-menu-entry)) + (entry-index int32) + (dynamic-menu? symbol) + (get-dynamic-menu-length (function int)) + (get-dynamic-menu-entry-label (function int string none)) + (on-dynamic-menu-entry-confirm (function int none)) + (dynamic-menu-entry-selected? (function int symbol)) + (on-dynamic-menu-reset (function none)))) + +(deftype popup-menu (process) + ((title string) + (entries (array popup-menu-entry)) + (menu-states popup-menu-state 10 :inline) + (curr-state-index int32) + (draw? symbol)) (:methods - (draw! (_type_) none) - (move-up! (_type_) none) - (move-down! (_type_) none) - (press! (_type_) none) - (get-widest-label (_type_ font-context) float))) + (update-menu! (_type_) none :behavior popup-menu) + (draw-menu (_type_) none) + (move-up! (_type_ int) none) + (move-down! (_type_ int) none) + (confirm! (_type_) none) + (reset! (_type_) none) + (back! (_type_) symbol)) + (:state-methods + closed + opened)) diff --git a/goal_src/jak2/pc/util/popup-menu.gc b/goal_src/jak2/pc/util/popup-menu.gc index 77f6aa3f4..99c1f2644 100644 --- a/goal_src/jak2/pc/util/popup-menu.gc +++ b/goal_src/jak2/pc/util/popup-menu.gc @@ -1,72 +1,315 @@ ;;-*-Lisp-*- (in-package goal) -(defmethod draw! ((this popup-menu-entry) (font-ctx font-context) (dma-buf dma-buffer)) - (let ((old-x (-> font-ctx origin x)) - (old-y (-> font-ctx origin y))) - (pc-encode-utf8-string (-> this label) *pc-encoded-temp-string*) - (draw-string-adv *pc-encoded-temp-string* dma-buf font-ctx) - (set! (-> font-ctx origin x) old-x) - (set! (-> font-ctx origin y) old-y)) - (none)) - -(defmethod draw! ((this popup-menu)) - (let ((font-ctx (new 'debug 'font-context *font-default-matrix* 0 0 0.0 (font-color default) (font-flags shadow kerning large)))) - (set! (-> font-ctx scale) 0.35) - (set! (-> font-ctx origin x) 15.0) - (set! (-> font-ctx origin y) 75.0) - (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) (bucket-id debug-no-zbuf1)) - ;; background border - (draw-sprite2d-xy buf - 6 - 64 - (+ 17 (the int (get-widest-label this font-ctx))) ;; width - (+ 17 (* 15 (-> this entries length))) ;; height - (new 'static 'rgba :r 255 :g 255 :b 255 :a 75)) - ;; background - (draw-sprite2d-xy buf - 7 - 65 - (+ 15 (the int (get-widest-label this font-ctx))) ;; width - (+ 15 (* 15 (-> this entries length))) ;; height - (new 'static 'rgba :r 0 :g 0 :b 0 :a 255)) - ;; menu contents - (dotimes (i (-> this entries length)) - (cond - ;; TODO - probably just handle this in the draw methods - ((type? (-> this entries i) popup-menu-label) (set! (-> font-ctx color) (font-color progress-option-off))) - ((type? (-> this entries i) popup-menu-flag) - (set! (-> font-ctx color) - (if (or ((-> (the-as popup-menu-flag (-> this entries i)) is-toggled?)) (= i (-> this curr-entry-index))) - (font-color cyan) - (font-color default)))) - (else (set! (-> font-ctx color) (if (= i (-> this curr-entry-index)) (font-color cyan) (font-color default))))) - (draw! (-> this entries i) font-ctx buf) - (set! (-> font-ctx origin y) (+ 15.0 (-> font-ctx origin y)))))) - (none)) - -(defmethod move-up! ((this popup-menu)) - (set! (-> this curr-entry-index) (max 0 (dec (-> this curr-entry-index)))) - ;; skip labels - (when (type? (-> this entries (-> this curr-entry-index)) popup-menu-label) - (set! (-> this curr-entry-index) (max 0 (dec (-> this curr-entry-index))))) - (none)) - -(defmethod move-down! ((this popup-menu)) - (set! (-> this curr-entry-index) (min (dec (-> this entries length)) (inc (-> this curr-entry-index)))) - ;; skip labels - (when (type? (-> this entries (-> this curr-entry-index)) popup-menu-label) - (set! (-> this curr-entry-index) (min (dec (-> this entries length)) (inc (-> this curr-entry-index))))) - (none)) - -(defmethod press! ((this popup-menu)) - (let ((entry (-> this entries (-> this curr-entry-index)))) ((-> entry on-press))) - (none)) - -(defmethod get-widest-label ((this popup-menu) (font-ctx font-context)) +(defun get-widest-entry ((entries (array popup-menu-entry)) (title string) (font-ctx font-context) (start-index int) (end-index int)) (let ((max-len 0.0)) - (dotimes (i (-> this entries length)) - (let ((label-len (-> (get-string-length (-> this entries i label) font-ctx) length))) + (dotimes (i (- end-index start-index)) + (let ((label-len (-> (get-string-length (-> entries (+ start-index i) label) font-ctx) length))) (when (> label-len max-len) (set! max-len label-len)))) - max-len)) + (let ((title-len (-> (get-string-length title font-ctx) length))) + (when (> title-len max-len) + (set! max-len title-len))) + (the int max-len))) + +(defun get-widest-dynamic-entry ((get-entry-label (function int string none)) (title string) (font-ctx font-context) (start-index int) (end-index int)) + (let ((max-len 0.0)) + (dotimes (i (- end-index start-index)) + (get-entry-label (+ start-index i) *pc-encoded-temp-string*) + (let ((label-len (-> (get-string-length *pc-encoded-temp-string* font-ctx) length))) + (when (> label-len max-len) + (set! max-len label-len)))) + (let ((title-len (-> (get-string-length title font-ctx) length))) + (when (> title-len max-len) + (set! max-len title-len))) + (the int max-len))) + +(defmethod draw-entry ((this popup-menu-entry) (font-ctx font-context) (dma-buf dma-buffer) (hovering? symbol)) + (let ((old-x (-> font-ctx origin x)) + (old-y (-> font-ctx origin y)) + (old-color (-> font-ctx color))) + (pc-encode-utf8-string (-> this label) *pc-encoded-temp-string*) + (when hovering? + (set! (-> font-ctx color) (font-color cyan))) + (when (and (nonzero? (-> this entry-disabled?)) ((-> this entry-disabled?))) + (set! (-> font-ctx color) (font-color menu-parent))) + (draw-string-adv *pc-encoded-temp-string* dma-buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y) + (set! (-> font-ctx color) old-color)) + (none)) + +(defmethod draw-entry ((this popup-menu-flag) (font-ctx font-context) (dma-buf dma-buffer) (hovering? symbol)) + (let ((old-x (-> font-ctx origin x)) + (old-y (-> font-ctx origin y)) + (old-color (-> font-ctx color))) + (when ((-> this is-toggled?)) + (set! (-> font-ctx color) (font-color green)) + (set! (-> font-ctx origin x) (- old-x 6.0)) + (draw-string-adv "\c86" dma-buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y) + (set! (-> font-ctx color) old-color)) + (pc-encode-utf8-string (-> this label) *pc-encoded-temp-string*) + (when hovering? + (set! (-> font-ctx color) (font-color cyan))) + (draw-string-adv *pc-encoded-temp-string* dma-buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y) + (set! (-> font-ctx color) old-color)) + (none)) + +(defun draw-dynamic-entry ((entry-id int) (get-label (function int string none)) (entry-selected? (function int symbol)) (font-ctx font-context) (dma-buf dma-buffer) (hovering? symbol)) + (let ((old-x (-> font-ctx origin x)) + (old-y (-> font-ctx origin y)) + (old-color (-> font-ctx color))) + (when (entry-selected? entry-id) + (set! (-> font-ctx color) (font-color green)) + (set! (-> font-ctx origin x) (- old-x 6.0)) + (draw-string-adv "\c86" dma-buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y) + (set! (-> font-ctx color) old-color)) + (clear *pc-encoded-temp-string*) + (get-label entry-id *pc-encoded-temp-string*) + (pc-encode-utf8-string *pc-encoded-temp-string* *pc-encoded-temp-string*) + (when hovering? + (set! (-> font-ctx color) (font-color cyan))) + (draw-string-adv *pc-encoded-temp-string* dma-buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y) + (set! (-> font-ctx color) old-color)) + (none)) + +(defmethod draw-menu ((this popup-menu)) + (let ((font-ctx (new 'debug 'font-context *font-default-matrix* 0 0 0.0 (font-color default) (font-flags shadow kerning large))) + (page-title (-> this menu-states (-> this curr-state-index) title)) + (dynamic-menu? (-> this menu-states (-> this curr-state-index) dynamic-menu?)) + (can-reset? (and (nonzero? (-> this menu-states (-> this curr-state-index) on-dynamic-menu-reset)) + (-> this menu-states (-> this curr-state-index) on-dynamic-menu-reset)))) + (set! (-> font-ctx scale) 0.25) + (set! (-> font-ctx origin x) 15.0) + (set! (-> font-ctx origin y) 75.0) + (let* ((entry-count (if dynamic-menu? + ((-> this menu-states (-> this curr-state-index) get-dynamic-menu-length)) + (-> this menu-states (-> this curr-state-index) entries length))) + (current-index (-> this menu-states (-> this curr-state-index) entry-index)) + (start-index (* (/ current-index 15) 15)) + (end-index (min (+ start-index 15) entry-count)) + (entry-count-to-render (- end-index start-index)) + (menu-rows (if (< end-index entry-count) (inc entry-count-to-render) entry-count-to-render)) + (widest-entry (if dynamic-menu? + (get-widest-dynamic-entry (-> this menu-states (-> this curr-state-index) get-dynamic-menu-entry-label) page-title font-ctx start-index end-index) + (get-widest-entry (-> this menu-states (-> this curr-state-index) entries) page-title font-ctx start-index end-index)))) + (with-dma-buffer-add-bucket ((buf (-> (current-frame) global-buf)) (bucket-id debug-no-zbuf2)) + ;; background border + (draw-sprite2d-xy buf + 6 + 64 + (+ 17 widest-entry) ;; width + (+ 17 (* 15 (inc menu-rows))) ;; height + (new 'static 'rgba :r 255 :g 255 :b 255 :a 75)) + ;; background + (draw-sprite2d-xy buf + 7 + 65 + (+ 15 widest-entry) ;; width + (+ 15 (* 15 (inc menu-rows))) ;; height + (new 'static 'rgba :r 0 :g 0 :b 0 :a 255)) + ;; title + ;; TODO - function + (pc-encode-utf8-string page-title *pc-encoded-temp-string*) + (set! (-> font-ctx color) (font-color menu-parent)) + (let ((old-x (-> font-ctx origin x)) + (old-y (-> font-ctx origin y))) + (draw-string-adv *pc-encoded-temp-string* buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y)) + (set! (-> font-ctx color) (font-color default)) + (set! (-> font-ctx origin y) (+ 15.0 (-> font-ctx origin y))) + ;; menu contents + (dotimes (i entry-count-to-render) + (if dynamic-menu? + (draw-dynamic-entry (+ i start-index) + (-> this menu-states (-> this curr-state-index) get-dynamic-menu-entry-label) + (-> this menu-states (-> this curr-state-index) dynamic-menu-entry-selected?) + font-ctx + buf + (= (+ i start-index) current-index)) + (draw-entry (-> (-> this menu-states (-> this curr-state-index) entries) i) font-ctx buf (= (+ i start-index) current-index))) + (set! (-> font-ctx origin y) (+ 15.0 (-> font-ctx origin y)))) + (when (< end-index entry-count) + (clear *pc-encoded-temp-string*) + (format *pc-encoded-temp-string* "~D more..." (- entry-count end-index)) + (pc-encode-utf8-string *pc-encoded-temp-string* *pc-encoded-temp-string*) + (set! (-> font-ctx color) (font-color menu-parent)) + (let ((old-x (-> font-ctx origin x)) + (old-y (-> font-ctx origin y))) + (draw-string-adv *pc-encoded-temp-string* buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y)) + (set! (-> font-ctx color) (font-color default)) + (set! (-> font-ctx origin y) (+ 15.0 (-> font-ctx origin y)))) + ;; button prompts + (cond + ((= (-> this curr-state-index) 0) + (pc-encode-utf8-string " Exit" *pc-encoded-temp-string*) + ) + ((and dynamic-menu? can-reset?) + (pc-encode-utf8-string " Reset / Back" *pc-encoded-temp-string*)) + (else + (pc-encode-utf8-string " Back" *pc-encoded-temp-string*)) + ) + (set! (-> font-ctx origin x) (- 25.0 (-> font-ctx origin x))) + (set! (-> font-ctx origin y) (+ 10.0 (-> font-ctx origin y))) + (let ((old-x (-> font-ctx origin x)) + (old-y (-> font-ctx origin y))) + (draw-string-adv *pc-encoded-temp-string* buf font-ctx) + (set! (-> font-ctx origin x) old-x) + (set! (-> font-ctx origin y) old-y)) + ))) + (none)) + +(defmethod move-up! ((this popup-menu) (amount int)) + (let* ((curr-state (-> this menu-states (-> this curr-state-index))) + (new-index (max 0 (-! (-> curr-state entry-index) amount)))) + ;; dynamic menus don't have options that are disabled (just dont include them) + (when (not (-> curr-state dynamic-menu?)) + (let ((entry (-> curr-state entries new-index))) + (when (and (nonzero? (-> entry entry-disabled?)) ((-> entry entry-disabled?))) + (set! new-index (max 0 (dec new-index)))))) + (set! (-> curr-state entry-index) new-index)) + (none)) + +(defmethod move-down! ((this popup-menu) (amount int)) + (let* ((curr-state (-> this menu-states (-> this curr-state-index))) + (max-entries (if (-> curr-state dynamic-menu?) + ((-> curr-state get-dynamic-menu-length)) + (-> curr-state entries length))) + (new-index (min (dec max-entries) (+! (-> curr-state entry-index) amount)))) + ;; dynamic menus don't have options that are disabled (just dont include them) + (when (not (-> curr-state dynamic-menu?)) + (let ((entry (-> curr-state entries new-index))) + (when (and (nonzero? (-> entry entry-disabled?)) ((-> entry entry-disabled?))) + (set! new-index (min (dec max-entries) (inc new-index)))))) + (set! (-> curr-state entry-index) new-index)) + (none)) + +(defmethod confirm! ((this popup-menu)) + (let* ((menu-state (-> this menu-states (-> this curr-state-index))) + (dynamic-menu? (-> menu-state dynamic-menu?))) + (if dynamic-menu? + ((-> menu-state on-dynamic-menu-entry-confirm) (-> menu-state entry-index)) + (let ((entry (-> menu-state entries (-> menu-state entry-index)))) + (cond + ((type? entry popup-menu-dynamic-submenu) + ;; TODO - dont allow more than 10 nested menus + (inc! (-> this curr-state-index)) + (set! (-> this menu-states (-> this curr-state-index) entry-index) 0) + (set! (-> this menu-states (-> this curr-state-index) title) (-> entry label)) + (set! (-> this menu-states (-> this curr-state-index) dynamic-menu?) #t) + (set! (-> this menu-states (-> this curr-state-index) get-dynamic-menu-length) (-> (the-as popup-menu-dynamic-submenu entry) get-length)) + (set! (-> this menu-states (-> this curr-state-index) get-dynamic-menu-entry-label) (-> (the-as popup-menu-dynamic-submenu entry) get-entry-label)) + (set! (-> this menu-states (-> this curr-state-index) on-dynamic-menu-entry-confirm) (-> (the-as popup-menu-dynamic-submenu entry) on-entry-confirm)) + (set! (-> this menu-states (-> this curr-state-index) dynamic-menu-entry-selected?) (-> (the-as popup-menu-dynamic-submenu entry) entry-selected?)) + (set! (-> this menu-states (-> this curr-state-index) on-dynamic-menu-reset) (-> (the-as popup-menu-dynamic-submenu entry) on-reset))) + ((type? entry popup-menu-submenu) + ;; TODO - dont allow more than 10 nested menus + (inc! (-> this curr-state-index)) + (set! (-> this menu-states (-> this curr-state-index) entry-index) 0) + (set! (-> this menu-states (-> this curr-state-index) dynamic-menu?) #f) + (set! (-> this menu-states (-> this curr-state-index) title) (-> entry label)) + (set! (-> this menu-states (-> this curr-state-index) entries) (-> (the-as popup-menu-submenu entry) entries))) + (else + ((-> entry on-confirm))))))) + (sound-play "menu-pick") + (none)) + +(defmethod reset! ((this popup-menu)) + (let* ((menu-state (-> this menu-states (-> this curr-state-index)))) + (when (and (-> menu-state dynamic-menu?) + (nonzero? (-> menu-state on-dynamic-menu-reset)) + (-> menu-state on-dynamic-menu-reset)) ;; dont call if theres no function defined + ((-> menu-state on-dynamic-menu-reset)) + (sound-play "menu-pick"))) + (none)) + +(defmethod back! ((this popup-menu)) + (sound-play "menu-pick") + (cond + ((<= (-> this curr-state-index) 0) + #t) + (else + (dec! (-> this curr-state-index)) + #f))) + +(defbehavior popup-menu-init popup-menu ((title string) (entries (array popup-menu-entry))) + (set! (-> self curr-state-index) 0) + (set! (-> self menu-states 0 title) title) + (set! (-> self menu-states 0 entries) entries) + (set! (-> self menu-states 0 entry-index) 0) + (set! (-> self menu-states 0 dynamic-menu?) #f) + (set! (-> self draw?) #f) + (go-virtual closed) + (none)) + +(defbehavior popup-menu-event-handler popup-menu ((proc process) (arg1 int) (event-type symbol) (event event-message-block)) + (case event-type + (('open-menu) + (set! (-> self draw?) #t) + (set! *popup-menu-open* #t) + (sound-play "menu-pick") + (go-virtual opened)) + (('close-menu) + (set! (-> self draw?) #f) + (set! *popup-menu-open* #f) + (go-virtual closed))) + (the-as object 0)) + +(defmethod update-menu! ((this popup-menu)) + "This can't be done inside a state because the popup-menu is used when the game is paused + during which time, processes are not executed." + (when (-> this draw?) + ;; handle input + (cond + ((cpad-pressed? 0 select) + (send-event this 'close-menu)) + ((cpad-pressed? 0 up) + (move-up! this 1)) + ((cpad-pressed? 0 down) + (move-down! this 1)) + ((cpad-pressed? 0 left) + (move-up! this 5)) + ((cpad-pressed? 0 right) + (move-down! this 5)) + ((cpad-pressed? 0 x) + (confirm! this)) + ((cpad-pressed? 0 square) + (reset! this)) + ((cpad-pressed? 0 triangle circle) + (when (back! this) + (send-event this 'close-menu)))) + (draw-menu this)) + (none)) + +(defstate closed (popup-menu) + :virtual #t + :event popup-menu-event-handler + :trans (behavior () + (none)) + :code (behavior () + (until #f (suspend)) + (none)) + :post (behavior () + (none))) + +(defstate opened (popup-menu) + :virtual #t + :event popup-menu-event-handler + :trans (behavior () + (none)) + :code (behavior () + (until #f (suspend)) + (none)) + :post (behavior () + (none)))