diff --git a/common/util/FontUtils.cpp b/common/util/FontUtils.cpp index 631c072433..b2e8448707 100644 --- a/common/util/FontUtils.cpp +++ b/common/util/FontUtils.cpp @@ -282,10 +282,6 @@ bool GameTextFontBank::valid_char_range(const char in) const { std::string GameTextFontBank::convert_game_to_utf8(const char* in) const { std::string temp; std::string result; - std::string test = in; - if (test.find("America") != std::string::npos) { - printf(""); - } while (*in) { auto remap = find_encode_to_utf8(in); if (remap != nullptr) { diff --git a/decompiler/config/jak2/all-types.gc b/decompiler/config/jak2/all-types.gc index eb0f5adfaf..56bd6cf15e 100644 --- a/decompiler/config/jak2/all-types.gc +++ b/decompiler/config/jak2/all-types.gc @@ -7210,6 +7210,16 @@ (scene-subtitles-disabled #x30e) (board-name #x30f) (progress-unknown-continue #x310) + + ;; PC port IDs START + (discord-rpc-not-in-mission #x1100) + (discord-rpc-in-side-mission #x1101) + (discord-rpc-red-gun-training #x1102) + (discord-rpc-yellow-gun-training #x1103) + (discord-rpc-blue-gun-training #x1104) + (discord-rpc-dark-gun-training #x1105) + (discord-rpc-onin-game #x1106) + (discord-rpc-whack #x1107) ) ;; ---text-id-h:text-id diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 548feb4bf6..e34cbdfee9 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -7,6 +7,8 @@ set(CMAKE_ASM_NASM_COMPILE_OBJECT " -f ${CMA set_source_files_properties(kernel/asm_funcs.asm PROPERTIES COMPILE_FLAGS "-g") set(RUNTIME_SOURCE external/discord.cpp + external/discord_jak1.cpp + external/discord_jak2.cpp graphics/display.cpp graphics/gfx.cpp graphics/jak2_texture_remap.cpp diff --git a/game/assets/jak2/game_text.gp b/game/assets/jak2/game_text.gp index 978f13e157..bb7f8cc2d7 100644 --- a/game/assets/jak2/game_text.gp +++ b/game/assets/jak2/game_text.gp @@ -7,6 +7,7 @@ ;; NOTE : we compile using the fixed v2 encoding because it's what we use. (file "$DECOMP/assets/game_text.txt") ;; this is the decompiler-generated file! ;; add custom files down here + (file "game/assets/jak2/text/game_custom_text_en.gs") ) diff --git a/game/assets/jak2/text/game_custom_text_en.gs b/game/assets/jak2/text/game_custom_text_en.gs new file mode 100644 index 0000000000..33dbc4d512 --- /dev/null +++ b/game/assets/jak2/text/game_custom_text_en.gs @@ -0,0 +1,43 @@ +(group-name "common") +(language-id 0 7) +(text-version jak2) + +(#x1100 + "Not in a mission" + "Not in a mission" + ) + +(#x1101 + "Playing a side mission" + "Playing a side mission" + ) + +(#x1102 + "Playing Scatter Gun Training" + "Playing Scatter Gun Training" + ) + +(#x1103 + "Playing Blaster Training" + "Playing Blaster Training" + ) + +(#x1104 + "Playing Vulcan Fury Training" + "Playing Vulcan Fury Training" + ) + +(#x1105 + "Playing Peace Maker Training" + "Playing Peace Maker Training" + ) + +(#x1106 + "Playing Onin's Game" + "Playing Onin's Game" + ) + +(#x1107 + "Playing Metal Head Mash" + "Playing Metal Head Mash" + ) \ No newline at end of file diff --git a/game/external/discord.cpp b/game/external/discord.cpp index a7e80bbece..d9cd595a08 100644 --- a/game/external/discord.cpp +++ b/game/external/discord.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "common/log/log.h" @@ -17,32 +16,6 @@ static const std::map rpc_client_ids = { {GameVersion::Jak1, "938876425585434654"}, {GameVersion::Jak2, "1060390251694149703"}}; -static const std::map jak1_level_names = { - {"intro", "Intro"}, - {"title", "Title screen"}, - {"training", "Geyser Rock"}, - {"village1", "Sandover Village"}, - {"beach", "Sentinel Beach"}, - {"jungle", "Forbidden Jungle"}, - {"misty", "Misty Island"}, - {"firecanyon", "Fire Canyon"}, - {"village2", "Rock Village"}, - {"swamp", "Boggy Swamp"}, - {"rolling", "Precursor Basin"}, - {"sunken", "Lost Precursor City"}, - {"ogre", "Mountain Pass"}, - {"village3", "Volcanic Crater"}, - {"snow", "Snowy Mountain"}, - {"maincave", "Spider Cave"}, - {"lavatube", "Lava Tube"}, - {"citadel", "Gol and Maia's Citadel"}, - {"finalboss", "Final Boss"}}; - -static const std::map jak1_level_name_remap = {{"jungleb", "jungle"}, - {"sunkenb", "sunken"}, - {"robocave", "maincave"}, - {"darkcave", "maincave"}}; - void handleDiscordReady(const DiscordUser* user) { lg::info("Discord: connected to user {}#{} - {}", user->username, user->discriminator, user->userId); @@ -82,31 +55,18 @@ void set_discord_rpc(int state) { } // get full level name from symbol name ("village1" -> "Sandover Village") -const char* jak1_get_full_level_name(const char* level_name) { +const char* get_full_level_name(const std::map& level_names, + const std::map& level_name_remap, + const char* level_name) { // ignore sublevels - auto it = jak1_level_name_remap.find(level_name); - auto actual_level_name = it == jak1_level_name_remap.end() ? level_name : it->second; + auto it = level_name_remap.find(level_name); + auto actual_level_name = it == level_name_remap.end() ? level_name : it->second; - const auto& nice_name = jak1_level_names.find(actual_level_name); - if (nice_name != jak1_level_names.end()) { + const auto& nice_name = level_names.find(actual_level_name); + if (nice_name != level_names.end()) { return nice_name->second.c_str(); } return "unknown"; -}; - -// time of day string to append to level name for icons -const char* time_of_day_str(float time) { - int hour = static_cast(time); - - if (hour >= 0 && hour <= 9) { - return "green-sun"; - } else if (hour < 22) { - return "day"; - } else if (hour < 25) { - return "evening"; - } else { - return ""; - } } // convert time of day float to a 24-hour hh:mm format string @@ -119,10 +79,6 @@ std::string get_time_of_day(float time) { } // are we in an area that's indoors, i.e. not affected by time of day? -int indoors(const char* level_name) { - return !strcmp(level_name, "intro") || !strcmp(level_name, "title") || - !strcmp(level_name, "jungleb") || !strcmp(level_name, "sunken") || - !strcmp(level_name, "sunkenb") || !strcmp(level_name, "maincave") || - !strcmp(level_name, "robocave") || !strcmp(level_name, "darkcave") || - !strcmp(level_name, "lavatube") || !strcmp(level_name, "citadel"); +bool indoors(std::vector indoor_levels, const char* level_name) { + return std::find(indoor_levels.begin(), indoor_levels.end(), level_name) != indoor_levels.end(); } diff --git a/game/external/discord.h b/game/external/discord.h index 5d9eb769db..d7db4080a6 100644 --- a/game/external/discord.h +++ b/game/external/discord.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include "common/versions/versions.h" @@ -11,10 +14,11 @@ extern int64_t gStartTime; void init_discord_rpc(); void set_discord_rpc(int state); -const char* jak1_get_full_level_name(const char* level_name); -const char* time_of_day_str(float time); std::string get_time_of_day(float time); -int indoors(const char* level_name); +const char* get_full_level_name(const std::map& level_names, + const std::map& level_name_remap, + const char* level_name); +bool indoors(std::vector indoor_levels, const char* level_name); void handleDiscordReady(const DiscordUser* user); void handleDiscordDisconnected(int errcode, const char* message); diff --git a/game/external/discord_jak1.cpp b/game/external/discord_jak1.cpp new file mode 100644 index 0000000000..77f21ac3a2 --- /dev/null +++ b/game/external/discord_jak1.cpp @@ -0,0 +1,50 @@ +#include "discord_jak1.h" + +namespace jak1 { +const std::map level_names = {{"intro", "Intro"}, + {"title", "Title screen"}, + {"training", "Geyser Rock"}, + {"village1", "Sandover Village"}, + {"beach", "Sentinel Beach"}, + {"jungle", "Forbidden Jungle"}, + {"misty", "Misty Island"}, + {"firecanyon", "Fire Canyon"}, + {"village2", "Rock Village"}, + {"swamp", "Boggy Swamp"}, + {"rolling", "Precursor Basin"}, + {"sunken", "Lost Precursor City"}, + {"ogre", "Mountain Pass"}, + {"village3", "Volcanic Crater"}, + {"snow", "Snowy Mountain"}, + {"maincave", "Spider Cave"}, + {"lavatube", "Lava Tube"}, + {"citadel", "Gol and Maia's Citadel"}, + {"finalboss", "Final Boss"}}; + +// for remapping sub-level names to the matching one in level_names +const std::map level_name_remap = {{"jungleb", "jungle"}, + {"sunkenb", "sunken"}, + {"robocave", "maincave"}, + {"darkcave", "maincave"}}; + +// levels that are not affected by time of day +const std::vector indoor_levels = { + "intro", "title", "jungleb", "sunken", "sunkenb", + "maincave", "robocave", "darkcave", "lavatube", "citadel", +}; + +// time of day string to append to level name for icons +const char* time_of_day_str(float time) { + int hour = static_cast(time); + + if (hour >= 0 && hour <= 9) { + return "green-sun"; + } else if (hour < 22) { + return "day"; + } else if (hour < 25) { + return "evening"; + } else { + return ""; + } +} +} // namespace jak1 \ No newline at end of file diff --git a/game/external/discord_jak1.h b/game/external/discord_jak1.h new file mode 100644 index 0000000000..b2e3e5f9f8 --- /dev/null +++ b/game/external/discord_jak1.h @@ -0,0 +1,10 @@ +#pragma once + +#include "discord.h" + +namespace jak1 { +extern const std::map level_names; +extern const std::map level_name_remap; +extern const std::vector indoor_levels; +const char* time_of_day_str(float time); +} // namespace jak1 diff --git a/game/external/discord_jak2.cpp b/game/external/discord_jak2.cpp new file mode 100644 index 0000000000..727a60d094 --- /dev/null +++ b/game/external/discord_jak2.cpp @@ -0,0 +1,88 @@ +#include "discord_jak2.h" + +namespace jak2 { +const std::map level_names = { + {"intro", "Intro"}, + {"title", "Title screen"}, + {"prison", "Prison"}, + {"forexit", "Fortress (Escape)"}, + {"fordump", "Fortress (Ammo Dump)"}, + {"forresc", "Fortress (Rescue Friends)"}, + {"ctyslum", "Haven City (Slums)"}, + {"ctyport", "Haven City (Port)"}, + {"ctyfarm", "Haven City (Gardens)"}, + {"ctyind", "Haven City (Industrial Zone)"}, + {"ctymark", "Haven City (Bazaar)"}, + {"ctypal", "Haven City (Palace Area)"}, + {"ctygen", "Haven City (Main Town)"}, + {"stadium", "Stadium"}, + {"stadiumb", "Stadium (Class 3 Race)"}, + {"stadiumc", "Stadium (Class 2 Race)"}, + {"stadiumd", "Stadium (Class 1 Race)"}, + {"skatea", "Stadium (JET-Board Course)"}, + {"palshaft", "Palace Lobby"}, + {"palcab", "Palace Cable"}, + {"palroof", "Palace Roof"}, + {"throne", "Throne Room"}, + {"palent", "Palace Interior"}, + {"vinroom", "Power Station"}, + {"oracle", "Oracle"}, + {"onintent", "Onin's Tent"}, + {"hiphog", "Hip Hog"}, + {"hideout", "Underground Hideout"}, + {"gungame", "Gun Course"}, + {"caspad", "Landing Pad"}, + {"castle", "Weapons Factory"}, + {"ruins", "Dead Town"}, + {"atoll", "Pumping Station"}, + {"sewer", "Sewer"}, + {"strip", "Strip Mine"}, + {"tomb", "Mar's Tomb"}, + {"dig", "Dig Site"}, + {"drill", "Drill Platform"}, + {"mountain", "Mountain Temple"}, + {"forest", "Haven Forest"}, + {"mincan", "No Man's Canyon"}, + {"consite", "Construction Site"}, + {"under", "Underport"}, + {"nest", "Metal Head Nest"}, + {"village1", "Sandover Village"}, +}; + +// for remapping sub-level names to the matching one in level_names +const std::map level_name_remap = { + {"forexita", "forexit"}, {"forexitb", "forexit"}, {"fordumpa", "fordump"}, + {"fordumpb", "fordump"}, {"fordumpc", "fordump"}, {"forresca", "forresc"}, + {"forrescb", "forresc"}, {"ctysluma", "ctyslum"}, {"ctyslumb", "ctyslum"}, + {"ctyslumc", "ctyslum"}, {"ctyfarma", "ctyfarm"}, {"ctyfarmb", "ctyfarm"}, + {"ctyinda", "ctyind"}, {"ctyindb", "ctyind"}, {"ctymarka", "ctymark"}, + {"ctymarkb", "ctymark"}, {"ctygena", "ctygen"}, {"ctygenb", "ctygen"}, + {"ctygenc", "ctygen"}, {"tomba", "tomb"}, {"tombb", "tomb"}, + {"tombboss", "tomb"}, {"tombc", "tomb"}, {"tombd", "tomb"}, + {"tombe", "tomb"}, {"dig1", "dig"}, {"dig3a", "dig"}, + {"drillmid", "drill"}, {"nestb", "nest"}, {"sewesc", "sewer"}, + {"garage", "stadium"}, {"casboss", "castle"}, {"introcst", "intro"}, + {"underb", "under"}, {"sagehut", "ruins"}, {"atollext", "atoll"}, + {"mtnext", "mountain"}, {"consiteb", "consite"}, {"drillmtn", "drill"}, + {"drillb", "drill"}, {"forestb", "forest"}, {"sewerb", "sewer"}, + {"sewescb", "sewer"}, +}; + +// levels that are not affected by time of day +const std::vector indoor_levels = { + "intro", "introcst", "title", "prison", "forexita", "forexitb", "fordumpa", "fordumpb", + "fordumpc", "forresca", "forrescb", "tomba", "tombb", "tombc", "tombd", "dig1", + "dig3a", "dig3b", "palshaft", "sewer", "sewesc", "castle", "tombe", "tombboss", + "gungame", "hideout", "vinroom", "under", "onintent", "oracle", "hiphog", "casboss"}; + +// time of day string to append to level name for icons +const char* time_of_day_str(float time) { + int hour = static_cast(time); + + if (hour > 6 && hour < 19) { + return "day"; + } else { + return "night"; + } +} +} // namespace jak2 \ No newline at end of file diff --git a/game/external/discord_jak2.h b/game/external/discord_jak2.h new file mode 100644 index 0000000000..63753ccb9e --- /dev/null +++ b/game/external/discord_jak2.h @@ -0,0 +1,10 @@ +#pragma once + +#include "discord.h" + +namespace jak2 { +extern const std::map level_names; +extern const std::map level_name_remap; +extern const std::vector indoor_levels; +const char* time_of_day_str(float time); +} // namespace jak2 diff --git a/game/kernel/jak1/kmachine.cpp b/game/kernel/jak1/kmachine.cpp index 5795f9f120..68665b50a6 100644 --- a/game/kernel/jak1/kmachine.cpp +++ b/game/kernel/jak1/kmachine.cpp @@ -15,7 +15,7 @@ #include "common/util/FileUtil.h" #include "common/util/string_util.h" -#include "game/external/discord.h" +#include "game/external/discord_jak1.h" #include "game/graphics/display.h" #include "game/graphics/gfx.h" #include "game/graphics/sceGraphicsInterface.h" @@ -440,9 +440,10 @@ void update_discord_rpc(u32 discord_info) { auto flutflut = Ptr(info->flutflut)->value; char* status = Ptr(info->status).c()->data(); char* level = Ptr(info->level).c()->data(); - const char* full_level_name = jak1_get_full_level_name(Ptr(info->level).c()->data()); + const char* full_level_name = + get_full_level_name(level_names, level_name_remap, Ptr(info->level).c()->data()); memset(&rpc, 0, sizeof(rpc)); - if (!indoors(level)) { + if (!indoors(indoor_levels, level)) { char level_with_tod[128]; strcpy(level_with_tod, level); strcat(level_with_tod, "-"); @@ -475,22 +476,16 @@ void update_discord_rpc(u32 discord_info) { strcpy(state, "Intro"); } else if (cutscene != offset_of_s7()) { strcpy(state, "Watching a cutscene"); + strcpy(large_image_text, fmt::format("Cells: {} | Orbs: {} | Flies: {} | Deaths: {}", + std::to_string(cells), std::to_string(orbs), + std::to_string(scout_flies), std::to_string(deaths)) + .c_str()); } else { - strcpy(state, "Cells: "); - strcat(state, std::to_string(cells).c_str()); - strcat(state, " | Orbs: "); - strcat(state, std::to_string(orbs).c_str()); - strcat(state, " | Flies: "); - strcat(state, std::to_string(scout_flies).c_str()); + strcpy(state, fmt::format("Cells: {} | Orbs: {} | Flies: {}", std::to_string(cells), + std::to_string(orbs), std::to_string(scout_flies)) + .c_str()); - strcat(large_image_text, " | Cells: "); - strcat(large_image_text, std::to_string(cells).c_str()); - strcat(large_image_text, " | Orbs: "); - strcat(large_image_text, std::to_string(orbs).c_str()); - strcat(large_image_text, " | Flies: "); - strcat(large_image_text, std::to_string(scout_flies).c_str()); - strcat(large_image_text, " | Deaths: "); - strcat(large_image_text, std::to_string(deaths).c_str()); + strcat(large_image_text, fmt::format(" | Deaths: {}", std::to_string(deaths)).c_str()); } rpc.largeImageText = large_image_text; rpc.state = state; @@ -501,7 +496,7 @@ void update_discord_rpc(u32 discord_info) { strcpy(small_image_key, "flutflut"); strcpy(small_image_text, "Riding on Flut Flut"); } else { - if (!indoors(level)) { + if (!indoors(indoor_levels, level)) { strcpy(small_image_key, time_of_day_str(time)); strcpy(small_image_text, "Time of day: "); strcat(small_image_text, get_time_of_day(time).c_str()); diff --git a/game/kernel/jak2/kmachine.cpp b/game/kernel/jak2/kmachine.cpp index f0b8870d41..d59d965be3 100644 --- a/game/kernel/jak2/kmachine.cpp +++ b/game/kernel/jak2/kmachine.cpp @@ -1,5 +1,6 @@ #include "kmachine.h" +#include #include #include #include @@ -7,9 +8,10 @@ #include "common/log/log.h" #include "common/symbols.h" #include "common/util/FileUtil.h" +#include "common/util/FontUtils.h" #include "common/util/string_util.h" -#include "game/external/discord.h" +#include "game/external/discord_jak2.h" #include "game/graphics/display.h" #include "game/graphics/jak2_texture_remap.h" #include "game/kernel/common/Symbol4.h" @@ -517,28 +519,47 @@ void update_discord_rpc(u32 discord_info) { auto info = discord_info ? Ptr(discord_info).c() : NULL; if (info) { // Get the data from GOAL - int orbs = (int)*Ptr(info->orb_count).c(); - int gems = (int)*Ptr(info->gem_count).c(); - char* status = Ptr(info->status).c()->data(); + int orbs = (int)info->orb_count; + int gems = (int)info->gem_count; + u32 deaths = info->death_count; + // convert encodings + std::string status = get_font_bank(GameTextVersion::JAK2) + ->convert_game_to_utf8(Ptr(info->status).c()->data()); + + // get rid of special encodings like + std::regex r("<.*?>"); + while (std::regex_search(status, r)) { + status = std::regex_replace(status, r, ""); + } + char* level = Ptr(info->level).c()->data(); auto cutscene = Ptr>(info->cutscene)->value(); - float time = *Ptr(info->time_of_day).c(); + float time = info->time_of_day; float percent_completed = info->percent_completed; + std::bitset<32> focus_status; + focus_status = info->focus_status; + char* task = Ptr(info->task).c()->data(); // Construct the DiscordRPC Object - // TODO - take nice screenshots with the various time of days once the graphics is in a final - // state const char* full_level_name = - "unknown"; // jak1_get_full_level_name(Ptr(info->level).c()->data()); + get_full_level_name(level_names, level_name_remap, Ptr(info->level).c()->data()); memset(&rpc, 0, sizeof(rpc)); - if (!indoors(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); + // 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 { - strcpy(large_image_key, level); + // 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")) { @@ -548,20 +569,42 @@ void update_discord_rpc(u32 discord_info) { 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)) + strcpy(state, fmt::format("{:.0f}% | Orbs: {} | Gems: {} | {}", percent_completed, + std::to_string(orbs), std::to_string(gems), get_time_of_day(time)) .c_str()); - strcpy(large_image_text, fmt::format(" | {:.0f}% | Orbs: {} | Gems: {}", percent_completed, - std::to_string(orbs), std::to_string(gems)) - .c_str()); } rpc.largeImageText = large_image_text; rpc.state = state; - if (!indoors(level)) { - strcpy(small_image_key, time_of_day_str(time)); - strcpy(small_image_text, "Time of day: "); - strcat(small_image_text, get_time_of_day(time).c_str()); + // 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, ""); @@ -569,7 +612,7 @@ void update_discord_rpc(u32 discord_info) { rpc.smallImageKey = small_image_key; rpc.smallImageText = small_image_text; rpc.startTimestamp = gStartTime; - rpc.details = status; + rpc.details = status.c_str(); rpc.partySize = 0; rpc.partyMax = 0; Discord_UpdatePresence(&rpc); diff --git a/game/kernel/jak2/kmachine.h b/game/kernel/jak2/kmachine.h index a8170e9981..fe5938b296 100644 --- a/game/kernel/jak2/kmachine.h +++ b/game/kernel/jak2/kmachine.h @@ -53,15 +53,55 @@ struct MouseInfo { // (speedy float :offset 108) }; +enum class FocusStatus : u32 { + Disable = 0, + Dead = 1, + Ignore = 2, + Inactive = 3, + Dangerous = 4, + InAir = 5, + Hit = 6, + Grabbed = 7, + InHead = 8, + TouchWater = 9, + OnWater = 10, + UnderWater = 11, + EdgeGrab = 12, + Pole = 13, + PilotRiding = 14, + Flut = 15, + Tube = 16, + Ice = 17, + Board = 18, + Gun = 19, + Pilot = 20, + Mech = 21, + Dark = 22, + Rail = 23, + Halfpipe = 24, + Carry = 25, + Super = 26, + Shooting = 27, + Indax = 28, + Arrestable = 29, + Teleporting = 30, + FS31 = 31, + Max = 32 +}; + +#define FOCUS_TEST(status, foc) (status.test(static_cast(foc))) + struct DiscordInfo { - u32 orb_count; // (pointer float) - u32 gem_count; // (pointer float) - u32 death_count; // (pointer int32) + float orb_count; // float + float gem_count; // float + u32 death_count; // int32 u32 status; // string u32 level; // string u32 cutscene; // symbol - bool - u32 time_of_day; // (pointer float + 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 diff --git a/goal_src/jak2/engine/game/task/task-control.gc b/goal_src/jak2/engine/game/task/task-control.gc index 379614fcd5..e507916ad9 100644 --- a/goal_src/jak2/engine/game/task/task-control.gc +++ b/goal_src/jak2/engine/game/task/task-control.gc @@ -700,6 +700,96 @@ obj ) +(defun get-active-mission-description ((info discord-info)) + "Added in PC port to retrieve a string for the currently active mission to display in Discord." + (let* ((nodes (-> *game-info* sub-task-list)) + (level (level-get-target-inside *level*)) + (task-level-idx (if level (-> level info task-level) 0)) + (side-missions-start (the int (game-task-node city-burning-bush-ring-1-introduction))) + (side-missions-end (the int (game-task-node stadium-burning-bush-race-class1-r-resolution))) + (gg-node? (lambda ((node game-task-node-info)) + (or (= (-> node task) (game-task city-red-gun-training)) + (= (-> node task) (game-task city-yellow-gun-training)) + (= (-> node task) (game-task city-blue-gun-training)) + (= (-> node task) (game-task city-dark-gun-training)))))) + (dotimes (i (-> nodes length)) + (when (nonzero? i) + (let* ((node (-> nodes i)) + (mgr-info (-> node info)) + (mgr-handle (if mgr-info (-> mgr-info manager) (the-as handle #f)))) + ;; if we are not in a valid task level, just return. + (when (= task-level-idx 0) + (return (lookup-text! *common-text* (text-id discord-rpc-not-in-mission) #f)) + ) + ;; check for onin-game + (when (and (= (-> level name) 'onintent) + (or (= (the game-task-node i) (game-task-node city-play-onin-game-post-game)) + (= (the game-task-node i) (game-task-node city-play-onin-game-skill))) + (task-node-open? (the game-task-node i)) + (process-by-name "onin-game" *active-pool*)) + (set! (-> info task) "onin-game") + (return (lookup-text! *common-text* (text-id discord-rpc-onin-game) #f)) + ) + ;; check for whack-a-metal + (when (and (= (-> level name) 'hiphog) + (= (the game-task-node i) (game-task-node city-whack-post-game)) + (task-node-open? (the game-task-node i)) + (process-by-name "whack-a-metal" *active-pool*)) + (set! (-> info task) "city-whack") + (return (lookup-text! *common-text* (text-id discord-rpc-whack) #f)) + ) + ;; check for gungame (there is probably a better way to do this) + (when (and (= (-> level name) 'gungame) + (gg-node? node) + (task-node-open? (the game-task-node i))) + (awhen (process-by-ename "training-manager-2") + (if (= (-> it next-state name) 'course) + (let ((feat (-> *setting-control* user-current features))) + (cond + ((not (logtest? feat (game-feature gun-yellow gun-blue gun-dark))) + (set! (-> info task) "city-red-gun-training") + (return (lookup-text! *common-text* (text-id discord-rpc-red-gun-training) #f)) + ) + ((not (logtest? feat (game-feature gun-red gun-blue gun-dark))) + (set! (-> info task) "city-yellow-gun-training") + (return (lookup-text! *common-text* (text-id discord-rpc-yellow-gun-training) #f)) + ) + ((not (logtest? feat (game-feature gun-red gun-yellow gun-dark))) + (set! (-> info task) "city-blue-gun-training") + (return (lookup-text! *common-text* (text-id discord-rpc-blue-gun-training) #f)) + ) + (else + (set! (-> info task) "city-dark-gun-training") + (return (lookup-text! *common-text* (text-id discord-rpc-dark-gun-training) #f)) + ) + ) + ) + ) + ) + ) + ;; check for side missions + (when (and (>= i side-missions-start) + (<= i side-missions-end) + (task-node-open? (the game-task-node i)) + mgr-handle) + (set! (-> info task) (game-task->string (-> node task))) + (return (lookup-text! *common-text* (text-id discord-rpc-in-side-mission) #f)) + ) + ;; check for any other tasks + (when (and (not (gg-node? node)) + (task-node-open? (the game-task-node i)) + (!= (-> node description) (the text-id 0)) + (= (-> *task-level* task-level-idx) (-> node level))) + (set! (-> info task) (game-task->string (-> node task))) + (return (lookup-text! *common-text* (-> node description) #f)) + ) + ) + ) + ) + (lookup-text! *common-text* (text-id discord-rpc-not-in-mission) #f) + ) + ) + (defmethod close! game-task-node-info ((obj game-task-node-info) (arg0 symbol)) (when (not (logtest? (-> obj flags) (game-task-node-flag closed))) (let ((task-node-close-func diff --git a/goal_src/jak2/engine/ui/text-id-h.gc b/goal_src/jak2/engine/ui/text-id-h.gc index 5b87c568b3..f1b30d7ba9 100644 --- a/goal_src/jak2/engine/ui/text-id-h.gc +++ b/goal_src/jak2/engine/ui/text-id-h.gc @@ -560,6 +560,16 @@ (scene-subtitles-disabled #x30e) (board-name #x30f) (progress-unknown-continue #x310) + + ;; PC port IDs START + (discord-rpc-not-in-mission #x1100) + (discord-rpc-in-side-mission #x1101) + (discord-rpc-red-gun-training #x1102) + (discord-rpc-yellow-gun-training #x1103) + (discord-rpc-blue-gun-training #x1104) + (discord-rpc-dark-gun-training #x1105) + (discord-rpc-onin-game #x1106) + (discord-rpc-whack #x1107) ) ;; ---text-id diff --git a/goal_src/jak2/pc/pckernel-impl.gc b/goal_src/jak2/pc/pckernel-impl.gc index 69d6378d64..b7c76d6397 100644 --- a/goal_src/jak2/pc/pckernel-impl.gc +++ b/goal_src/jak2/pc/pckernel-impl.gc @@ -68,18 +68,19 @@ ;; jak 2 discord rpc structure (deftype discord-info (structure) - ((orb-count (pointer float)) - (gem-count (pointer float)) - (death-count (pointer int32)) + ((orb-count float) + (gem-count float) + (death-count int32) (status string) (level string) (cutscene? symbol) - (time-of-day (pointer float)) + (time-of-day float) (percent-complete float) + (focus-status uint32) + (task string) ;; currenly active game-task used for mission specific images ) ) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; resets ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/goal_src/jak2/pc/pckernel.gc b/goal_src/jak2/pc/pckernel.gc index cd31a75949..42cca43e98 100644 --- a/goal_src/jak2/pc/pckernel.gc +++ b/goal_src/jak2/pc/pckernel.gc @@ -8,6 +8,7 @@ |# +(define-extern get-active-mission-description (function discord-info string)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; methods @@ -62,19 +63,29 @@ (defmethod update-discord-rpc pc-settings-jak2 ((obj pc-settings-jak2)) "update discord rpc module" (let ((info (new 'stack 'discord-info))) - (set! (-> info orb-count) (&-> *game-info* skill-total)) - (set! (-> info gem-count) (&-> *game-info* gem-total)) - (set! (-> info death-count) (&-> *game-info* total-deaths)) - (set! (-> info status) "Playing Jak II™") + (set! (-> info orb-count) (-> *game-info* skill-total)) + (set! (-> info gem-count) (-> *game-info* gem-total)) + (set! (-> info death-count) (-> *game-info* total-deaths)) + (set! (-> info task) "unknown") + (set! (-> info status) (get-active-mission-description info)) ;; grab the name of the level we're in - (if (and *level* (level-get-target-inside *level*)) - (set! (-> info level) (symbol->string (-> (level-get-target-inside *level*) name))) - (set! (-> info level) "unknown")) - (set! (-> info cutscene?) #f) ;; TODO - cutscenes don't work yet - (set! (-> info time-of-day) (&-> *time-of-day-context* time)) + (cond + ((or (aif (level-get *level* 'title) (= (-> it status) 'active)) + (and *progress-process* + (= 'title (-> *progress-process* 0 state-stack 0)))) + ;; in title screen. + (set! (-> info level) (symbol->string 'title)) + (set! (-> info status) "In title screen")) + (else + (set! (-> info level) (aif (-> *load-state* vis-nick) (symbol->string it) "unknown"))) + ) + (set! (-> info cutscene?) (-> *pc-settings* movie?)) + (set! (-> info time-of-day) (-> *time-of-day-context* time)) (set! (-> info percent-complete) (calculate-percentage *game-info*)) + (set! (-> info focus-status) (if *target* (-> *target* focus-status) 0)) ;; TODO - wrapping in `with-profiler` causes an error, fix it - (pc-discord-rpc-update info)) + (pc-discord-rpc-update info) + ) (none)) (defmethod update-speedrun pc-settings-jak2 ((obj pc-settings-jak2))