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:
Tyler Wilding 2024-02-23 19:04:44 -05:00 committed by GitHub
parent 685ff2cf1c
commit db66ae4627
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2122 additions and 800 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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();

View File

@ -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

View 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

View 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

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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))))

View File

@ -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

View File

@ -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 ()

View File

@ -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))

View File

@ -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)))