mirror of
https://github.com/open-goal/jak-project.git
synced 2024-11-23 14:20:07 +00:00
g/j2: Dynamic speedrun mode categories and implement a significant amount of a practice mode (#3378)
For example, `AppData/OpenGOAL/jak2/features/speedrun-categories.json` is defined as such: ```json [ { "cheats": 0, "completed_task": 0, "continue_point_name": "", "features": 0, "forbidden_features": 992, "name": "Gunless", "secrets": 0 }, { "cheats": 1, "completed_task": 29, "continue_point_name": "ctypal-shaft", "features": 1024, "forbidden_features": 0, "name": "Turbo Jetboard - After Praxis 1", "secrets": 0 } ] ``` > These entries can be created using the in-game menu as well. https://github.com/open-goal/jak-project/assets/13153231/9b17a116-4aa9-40ad-b9f5-02b04e0ad4f3 --------- Co-authored-by: dallmeyer <2515356+dallmeyer@users.noreply.github.com>
This commit is contained in:
parent
685ff2cf1c
commit
db66ae4627
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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<DiscordInfo>(discord_info).c() : NULL;
|
||||
if (info) {
|
||||
// Get the data from GOAL
|
||||
int orbs = (int)info->orb_count;
|
||||
int gems = (int)info->gem_count;
|
||||
// convert encodings
|
||||
std::string status = get_font_bank(GameTextVersion::JAK2)
|
||||
->convert_game_to_utf8(Ptr<String>(info->status).c()->data());
|
||||
|
||||
// get rid of special encodings like <COLOR_WHITE>
|
||||
std::regex r("<.*?>");
|
||||
while (std::regex_search(status, r)) {
|
||||
status = std::regex_replace(status, r, "");
|
||||
}
|
||||
|
||||
char* level = Ptr<String>(info->level).c()->data();
|
||||
auto cutscene = Ptr<Symbol4<u32>>(info->cutscene)->value();
|
||||
float time = info->time_of_day;
|
||||
float percent_completed = info->percent_completed;
|
||||
std::bitset<32> focus_status;
|
||||
focus_status = info->focus_status;
|
||||
char* task = Ptr<String>(info->task).c()->data();
|
||||
|
||||
// Construct the DiscordRPC Object
|
||||
const char* full_level_name =
|
||||
get_full_level_name(level_names, level_name_remap, Ptr<String>(info->level).c()->data());
|
||||
memset(&rpc, 0, sizeof(rpc));
|
||||
// if we have an active task, set the mission specific image for it
|
||||
// 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<std::string> levels;
|
||||
for (int i = 0; i < LEVEL_MAX; i++) {
|
||||
u32 lev = *Ptr<u32>(lev_list + i * 4);
|
||||
std::string ls = Ptr<String>(lev).c()->data();
|
||||
if (ls != "none" && ls != "#f" && ls != "") {
|
||||
levels.push_back(ls);
|
||||
}
|
||||
}
|
||||
|
||||
Gfx::GetCurrentRenderer()->set_levels(levels);
|
||||
}
|
||||
|
||||
void pc_set_active_levels(u32 lev_list) {
|
||||
if (!Gfx::GetCurrentRenderer()) {
|
||||
return;
|
||||
}
|
||||
std::vector<std::string> levels;
|
||||
for (int i = 0; i < LEVEL_MAX; i++) {
|
||||
u32 lev = *Ptr<u32>(lev_list + i * 4);
|
||||
std::string ls = Ptr<String>(lev).c()->data();
|
||||
if (ls != "none" && ls != "#f" && ls != "") {
|
||||
levels.push_back(ls);
|
||||
}
|
||||
}
|
||||
|
||||
Gfx::GetCurrentRenderer()->set_active_levels(levels);
|
||||
}
|
||||
|
||||
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<u64>)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<u64>(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<String>(src_str_ptr).c()->data());
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(str);
|
||||
strcpy(Ptr<String>(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<std::string, std::vector<std::pair<std::string, float>>>
|
||||
external_speedrun_time_cache = {};
|
||||
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
|
||||
external_race_time_cache = {};
|
||||
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
|
||||
external_highscores_cache = {};
|
||||
|
||||
// clang-format off
|
||||
// TODO - eventually don't depend on SRC
|
||||
const std::unordered_map<std::string, std::string> external_speedrun_lookup_urls = {
|
||||
{"any", "https://www.speedrun.com/api/v1/leaderboards/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<std::string, std::string> 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<std::string, std::string> 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<std::string> 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<std::pair<std::string, float>> times = {};
|
||||
for (const auto& run_info : runs) {
|
||||
std::pair<std::string, float> time_info;
|
||||
if (players.size() > times.size() && players.at(times.size()).contains("names") &&
|
||||
players.at(times.size()).at("names").contains("international")) {
|
||||
time_info.first = players.at(times.size()).at("names").at("international");
|
||||
} else if (players.size() > times.size() && players.at(times.size()).contains("name")) {
|
||||
time_info.first = players.at(times.size()).at("name");
|
||||
} else {
|
||||
time_info.first = "Unknown";
|
||||
}
|
||||
if (run_info.contains("run") && run_info.at("run").contains("times") &&
|
||||
run_info.at("run").at("times").contains("primary_t")) {
|
||||
time_info.second = run_info.at("run").at("times").at("primary_t");
|
||||
times.push_back(time_info);
|
||||
}
|
||||
}
|
||||
external_speedrun_time_cache[cache_id] = times;
|
||||
intern_from_c("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
|
||||
}
|
||||
|
||||
// TODO - duplicate code, put it in a function
|
||||
void callback_fetch_external_race_times(bool success,
|
||||
const std::string& cache_id,
|
||||
std::optional<std::string> result) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
|
||||
if (!success) {
|
||||
intern_from_c("*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<std::pair<std::string, float>> times = {};
|
||||
for (const auto& run_info : runs) {
|
||||
std::pair<std::string, float> time_info;
|
||||
if (players.size() > times.size() && players.at(times.size()).contains("names") &&
|
||||
players.at(times.size()).at("names").contains("international")) {
|
||||
time_info.first = players.at(times.size()).at("names").at("international");
|
||||
} else if (players.size() > times.size() && players.at(times.size()).contains("name")) {
|
||||
time_info.first = players.at(times.size()).at("name");
|
||||
} else {
|
||||
time_info.first = "Unknown";
|
||||
}
|
||||
if (run_info.contains("run") && run_info.at("run").contains("times") &&
|
||||
run_info.at("run").at("times").contains("primary_t")) {
|
||||
time_info.second = run_info.at("run").at("times").at("primary_t");
|
||||
times.push_back(time_info);
|
||||
}
|
||||
}
|
||||
external_race_time_cache[cache_id] = times;
|
||||
intern_from_c("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
|
||||
}
|
||||
|
||||
// TODO - duplicate code, put it in a function
|
||||
void callback_fetch_external_highscores(bool success,
|
||||
const std::string& cache_id,
|
||||
std::optional<std::string> result) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
|
||||
if (!success) {
|
||||
intern_from_c("*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<std::pair<std::string, float>> times = {};
|
||||
for (const auto& highscore_info : data.value()) {
|
||||
if (highscore_info.contains("playerName") && highscore_info.contains("score")) {
|
||||
std::pair<std::string, float> time_info;
|
||||
time_info.first = highscore_info.at("playerName");
|
||||
time_info.second = highscore_info.at("score");
|
||||
times.push_back(time_info);
|
||||
}
|
||||
}
|
||||
external_highscores_cache[cache_id] = times;
|
||||
intern_from_c("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
|
||||
}
|
||||
|
||||
void pc_fetch_external_speedrun_times(u32 speedrun_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
|
||||
if (external_speedrun_lookup_urls.find(speedrun_id) == external_speedrun_lookup_urls.end()) {
|
||||
lg::error("No URL for speedrun_id: '{}'", speedrun_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check to see if we've already retrieved this info
|
||||
if (external_speedrun_time_cache.find(speedrun_id) == external_speedrun_time_cache.end()) {
|
||||
intern_from_c("*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<String>(race_id_ptr).c()->data());
|
||||
if (external_race_lookup_urls.find(race_id) == external_race_lookup_urls.end()) {
|
||||
lg::error("No URL for race_id: '{}'", race_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check to see if we've already retrieved this info
|
||||
if (external_race_time_cache.find(race_id) == external_race_time_cache.end()) {
|
||||
intern_from_c("*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<String>(highscore_id_ptr).c()->data());
|
||||
if (external_highscores_lookup_urls.find(highscore_id) == external_highscores_lookup_urls.end()) {
|
||||
lg::error("No URL for highscore_id: '{}'", highscore_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check to see if we've already retrieved this info
|
||||
if (external_highscores_cache.find(highscore_id) == external_highscores_cache.end()) {
|
||||
intern_from_c("*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<String>(speedrun_id_ptr).c()->data());
|
||||
if (external_speedrun_time_cache.find(speedrun_id) != external_speedrun_time_cache.end()) {
|
||||
const auto& runs = external_speedrun_time_cache.at(speedrun_id);
|
||||
if (index < (int)runs.size()) {
|
||||
const auto& run_info = external_speedrun_time_cache.at(speedrun_id).at(index);
|
||||
std::string converted =
|
||||
get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(run_info.first);
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
|
||||
} else {
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game("");
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pc_get_external_race_time(u32 race_id_ptr, s32 index, u32 name_dest_ptr, u32 time_dest_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
|
||||
if (external_race_time_cache.find(race_id) != external_race_time_cache.end()) {
|
||||
const auto& runs = external_race_time_cache.at(race_id);
|
||||
if (index < (int)runs.size()) {
|
||||
const auto& run_info = external_race_time_cache.at(race_id).at(index);
|
||||
std::string converted =
|
||||
get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(run_info.first);
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
|
||||
} else {
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game("");
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pc_get_external_highscore(u32 highscore_id_ptr,
|
||||
s32 index,
|
||||
u32 name_dest_ptr,
|
||||
u32 time_dest_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
|
||||
if (external_highscores_cache.find(highscore_id) != external_highscores_cache.end()) {
|
||||
const auto& runs = external_highscores_cache.at(highscore_id);
|
||||
if (index < (int)runs.size()) {
|
||||
const auto& run_info = external_highscores_cache.at(highscore_id).at(index);
|
||||
std::string converted =
|
||||
get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(run_info.first);
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
|
||||
} else {
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game("");
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s32 pc_get_num_external_speedrun_times(u32 speedrun_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
|
||||
if (external_speedrun_time_cache.find(speedrun_id) != external_speedrun_time_cache.end()) {
|
||||
return external_speedrun_time_cache.at(speedrun_id).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
s32 pc_get_num_external_race_times(u32 race_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
|
||||
if (external_race_time_cache.find(race_id) != external_race_time_cache.end()) {
|
||||
return external_race_time_cache.at(race_id).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
s32 pc_get_num_external_highscores(u32 highscore_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
|
||||
if (external_highscores_cache.find(highscore_id) != external_highscores_cache.end()) {
|
||||
return external_highscores_cache.at(highscore_id).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void 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();
|
||||
|
@ -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<size_t>(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
|
||||
|
931
game/kernel/jak2/kmachine_extras.cpp
Normal file
931
game/kernel/jak2/kmachine_extras.cpp
Normal file
@ -0,0 +1,931 @@
|
||||
#include "kmachine_extras.h"
|
||||
|
||||
#include <bitset>
|
||||
#include <regex>
|
||||
|
||||
#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<DiscordInfo>(discord_info).c() : NULL;
|
||||
if (info) {
|
||||
// Get the data from GOAL
|
||||
int orbs = (int)info->orb_count;
|
||||
int gems = (int)info->gem_count;
|
||||
// convert encodings
|
||||
std::string status = get_font_bank(GameTextVersion::JAK2)
|
||||
->convert_game_to_utf8(Ptr<String>(info->status).c()->data());
|
||||
|
||||
// get rid of special encodings like <COLOR_WHITE>
|
||||
std::regex r("<.*?>");
|
||||
while (std::regex_search(status, r)) {
|
||||
status = std::regex_replace(status, r, "");
|
||||
}
|
||||
|
||||
char* level = Ptr<String>(info->level).c()->data();
|
||||
auto cutscene = Ptr<Symbol4<u32>>(info->cutscene)->value();
|
||||
float time = info->time_of_day;
|
||||
float percent_completed = info->percent_completed;
|
||||
std::bitset<32> focus_status;
|
||||
focus_status = info->focus_status;
|
||||
char* task = Ptr<String>(info->task).c()->data();
|
||||
|
||||
// Construct the DiscordRPC Object
|
||||
const char* full_level_name =
|
||||
get_full_level_name(level_names, level_name_remap, Ptr<String>(info->level).c()->data());
|
||||
memset(&rpc, 0, sizeof(rpc));
|
||||
// if we have an active task, set the mission specific image for it
|
||||
// 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<std::string> levels;
|
||||
for (int i = 0; i < LEVEL_MAX; i++) {
|
||||
u32 lev = *Ptr<u32>(lev_list + i * 4);
|
||||
std::string ls = Ptr<String>(lev).c()->data();
|
||||
if (ls != "none" && ls != "#f" && ls != "") {
|
||||
levels.push_back(ls);
|
||||
}
|
||||
}
|
||||
|
||||
Gfx::GetCurrentRenderer()->set_levels(levels);
|
||||
}
|
||||
|
||||
void pc_set_active_levels(u32 lev_list) {
|
||||
if (!Gfx::GetCurrentRenderer()) {
|
||||
return;
|
||||
}
|
||||
std::vector<std::string> levels;
|
||||
for (int i = 0; i < LEVEL_MAX; i++) {
|
||||
u32 lev = *Ptr<u32>(lev_list + i * 4);
|
||||
std::string ls = Ptr<String>(lev).c()->data();
|
||||
if (ls != "none" && ls != "#f" && ls != "") {
|
||||
levels.push_back(ls);
|
||||
}
|
||||
}
|
||||
|
||||
Gfx::GetCurrentRenderer()->set_active_levels(levels);
|
||||
}
|
||||
|
||||
u32 alloc_vagdir_names(u32 heap_sym) {
|
||||
auto alloced_heap = (Ptr<u64>)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<u64>(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<String>(src_str_ptr).c()->data());
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(str);
|
||||
strcpy(Ptr<String>(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<std::string, std::vector<std::pair<std::string, float>>>
|
||||
external_speedrun_time_cache = {};
|
||||
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
|
||||
external_race_time_cache = {};
|
||||
std::unordered_map<std::string, std::vector<std::pair<std::string, float>>>
|
||||
external_highscores_cache = {};
|
||||
|
||||
// clang-format off
|
||||
// TODO - eventually don't depend on SRC
|
||||
const std::unordered_map<std::string, std::string> external_speedrun_lookup_urls = {
|
||||
{"any", "https://www.speedrun.com/api/v1/leaderboards/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<std::string, std::string> 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<std::string, std::string> 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<std::string> 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<std::pair<std::string, float>> times = {};
|
||||
for (const auto& run_info : runs) {
|
||||
std::pair<std::string, float> time_info;
|
||||
if (players.size() > times.size() && players.at(times.size()).contains("names") &&
|
||||
players.at(times.size()).at("names").contains("international")) {
|
||||
time_info.first = players.at(times.size()).at("names").at("international");
|
||||
} else if (players.size() > times.size() && players.at(times.size()).contains("name")) {
|
||||
time_info.first = players.at(times.size()).at("name");
|
||||
} else {
|
||||
time_info.first = "Unknown";
|
||||
}
|
||||
if (run_info.contains("run") && run_info.at("run").contains("times") &&
|
||||
run_info.at("run").at("times").contains("primary_t")) {
|
||||
time_info.second = run_info.at("run").at("times").at("primary_t");
|
||||
times.push_back(time_info);
|
||||
}
|
||||
}
|
||||
external_speedrun_time_cache[cache_id] = times;
|
||||
intern_from_c("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
|
||||
}
|
||||
|
||||
// TODO - duplicate code, put it in a function
|
||||
void callback_fetch_external_race_times(bool success,
|
||||
const std::string& cache_id,
|
||||
std::optional<std::string> result) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
|
||||
if (!success) {
|
||||
intern_from_c("*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<std::pair<std::string, float>> times = {};
|
||||
for (const auto& run_info : runs) {
|
||||
std::pair<std::string, float> time_info;
|
||||
if (players.size() > times.size() && players.at(times.size()).contains("names") &&
|
||||
players.at(times.size()).at("names").contains("international")) {
|
||||
time_info.first = players.at(times.size()).at("names").at("international");
|
||||
} else if (players.size() > times.size() && players.at(times.size()).contains("name")) {
|
||||
time_info.first = players.at(times.size()).at("name");
|
||||
} else {
|
||||
time_info.first = "Unknown";
|
||||
}
|
||||
if (run_info.contains("run") && run_info.at("run").contains("times") &&
|
||||
run_info.at("run").at("times").contains("primary_t")) {
|
||||
time_info.second = run_info.at("run").at("times").at("primary_t");
|
||||
times.push_back(time_info);
|
||||
}
|
||||
}
|
||||
external_race_time_cache[cache_id] = times;
|
||||
intern_from_c("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
|
||||
}
|
||||
|
||||
// TODO - duplicate code, put it in a function
|
||||
void callback_fetch_external_highscores(bool success,
|
||||
const std::string& cache_id,
|
||||
std::optional<std::string> result) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
|
||||
if (!success) {
|
||||
intern_from_c("*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<std::pair<std::string, float>> times = {};
|
||||
for (const auto& highscore_info : data.value()) {
|
||||
if (highscore_info.contains("playerName") && highscore_info.contains("score")) {
|
||||
std::pair<std::string, float> time_info;
|
||||
time_info.first = highscore_info.at("playerName");
|
||||
time_info.second = highscore_info.at("score");
|
||||
times.push_back(time_info);
|
||||
}
|
||||
}
|
||||
external_highscores_cache[cache_id] = times;
|
||||
intern_from_c("*pc-waiting-on-rpc?*")->value() = bool_to_symbol(false);
|
||||
}
|
||||
|
||||
void pc_fetch_external_speedrun_times(u32 speedrun_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
|
||||
if (external_speedrun_lookup_urls.find(speedrun_id) == external_speedrun_lookup_urls.end()) {
|
||||
lg::error("No URL for speedrun_id: '{}'", speedrun_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check to see if we've already retrieved this info
|
||||
if (external_speedrun_time_cache.find(speedrun_id) == external_speedrun_time_cache.end()) {
|
||||
intern_from_c("*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<String>(race_id_ptr).c()->data());
|
||||
if (external_race_lookup_urls.find(race_id) == external_race_lookup_urls.end()) {
|
||||
lg::error("No URL for race_id: '{}'", race_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check to see if we've already retrieved this info
|
||||
if (external_race_time_cache.find(race_id) == external_race_time_cache.end()) {
|
||||
intern_from_c("*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<String>(highscore_id_ptr).c()->data());
|
||||
if (external_highscores_lookup_urls.find(highscore_id) == external_highscores_lookup_urls.end()) {
|
||||
lg::error("No URL for highscore_id: '{}'", highscore_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// First check to see if we've already retrieved this info
|
||||
if (external_highscores_cache.find(highscore_id) == external_highscores_cache.end()) {
|
||||
intern_from_c("*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<String>(speedrun_id_ptr).c()->data());
|
||||
if (external_speedrun_time_cache.find(speedrun_id) != external_speedrun_time_cache.end()) {
|
||||
const auto& runs = external_speedrun_time_cache.at(speedrun_id);
|
||||
if (index < (int)runs.size()) {
|
||||
const auto& run_info = external_speedrun_time_cache.at(speedrun_id).at(index);
|
||||
std::string converted =
|
||||
get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(run_info.first);
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
|
||||
} else {
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game("");
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pc_get_external_race_time(u32 race_id_ptr, s32 index, u32 name_dest_ptr, u32 time_dest_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
|
||||
if (external_race_time_cache.find(race_id) != external_race_time_cache.end()) {
|
||||
const auto& runs = external_race_time_cache.at(race_id);
|
||||
if (index < (int)runs.size()) {
|
||||
const auto& run_info = external_race_time_cache.at(race_id).at(index);
|
||||
std::string converted =
|
||||
get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(run_info.first);
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
|
||||
} else {
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game("");
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pc_get_external_highscore(u32 highscore_id_ptr,
|
||||
s32 index,
|
||||
u32 name_dest_ptr,
|
||||
u32 time_dest_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
|
||||
if (external_highscores_cache.find(highscore_id) != external_highscores_cache.end()) {
|
||||
const auto& runs = external_highscores_cache.at(highscore_id);
|
||||
if (index < (int)runs.size()) {
|
||||
const auto& run_info = external_highscores_cache.at(highscore_id).at(index);
|
||||
std::string converted =
|
||||
get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game(run_info.first);
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = run_info.second;
|
||||
} else {
|
||||
std::string converted = get_font_bank(GameTextVersion::JAK2)->convert_utf8_to_game("");
|
||||
strcpy(Ptr<String>(name_dest_ptr).c()->data(), converted.c_str());
|
||||
*(Ptr<float>(time_dest_ptr).c()) = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s32 pc_get_num_external_speedrun_times(u32 speedrun_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto speedrun_id = std::string(Ptr<String>(speedrun_id_ptr).c()->data());
|
||||
if (external_speedrun_time_cache.find(speedrun_id) != external_speedrun_time_cache.end()) {
|
||||
return external_speedrun_time_cache.at(speedrun_id).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
s32 pc_get_num_external_race_times(u32 race_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto race_id = std::string(Ptr<String>(race_id_ptr).c()->data());
|
||||
if (external_race_time_cache.find(race_id) != external_race_time_cache.end()) {
|
||||
return external_race_time_cache.at(race_id).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
s32 pc_get_num_external_highscores(u32 highscore_id_ptr) {
|
||||
std::scoped_lock lock{background_task_lock};
|
||||
auto highscore_id = std::string(Ptr<String>(highscore_id_ptr).c()->data());
|
||||
if (external_highscores_cache.find(highscore_id) != external_highscores_cache.end()) {
|
||||
return external_highscores_cache.at(highscore_id).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void to_json(json& j, const SpeedrunPracticeEntryHistoryAttempt& obj) {
|
||||
if (obj.time) {
|
||||
j["time"] = obj.time.value();
|
||||
} else {
|
||||
j["time"] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const json& j, SpeedrunPracticeEntryHistoryAttempt& obj) {
|
||||
if (j["time"].is_null()) {
|
||||
obj.time = {};
|
||||
} else {
|
||||
obj.time = j["time"];
|
||||
}
|
||||
}
|
||||
|
||||
void to_json(json& j, const SpeedrunPracticeEntry& obj) {
|
||||
json_serialize(name);
|
||||
json_serialize(continue_point_name);
|
||||
json_serialize(flags);
|
||||
json_serialize(completed_task);
|
||||
json_serialize(features);
|
||||
json_serialize(secrets);
|
||||
json_serialize(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<SpeedrunPracticeEntry> g_speedrun_practice_entries;
|
||||
std::unordered_map<int, SpeedrunPracticeState> g_speedrun_practice_state;
|
||||
|
||||
s32 pc_sr_mode_get_practice_entries_amount() {
|
||||
// load practice entries from the file
|
||||
const auto file_path =
|
||||
file_util::get_user_features_dir(g_game_version) / "speedrun-practice.json";
|
||||
if (!file_util::file_exists(file_path.string())) {
|
||||
lg::info("speedrun-practice.json not found, no entries to return!");
|
||||
return 0;
|
||||
}
|
||||
const auto file_contents = safe_parse_json(file_util::read_text_file(file_path));
|
||||
if (!file_contents) {
|
||||
lg::error("speedrun-practice.json could not be parsed!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_speedrun_practice_entries = *file_contents;
|
||||
|
||||
for (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<String>(name_str_ptr).c()->data(), name.c_str());
|
||||
}
|
||||
|
||||
void pc_sr_mode_get_practice_entry_continue_point(s32 entry_index, u32 name_str_ptr) {
|
||||
std::string name = "";
|
||||
if (!g_speedrun_practice_entries.size() <= entry_index) {
|
||||
name = g_speedrun_practice_entries.at(entry_index).continue_point_name;
|
||||
}
|
||||
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
|
||||
}
|
||||
|
||||
s32 pc_sr_mode_get_practice_entry_history_success(s32 entry_index) {
|
||||
return g_speedrun_practice_state.at(entry_index).total_successes;
|
||||
}
|
||||
|
||||
s32 pc_sr_mode_get_practice_entry_history_attempts(s32 entry_index) {
|
||||
return g_speedrun_practice_state.at(entry_index).total_attempts;
|
||||
}
|
||||
|
||||
s32 pc_sr_mode_get_practice_entry_session_success(s32 entry_index) {
|
||||
return g_speedrun_practice_state.at(entry_index).session_successes;
|
||||
}
|
||||
|
||||
s32 pc_sr_mode_get_practice_entry_session_attempts(s32 entry_index) {
|
||||
return g_speedrun_practice_state.at(entry_index).session_attempts;
|
||||
}
|
||||
|
||||
void pc_sr_mode_get_practice_entry_avg_time(s32 entry_index, u32 time_str_ptr) {
|
||||
const auto time = fmt::format("{:.2f}", g_speedrun_practice_state.at(entry_index).average_time);
|
||||
strcpy(Ptr<String>(time_str_ptr).c()->data(), time.c_str());
|
||||
}
|
||||
|
||||
void pc_sr_mode_get_practice_entry_fastest_time(s32 entry_index, u32 time_str_ptr) {
|
||||
const auto time = fmt::format("{:.2f}", g_speedrun_practice_state.at(entry_index).fastest_time);
|
||||
strcpy(Ptr<String>(time_str_ptr).c()->data(), time.c_str());
|
||||
}
|
||||
|
||||
u64 pc_sr_mode_record_practice_entry_attempt(s32 entry_index, u32 success_bool, u32 time_ptr) {
|
||||
auto& state = g_speedrun_practice_state.at(entry_index);
|
||||
const auto was_successful = symbol_to_bool(success_bool);
|
||||
state.total_attempts++;
|
||||
state.session_attempts++;
|
||||
bool ret = false;
|
||||
SpeedrunPracticeEntryHistoryAttempt new_history_entry;
|
||||
if (was_successful) {
|
||||
auto time = Ptr<float>(time_ptr).c();
|
||||
new_history_entry.time = *time;
|
||||
state.total_successes++;
|
||||
state.session_successes++;
|
||||
state.total_time += *time;
|
||||
state.average_time = state.total_time / state.total_successes;
|
||||
if (*time < state.fastest_time) {
|
||||
state.fastest_time = *time;
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
// persist to file
|
||||
const auto file_path =
|
||||
file_util::get_user_features_dir(g_game_version) / "speedrun-practice.json";
|
||||
if (!file_util::file_exists(file_path.string())) {
|
||||
lg::info("speedrun-practice.json not found, not persisting!");
|
||||
} else {
|
||||
auto& history = g_speedrun_practice_entries.at(entry_index).history;
|
||||
if (history.find(fmt::format("{}", state.current_session_id)) == history.end()) {
|
||||
history[fmt::format("{}", state.current_session_id)] = {};
|
||||
}
|
||||
history[fmt::format("{}", state.current_session_id)].push_back(new_history_entry);
|
||||
json data = g_speedrun_practice_entries;
|
||||
file_util::write_text_file(file_path, data.dump(2));
|
||||
}
|
||||
// return
|
||||
return bool_to_symbol(ret);
|
||||
}
|
||||
|
||||
void pc_sr_mode_init_practice_info(s32 entry_index, u32 speedrun_practice_obj_ptr) {
|
||||
if (entry_index >= g_speedrun_practice_entries.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto objective = speedrun_practice_obj_ptr
|
||||
? Ptr<SpeedrunPracticeObjective>(speedrun_practice_obj_ptr).c()
|
||||
: NULL;
|
||||
if (objective) {
|
||||
const auto& json_info = g_speedrun_practice_entries.at(entry_index);
|
||||
|
||||
objective->index = entry_index;
|
||||
objective->flags = json_info.flags;
|
||||
objective->completed_task = json_info.completed_task;
|
||||
objective->features = json_info.features;
|
||||
objective->secrets = json_info.secrets;
|
||||
auto starting_position =
|
||||
objective->starting_position ? Ptr<Vector>(objective->starting_position).c() : NULL;
|
||||
if (starting_position) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
starting_position->data[i] = json_info.starting_position.at(i) * 4096.0;
|
||||
}
|
||||
}
|
||||
auto starting_rotation =
|
||||
objective->starting_rotation ? Ptr<Vector>(objective->starting_rotation).c() : NULL;
|
||||
if (starting_rotation) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
starting_rotation->data[i] = json_info.starting_rotation.at(i);
|
||||
}
|
||||
}
|
||||
auto starting_camera_position = objective->starting_camera_position
|
||||
? Ptr<Vector>(objective->starting_camera_position).c()
|
||||
: NULL;
|
||||
if (starting_camera_position) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
starting_camera_position->data[i] = json_info.starting_camera_position.at(i) * 4096.0;
|
||||
}
|
||||
}
|
||||
auto starting_camera_rotation = objective->starting_camera_rotation
|
||||
? Ptr<Vector>(objective->starting_camera_rotation).c()
|
||||
: NULL;
|
||||
if (starting_camera_rotation) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
starting_camera_rotation->data[i] = json_info.starting_camera_rotation.at(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (json_info.end_task) {
|
||||
objective->end_task = *json_info.end_task;
|
||||
} else {
|
||||
objective->end_task = 0;
|
||||
}
|
||||
|
||||
auto starting_zone = objective->start_zone_init_params
|
||||
? Ptr<ObjectiveZoneInitParams>(objective->start_zone_init_params).c()
|
||||
: NULL;
|
||||
if (starting_zone) {
|
||||
starting_zone->v1[0] = json_info.start_zone_v1.at(0) * 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<ObjectiveZoneInitParams>(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<SpeedrunCustomCategoryEntry> g_speedrun_custom_categories;
|
||||
|
||||
s32 pc_sr_mode_get_custom_category_amount() {
|
||||
// load practice entries from the file
|
||||
const auto file_path =
|
||||
file_util::get_user_features_dir(g_game_version) / "speedrun-categories.json";
|
||||
if (!file_util::file_exists(file_path.string())) {
|
||||
lg::info("speedrun-categories.json not found, no entries to return!");
|
||||
return 0;
|
||||
}
|
||||
const auto file_contents = safe_parse_json(file_util::read_text_file(file_path));
|
||||
if (!file_contents) {
|
||||
lg::error("speedrun-categories.json could not be parsed!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_speedrun_custom_categories = *file_contents;
|
||||
|
||||
return g_speedrun_custom_categories.size();
|
||||
}
|
||||
|
||||
void pc_sr_mode_get_custom_category_name(s32 entry_index, u32 name_str_ptr) {
|
||||
std::string name = "";
|
||||
if (!g_speedrun_custom_categories.size() <= entry_index) {
|
||||
name = g_speedrun_custom_categories.at(entry_index).name;
|
||||
}
|
||||
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
|
||||
}
|
||||
|
||||
void pc_sr_mode_get_custom_category_continue_point(s32 entry_index, u32 name_str_ptr) {
|
||||
std::string name = "";
|
||||
if (!g_speedrun_custom_categories.size() <= entry_index) {
|
||||
name = g_speedrun_custom_categories.at(entry_index).continue_point_name;
|
||||
}
|
||||
strcpy(Ptr<String>(name_str_ptr).c()->data(), name.c_str());
|
||||
}
|
||||
|
||||
void pc_sr_mode_init_custom_category_info(s32 entry_index, u32 speedrun_custom_category_ptr) {
|
||||
if (entry_index >= g_speedrun_custom_categories.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto category = speedrun_custom_category_ptr
|
||||
? Ptr<SpeedrunCustomCategory>(speedrun_custom_category_ptr).c()
|
||||
: NULL;
|
||||
if (category) {
|
||||
const auto& json_info = g_speedrun_custom_categories.at(entry_index);
|
||||
category->index = entry_index;
|
||||
category->secrets = json_info.secrets;
|
||||
category->features = json_info.features;
|
||||
category->forbidden_features = json_info.forbidden_features;
|
||||
category->cheats = json_info.cheats;
|
||||
category->completed_task = json_info.completed_task;
|
||||
}
|
||||
}
|
||||
|
||||
void pc_sr_mode_dump_new_custom_category(u32 speedrun_custom_category_ptr) {
|
||||
const auto file_path =
|
||||
file_util::get_user_features_dir(g_game_version) / "speedrun-categories.json";
|
||||
if (file_util::file_exists(file_path.string())) {
|
||||
// read current categories from file
|
||||
const auto file_contents = safe_parse_json(file_util::read_text_file(file_path));
|
||||
if (file_contents) {
|
||||
g_speedrun_custom_categories = *file_contents;
|
||||
}
|
||||
}
|
||||
|
||||
auto category = speedrun_custom_category_ptr
|
||||
? Ptr<SpeedrunCustomCategory>(speedrun_custom_category_ptr).c()
|
||||
: NULL;
|
||||
if (category) {
|
||||
SpeedrunCustomCategoryEntry new_category;
|
||||
new_category.name = fmt::format("custom-category-{}", g_speedrun_custom_categories.size());
|
||||
new_category.secrets = category->secrets;
|
||||
new_category.features = category->features;
|
||||
new_category.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
|
210
game/kernel/jak2/kmachine_extras.h
Normal file
210
game/kernel/jak2/kmachine_extras.h
Normal file
@ -0,0 +1,210 @@
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#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<std::string> result);
|
||||
void callback_fetch_external_race_times(bool success,
|
||||
const std::string& cache_id,
|
||||
std::optional<std::string> result);
|
||||
void callback_fetch_external_highscores(bool success,
|
||||
const std::string& cache_id,
|
||||
std::optional<std::string> result);
|
||||
void pc_fetch_external_speedrun_times(u32 speedrun_id_ptr);
|
||||
void pc_fetch_external_race_times(u32 race_id_ptr);
|
||||
void pc_fetch_external_highscores(u32 highscore_id_ptr);
|
||||
void pc_get_external_speedrun_time(u32 speedrun_id_ptr,
|
||||
s32 index,
|
||||
u32 name_dest_ptr,
|
||||
u32 time_dest_ptr);
|
||||
void pc_get_external_race_time(u32 race_id_ptr, s32 index, u32 name_dest_ptr, u32 time_dest_ptr);
|
||||
void pc_get_external_highscore(u32 highscore_id_ptr,
|
||||
s32 index,
|
||||
u32 name_dest_ptr,
|
||||
u32 time_dest_ptr);
|
||||
s32 pc_get_num_external_speedrun_times(u32 speedrun_id_ptr);
|
||||
s32 pc_get_num_external_race_times(u32 race_id_ptr);
|
||||
s32 pc_get_num_external_highscores(u32 highscore_id_ptr);
|
||||
s32 pc_sr_mode_get_practice_entries_amount();
|
||||
void pc_sr_mode_get_practice_entry_name(s32 entry_index, u32 name_str_ptr);
|
||||
void pc_sr_mode_get_practice_entry_continue_point(s32 entry_index, u32 name_str_ptr);
|
||||
s32 pc_sr_mode_get_practice_entry_history_success(s32 entry_index);
|
||||
s32 pc_sr_mode_get_practice_entry_history_attempts(s32 entry_index);
|
||||
s32 pc_sr_mode_get_practice_entry_session_success(s32 entry_index);
|
||||
s32 pc_sr_mode_get_practice_entry_session_attempts(s32 entry_index);
|
||||
void pc_sr_mode_get_practice_entry_avg_time(s32 entry_index, u32 time_str_ptr);
|
||||
void pc_sr_mode_get_practice_entry_fastest_time(s32 entry_index, u32 time_str_ptr);
|
||||
u64 pc_sr_mode_record_practice_entry_attempt(s32 entry_index, u32 success_bool, u32 time);
|
||||
void pc_sr_mode_init_practice_info(s32 entry_index, u32 speedrun_practice_obj_ptr);
|
||||
s32 pc_sr_mode_get_custom_category_amount();
|
||||
void pc_sr_mode_get_custom_category_name(s32 entry_index, u32 name_str_ptr);
|
||||
void pc_sr_mode_get_custom_category_continue_point(s32 entry_index, u32 name_str_ptr);
|
||||
void pc_sr_mode_init_custom_category_info(s32 entry_index, u32 speedrun_custom_category_ptr);
|
||||
void pc_sr_mode_dump_new_custom_category(u32 speedrun_custom_category_ptr);
|
||||
|
||||
struct DiscordInfo {
|
||||
float orb_count; // float
|
||||
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<size_t>(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<float> time;
|
||||
};
|
||||
void to_json(json& j, const SpeedrunPracticeEntryHistoryAttempt& obj);
|
||||
void from_json(const json& j, SpeedrunPracticeEntryHistoryAttempt& obj);
|
||||
|
||||
struct SpeedrunPracticeEntry {
|
||||
std::string name;
|
||||
std::string continue_point_name;
|
||||
u64 flags;
|
||||
u64 completed_task;
|
||||
u64 features;
|
||||
u64 secrets;
|
||||
std::vector<float> starting_position;
|
||||
std::vector<float> starting_rotation;
|
||||
std::vector<float> starting_camera_position;
|
||||
std::vector<float> starting_camera_rotation;
|
||||
std::vector<float> start_zone_v1;
|
||||
std::vector<float> start_zone_v2;
|
||||
std::optional<std::vector<float>> end_zone_v1;
|
||||
std::optional<std::vector<float>> end_zone_v2;
|
||||
std::optional<u64> end_task;
|
||||
std::map<std::string, std::vector<SpeedrunPracticeEntryHistoryAttempt>> history;
|
||||
};
|
||||
void to_json(json& j, const SpeedrunPracticeEntry& obj);
|
||||
void from_json(const json& j, SpeedrunPracticeEntry& obj);
|
||||
|
||||
struct SpeedrunPracticeState {
|
||||
s32 current_session_id;
|
||||
s32 total_attempts;
|
||||
s32 total_successes;
|
||||
s32 session_attempts;
|
||||
s32 session_successes;
|
||||
double total_time;
|
||||
float average_time;
|
||||
float fastest_time;
|
||||
};
|
||||
|
||||
struct ObjectiveZoneInitParams {
|
||||
float v1[4];
|
||||
float v2[4];
|
||||
};
|
||||
|
||||
struct Vector {
|
||||
float data[4];
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
float data[16];
|
||||
};
|
||||
|
||||
struct SpeedrunPracticeObjective {
|
||||
s32 index;
|
||||
u8 pad1[4];
|
||||
u64 flags;
|
||||
u8 completed_task;
|
||||
u8 pad2[7];
|
||||
u64 features;
|
||||
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
|
@ -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"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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* "<COLOR_WHITE>Practicing: <COLOR_GREEN>~S~%" *pc-encoded-temp-string*)
|
||||
(if (> (pc-sr-mode-get-practice-entry-history-attempts (-> this index)) 0)
|
||||
(format *temp-string* "<COLOR_WHITE>History: <COLOR_GREEN>~D<COLOR_WHITE>/~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* "<COLOR_WHITE>History: --~%"))
|
||||
(if (> (pc-sr-mode-get-practice-entry-session-attempts (-> this index)) 0)
|
||||
(format *temp-string* "<COLOR_WHITE>Session: <COLOR_GREEN>~D<COLOR_WHITE>/~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* "<COLOR_WHITE>Session: --~%"))
|
||||
(pc-sr-mode-get-practice-entry-avg-time (-> this index) *pc-encoded-temp-string*)
|
||||
(format *temp-string* "<COLOR_WHITE>Average Time: <COLOR_GREEN>~Ss~%" *pc-encoded-temp-string*)
|
||||
(pc-sr-mode-get-practice-entry-fastest-time (-> this index) *pc-encoded-temp-string*)
|
||||
(format *temp-string* "<COLOR_WHITE>Fastest Time: <COLOR_GREEN>~Ss~%" *pc-encoded-temp-string*)
|
||||
(format *temp-string* "<COLOR_WHITE>\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* "<COLOR_WHITE>Category: <COLOR_GREEN>~S~%<COLOR_WHITE>PC Cheats: <COLOR_GREEN>~D~%<COLOR_WHITE>Frame Rate: <COLOR_GREEN>~D~%<COLOR_WHITE>PS2 Actor Vis?: <COLOR_GREEN>~S~%<COLOR_WHITE>Version: <COLOR_GREEN>~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*
|
||||
"<COLOR_WHITE>Category: <COLOR_GREEN>~S~%<COLOR_WHITE>Secrets: <COLOR_GREEN>~D~%<COLOR_WHITE>Features: <COLOR_GREEN>~D~%<COLOR_WHITE>Forbidden Features: <COLOR_GREEN>~D~%<COLOR_WHITE>Cheats: <COLOR_GREEN>~D~%<COLOR_WHITE>Version: <COLOR_GREEN>~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*
|
||||
"<COLOR_WHITE>Category: <COLOR_GREEN>~S~%<COLOR_WHITE>PC Cheats: <COLOR_GREEN>~D~%<COLOR_WHITE>Frame Rate: <COLOR_GREEN>~D~%<COLOR_WHITE>PS2 Actor Vis?: <COLOR_GREEN>~S~%<COLOR_WHITE>Version: <COLOR_GREEN>~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))
|
||||
|
@ -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)
|
||||
|
@ -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* "<COLOR_WHITE>~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))))
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 ()
|
||||
|
@ -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))
|
||||
|
@ -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 "<PAD_CIRCLE> Exit" *pc-encoded-temp-string*)
|
||||
)
|
||||
((and dynamic-menu? can-reset?)
|
||||
(pc-encode-utf8-string "<PAD_SQUARE> Reset / <PAD_CIRCLE> Back" *pc-encoded-temp-string*))
|
||||
(else
|
||||
(pc-encode-utf8-string "<PAD_CIRCLE> 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)))
|
||||
|
Loading…
Reference in New Issue
Block a user