game: refactor discord code and improve jak 2 support (#2714)

The Discord RPC code has been cleaned up and split up between game
versions.

For Jak 2, the Discord integration now shows large images for all levels
(with corresponding day and nighttime variants if required) and small
images for time of day and various states like being on the jetboard,
driving a zoomer, playing as Daxter, etc.

TODO:
- [x] mission specific images and detection
- [x] detect side missions properly
- [x] `onin-game` detection


![image](https://github.com/open-goal/jak-project/assets/6624576/35ec273f-404c-4475-a7c7-06121a17b1a5)

![image](https://github.com/open-goal/jak-project/assets/6624576/8456f5e2-4f96-4c72-ae9e-d930d76c93af)

![image](https://github.com/open-goal/jak-project/assets/6624576/9a17a0ec-c9bd-40fa-8556-f139712d8f07)

---------

Co-authored-by: ManDude <7569514+ManDude@users.noreply.github.com>
This commit is contained in:
Hat Kid
2023-06-17 06:16:40 +02:00
committed by GitHub
parent 2624934e72
commit d8cca2bf83
18 changed files with 482 additions and 122 deletions
-4
View File
@@ -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) {
+10
View File
@@ -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
+2
View File
@@ -7,6 +7,8 @@ set(CMAKE_ASM_NASM_COMPILE_OBJECT "<CMAKE_ASM_NASM_COMPILER> <INCLUDES> -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
+1
View File
@@ -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")
)
@@ -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"
)
+9 -53
View File
@@ -4,7 +4,6 @@
#include <iomanip>
#include <map>
#include <sstream>
#include <string>
#include "common/log/log.h"
@@ -17,32 +16,6 @@ static const std::map<GameVersion, std::string> rpc_client_ids = {
{GameVersion::Jak1, "938876425585434654"},
{GameVersion::Jak2, "1060390251694149703"}};
static const std::map<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string>& level_names,
const std::map<std::string, std::string>& 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<int>(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<std::string> indoor_levels, const char* level_name) {
return std::find(indoor_levels.begin(), indoor_levels.end(), level_name) != indoor_levels.end();
}
+7 -3
View File
@@ -1,5 +1,8 @@
#pragma once
#include <algorithm>
#include <cstring>
#include <map>
#include <string>
#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<std::string, std::string>& level_names,
const std::map<std::string, std::string>& level_name_remap,
const char* level_name);
bool indoors(std::vector<std::string> indoor_levels, const char* level_name);
void handleDiscordReady(const DiscordUser* user);
void handleDiscordDisconnected(int errcode, const char* message);
+50
View File
@@ -0,0 +1,50 @@
#include "discord_jak1.h"
namespace jak1 {
const std::map<std::string, std::string> 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<std::string, std::string> level_name_remap = {{"jungleb", "jungle"},
{"sunkenb", "sunken"},
{"robocave", "maincave"},
{"darkcave", "maincave"}};
// levels that are not affected by time of day
const std::vector<std::string> 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<int>(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
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "discord.h"
namespace jak1 {
extern const std::map<std::string, std::string> level_names;
extern const std::map<std::string, std::string> level_name_remap;
extern const std::vector<std::string> indoor_levels;
const char* time_of_day_str(float time);
} // namespace jak1
+88
View File
@@ -0,0 +1,88 @@
#include "discord_jak2.h"
namespace jak2 {
const std::map<std::string, std::string> 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<std::string, std::string> 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<std::string> 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<int>(time);
if (hour > 6 && hour < 19) {
return "day";
} else {
return "night";
}
}
} // namespace jak2
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "discord.h"
namespace jak2 {
extern const std::map<std::string, std::string> level_names;
extern const std::map<std::string, std::string> level_name_remap;
extern const std::vector<std::string> indoor_levels;
const char* time_of_day_str(float time);
} // namespace jak2
+13 -18
View File
@@ -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<Symbol>(info->flutflut)->value;
char* status = Ptr<String>(info->status).c()->data();
char* level = Ptr<String>(info->level).c()->data();
const char* full_level_name = jak1_get_full_level_name(Ptr<String>(info->level).c()->data());
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 (!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());
+68 -25
View File
@@ -1,5 +1,6 @@
#include "kmachine.h"
#include <bitset>
#include <cstring>
#include <stdexcept>
#include <string>
@@ -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<DiscordInfo>(discord_info).c() : NULL;
if (info) {
// Get the data from GOAL
int orbs = (int)*Ptr<float>(info->orb_count).c();
int gems = (int)*Ptr<float>(info->gem_count).c();
char* status = Ptr<String>(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<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 = *Ptr<float>(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<String>(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<String>(info->level).c()->data());
get_full_level_name(level_names, level_name_remap, Ptr<String>(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);
+44 -4
View File
@@ -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<size_t>(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
@@ -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
+10
View File
@@ -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
+6 -5
View File
@@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+21 -10
View File
@@ -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))