diff --git a/game/discord.cpp b/game/discord.cpp index f1aeba908..a7e80bbec 100644 --- a/game/discord.cpp +++ b/game/discord.cpp @@ -6,9 +6,17 @@ #include #include +#include "common/log/log.h" + +#include "game/runtime.h" + int gDiscordRpcEnabled; int64_t gStartTime; -static const char* APPLICATION_ID = "938876425585434654"; + +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"}, @@ -29,12 +37,34 @@ static const std::map jak1_level_names = { {"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); +} + +void handleDiscordDisconnected(int errcode, const char* message) { + lg::info("Discord: disconnected ({}: {})", errcode, message); +} + +void handleDiscordError(int errcode, const char* message) { + lg::info("Discord: error ({}: {})", errcode, message); +} + +void handleDiscordJoin(const char* /*secret*/) {} +void handleDiscordJoinRequest(const DiscordUser* /*request*/) {} +void handleDiscordSpectate(const char* /*secret*/) {} + void init_discord_rpc() { + if (g_game_version != GameVersion::Jak1 && g_game_version != GameVersion::Jak2) { + lg::error("Game version unsupported for Discord RPC - {}", fmt::underlying(g_game_version)); + return; + } gDiscordRpcEnabled = 1; DiscordEventHandlers handlers; memset(&handlers, 0, sizeof(handlers)); @@ -44,7 +74,7 @@ void init_discord_rpc() { handlers.joinGame = handleDiscordJoin; handlers.joinRequest = handleDiscordJoinRequest; handlers.spectateGame = handleDiscordSpectate; - Discord_Initialize(APPLICATION_ID, &handlers, 1, NULL); + Discord_Initialize(rpc_client_ids.at(g_game_version).c_str(), &handlers, 1, NULL); } void set_discord_rpc(int state) { @@ -68,14 +98,15 @@ const char* jak1_get_full_level_name(const char* level_name) { const char* time_of_day_str(float time) { int hour = static_cast(time); - if (hour >= 0 && hour <= 9) + if (hour >= 0 && hour <= 9) { return "green-sun"; - else if (hour < 22) + } else if (hour < 22) { return "day"; - else if (hour < 25) + } else if (hour < 25) { return "evening"; - else + } else { return ""; + } } // convert time of day float to a 24-hour hh:mm format string @@ -95,20 +126,3 @@ int indoors(const char* level_name) { !strcmp(level_name, "robocave") || !strcmp(level_name, "darkcave") || !strcmp(level_name, "lavatube") || !strcmp(level_name, "citadel"); } - -void handleDiscordReady(const DiscordUser* user) { - printf("\nDiscord: connected to user %s#%s - %s\n", user->username, user->discriminator, - user->userId); -} - -void handleDiscordDisconnected(int errcode, const char* message) { - printf("\nDiscord: disconnected (%d: %s)\n", errcode, message); -} - -void handleDiscordError(int errcode, const char* message) { - printf("\nDiscord: error (%d: %s)\n", errcode, message); -} - -void handleDiscordJoin(const char* /*secret*/) {} -void handleDiscordJoinRequest(const DiscordUser* /*request*/) {} -void handleDiscordSpectate(const char* /*secret*/) {} diff --git a/game/discord.h b/game/discord.h index 4a5498a92..f694d2826 100644 --- a/game/discord.h +++ b/game/discord.h @@ -2,8 +2,13 @@ #include +#include "common/versions.h" + #include "third-party/discord-rpc/include/discord_rpc.h" +extern int gDiscordRpcEnabled; +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); diff --git a/game/kernel/jak1/kmachine.h b/game/kernel/jak1/kmachine.h index c23de1a8c..6e6c4152a 100644 --- a/game/kernel/jak1/kmachine.h +++ b/game/kernel/jak1/kmachine.h @@ -3,8 +3,6 @@ #include "common/common_types.h" // Discord RPC struct DiscordRichPresence; -extern int gDiscordRpcEnabled; -extern int64_t gStartTime; namespace jak1 { /*! * Initialize global variables based on command line parameters diff --git a/game/kernel/jak2/kmachine.cpp b/game/kernel/jak2/kmachine.cpp index b1e4189db..e4cf9e8eb 100644 --- a/game/kernel/jak2/kmachine.cpp +++ b/game/kernel/jak2/kmachine.cpp @@ -8,6 +8,7 @@ #include "common/symbols.h" #include "common/util/FileUtil.h" +#include "game/discord.h" #include "game/kernel/common/Symbol4.h" #include "game/kernel/common/fileio.h" #include "game/kernel/common/kboot.h" @@ -502,6 +503,79 @@ void pc_set_levels(u32 lev_list) { Gfx::set_levels(levels); } +void update_discord_rpc(u32 discord_info) { + if (gDiscordRpcEnabled) { + DiscordRichPresence rpc; + char state[128]; + char large_image_key[128]; + char large_image_text[128]; + char small_image_key[128]; + char small_image_text[128]; + auto info = discord_info ? Ptr(discord_info).c() : NULL; + if (info) { + // Get the data from GOAL + int orbs = (int)*Ptr(info->orb_count).c(); + int gems = (int)*Ptr(info->gem_count).c(); + char* status = Ptr(info->status).c()->data(); + char* level = Ptr(info->level).c()->data(); + auto cutscene = Ptr>(info->cutscene)->value(); + float time = *Ptr(info->time_of_day).c(); + float percent_completed = info->percent_completed; + + // 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()); + 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); + } 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"); + } else { + strcpy(state, fmt::format("{:.0f}% | Orbs: {} | Gems: {}", percent_completed, + std::to_string(orbs), std::to_string(gems)) + .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()); + } 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; + rpc.partySize = 0; + rpc.partyMax = 0; + Discord_UpdatePresence(&rpc); + } + } else { + Discord_ClearPresence(); + } +} + void InitMachine_PCPort() { // PC Port added functions @@ -550,8 +624,8 @@ void InitMachine_PCPort() { make_function_symbol_from_c("pc-mkdir-file-path", (void*)mkdir_path); // discord rich presence - // make_function_symbol_from_c("pc-discord-rpc-set", (void*)set_discord_rpc); - // make_function_symbol_from_c("pc-discord-rpc-update", (void*)update_discord_rpc); + make_function_symbol_from_c("pc-discord-rpc-set", (void*)set_discord_rpc); + make_function_symbol_from_c("pc-discord-rpc-update", (void*)update_discord_rpc); // profiler make_function_symbol_from_c("pc-prof", (void*)prof_event); diff --git a/game/kernel/jak2/kmachine.h b/game/kernel/jak2/kmachine.h index abfd87e93..5ae15502a 100644 --- a/game/kernel/jak2/kmachine.h +++ b/game/kernel/jak2/kmachine.h @@ -52,4 +52,16 @@ struct MouseInfo { // (speedx float :offset 92) // (speedy float :offset 108) }; + +struct DiscordInfo { + u32 orb_count; // (pointer float) + u32 gem_count; // (pointer float) + u32 death_count; // (pointer int32) + u32 status; // string + u32 level; // string + u32 cutscene; // symbol - bool + u32 time_of_day; // (pointer float + float percent_completed; // float +}; + } // namespace jak2 diff --git a/game/main.cpp b/game/main.cpp index f01efae6c..d84ea9e2d 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -15,8 +15,6 @@ #include "common/util/unicode_util.h" #include "common/versions.h" -#include "game/discord.h" - #ifdef _WIN32 extern "C" { __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; @@ -24,9 +22,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } #endif -// Discord RPC -extern int64_t gStartTime; - /*! * Set up logging system to log to file. * @param verbose : should we print debug-level messages to stdout? @@ -87,10 +82,6 @@ int main(int argc, char** argv) { return 1; } - // set up discord stuff - gStartTime = time(nullptr); - init_discord_rpc(); - if (disable_avx2) { // for debugging the non-avx2 code paths, there's a flag to manually disable. printf("Note: AVX2 code has been manually disabled.\n"); diff --git a/game/runtime.cpp b/game/runtime.cpp index ca3df3f57..13e550021 100644 --- a/game/runtime.cpp +++ b/game/runtime.cpp @@ -29,6 +29,7 @@ #include "common/util/FileUtil.h" #include "common/versions.h" +#include "game/discord.h" #include "game/graphics/gfx.h" #include "game/kernel/common/fileio.h" #include "game/kernel/common/kdgo.h" @@ -329,6 +330,10 @@ RuntimeExitStatus exec_runtime(int argc, char** argv) { } } + // set up discord stuff + gStartTime = time(nullptr); + init_discord_rpc(); + // initialize graphics first - the EE code will upload textures during boot and we // want the graphics system to catch them. if (enable_display) { @@ -385,5 +390,6 @@ RuntimeExitStatus exec_runtime(int argc, char** argv) { } lg::info("GOAL Runtime Shutdown (code {})", fmt::underlying(MasterExit)); munmap(g_ee_main_mem, EE_MAIN_MEM_SIZE); + Discord_Shutdown(); return MasterExit; } diff --git a/goal_src/jak2/dgos/engine.gd b/goal_src/jak2/dgos/engine.gd index e4b696d0a..5483d6e16 100644 --- a/goal_src/jak2/dgos/engine.gd +++ b/goal_src/jak2/dgos/engine.gd @@ -30,6 +30,7 @@ ("dma-bucket.o" "dma-bucket") ("dma-disasm.o" "dma-disasm") ("pad.o" "pad") + ("pckernel-h.o" "pckernel-h") ;; added ("gs.o" "gs") ("display-h.o" "display-h") ("geometry.o" "geometry") @@ -231,6 +232,7 @@ ("game-task.o" "game-task") ("game-save.o" "game-save") ("settings.o" "settings") + ("pckernel.o" "pckernel") ;; added ("mood-tables.o" "mood-tables") ("mood-tables2.o" "mood-tables2") ("mood.o" "mood") diff --git a/goal_src/jak2/dgos/game.gd b/goal_src/jak2/dgos/game.gd index 54f77fe1b..18c2a2441 100644 --- a/goal_src/jak2/dgos/game.gd +++ b/goal_src/jak2/dgos/game.gd @@ -29,6 +29,7 @@ ("dma-buffer.o" "dma-buffer") ("dma-bucket.o" "dma-bucket") ("dma-disasm.o" "dma-disasm") + ("pckernel-h.o" "pckernel-h") ;; added ("pad.o" "pad") ("gs.o" "gs") ("display-h.o" "display-h") @@ -231,6 +232,7 @@ ("game-task.o" "game-task") ("game-save.o" "game-save") ("settings.o" "settings") + ("pckernel.o" "pckernel") ;; added ("mood-tables.o" "mood-tables") ("mood-tables2.o" "mood-tables2") ("mood.o" "mood") diff --git a/goal_src/jak2/engine/debug/default-menu.gc b/goal_src/jak2/engine/debug/default-menu.gc index c55f38a63..8e88c0a6f 100644 --- a/goal_src/jak2/engine/debug/default-menu.gc +++ b/goal_src/jak2/engine/debug/default-menu.gc @@ -32,6 +32,15 @@ ;; og:ignore-form:all-texture-tweak-adjust +(defmacro dm-lambda-boolean-flag (val) + "helper macro for making boolean buttons that don't just access symbols directly" + `,(lambda (arg (msg debug-menu-msg)) + (if (= msg (debug-menu-msg press)) + (not! ,val) + ) + ,val) + ) + ;; DECOMP BEGINS ;; this file is debug only @@ -5890,6 +5899,17 @@ ) (debug-menu-append-item s5-0 (debug-menu-make-task-menu arg0)) (debug-menu-append-item s5-0 (debug-menu-make-play-menu arg0)) + (debug-menu-append-item + s5-0 + (debug-menu-make-from-template + arg0 + '(menu + "PC Settings" + (flag "Discord RPC" #t ,(dm-lambda-boolean-flag (-> *pc-settings* discord-rpc?))) + (function "Save" #f ,(lambda () (commit-to-file *pc-settings*))) + ) + ) + ) ) arg0 ) diff --git a/goal_src/jak2/engine/game/main.gc b/goal_src/jak2/engine/game/main.gc index b3ef7c9f4..98c92761f 100644 --- a/goal_src/jak2/engine/game/main.gc +++ b/goal_src/jak2/engine/game/main.gc @@ -1657,6 +1657,8 @@ (with-profiler 'actors *profile-actors-color* (suspend) ) + (#when PC_PORT + (update *pc-settings*)) ) ) (set! *dproc* #f) diff --git a/goal_src/jak2/engine/load/file-io.gc b/goal_src/jak2/engine/load/file-io.gc index a3b80b749..b2984c31e 100644 --- a/goal_src/jak2/engine/load/file-io.gc +++ b/goal_src/jak2/engine/load/file-io.gc @@ -36,6 +36,18 @@ ) ) +(defconstant SCE_SEEK_SET 0) +(defconstant SCE_SEEK_CUR 1) +(defconstant SCE_SEEK_END 2) + +(defmacro file-stream-valid? (fs) + `(>= (the-as int (-> ,fs file)) 0) + ) + +(defmacro file-stream-tell (fs) + `(file-stream-seek ,fs 0 SCE_SEEK_CUR) + ) + (defmethod new file-stream ((allocation symbol) (type-to-make type) (arg0 string) (arg1 symbol)) "Allocate a file-stream and open it." (let ((a0-1 (object-new allocation type-to-make (the-as int (-> type-to-make size))))) diff --git a/goal_src/jak2/game.gp b/goal_src/jak2/game.gp index aa0254b6b..00196f2d0 100644 --- a/goal_src/jak2/game.gp +++ b/goal_src/jak2/game.gp @@ -251,7 +251,17 @@ "dma/dma-buffer.gc" "dma/dma-bucket.gc" "dma/dma-disasm.gc" - "ps2/pad.gc" + ) + +(goal-src "engine/ps2/pad.gc" "pckernel-h") + +(goal-src-sequence + ;; prefix + "engine/" + + :deps + ("$OUT/obj/pad.o" + "$OUT/obj/dma-disasm.o") "gfx/hw/gs.gc" "gfx/hw/display-h.gc" "geometry/geometry.gc" @@ -557,7 +567,18 @@ "gfx/background/prototype.gc" "collide/main-collide.gc" "gfx/hw/video.gc" - "game/main.gc" + ) + +(goal-src "engine/game/main.gc" "pckernel" "video") + +(goal-src-sequence + ;; prefix + "engine/" + + :deps + ("$OUT/obj/main.o" + "$OUT/obj/video.o") + "collide/collide-cache.gc" "collide/collide-debug.gc" "entity/relocate.gc" @@ -4653,6 +4674,10 @@ `("$OUT/iso/0COMMON.TXT") ) +;; Custom or Modified Code +(goal-src "pc/pckernel-h.gc" "dma-buffer") +(goal-src "pc/pckernel.gc" "video") + ;; used for the type consistency test. (group-list "all-code" `(,@(reverse *all-gc*)) diff --git a/goal_src/jak2/kernel-defs.gc b/goal_src/jak2/kernel-defs.gc index 80710ecd3..4dbeafd7e 100644 --- a/goal_src/jak2/kernel-defs.gc +++ b/goal_src/jak2/kernel-defs.gc @@ -218,4 +218,12 @@ ) (define-extern pc-prof (function string pc-prof-event none)) +(define-extern *pc-settings-folder* string) +(define-extern *pc-settings-built-sha* string) + +(define-extern pc-filepath-exists? (function string symbol)) +(define-extern pc-mkdir-file-path (function string none)) (define-extern pc-filter-debug-string? (function string float symbol)) +(declare-type discord-info structure) +(define-extern pc-discord-rpc-update (function discord-info none)) +(define-extern pc-discord-rpc-set (function int none)) diff --git a/goal_src/jak2/kernel/gstring.gc b/goal_src/jak2/kernel/gstring.gc index f96a30a35..71b9a8a82 100644 --- a/goal_src/jak2/kernel/gstring.gc +++ b/goal_src/jak2/kernel/gstring.gc @@ -458,6 +458,21 @@ (cat-string<-string_to_charp arg0 arg1 s4-0) ) ) +(defmacro is-whitespace-char? (c) + ;; 32 = space + ;; 9 = \t + ;; 13 = \r + ;; 10 = \n + `(or (= ,c 32) + (= ,c 9) + (= ,c 13) + (= ,c 10) + ) + ) + +(defmacro not-whitespace-char? (c) + `(not (is-whitespace-char? ,c)) + ) (defun string-skip-whitespace ((arg0 (pointer uint8))) "Skip over spaces, tabs, r's and n's" @@ -784,6 +799,16 @@ ;; up from 256 bytes in jak 1 (define *temp-string* (new 'global 'string 2048 (the-as string #f))) +(defmacro string-format (&rest args) + "Formats into *temp-string* and returns it, for in-place string formating. + DO NOT USE *temp-string* WITH THIS MACRO! It is read as input AFTER all of the args evaluate." + + `(begin + (format (clear *temp-string*) ,@args) + *temp-string* + ) + ) + (kmemclose) diff --git a/goal_src/jak2/pc/pckernel-h.gc b/goal_src/jak2/pc/pckernel-h.gc new file mode 100644 index 000000000..97e528a70 --- /dev/null +++ b/goal_src/jak2/pc/pckernel-h.gc @@ -0,0 +1,100 @@ +;;-*-Lisp-*- +(in-package goal) + +#| + + This file contains code that we need for the PC port of the game specifically. + It should be included as part of the game engine package (engine.cgo). + + This file contains various types and functions to store PC-specific information + and also to communicate between the game (GOAL) and the operating system. + This way we can poll, change and display information about the system the game + is running on, such as: + - display devices and their settings, such as fullscreen, DPI, refresh rate, etc. + - audio devices and their settings, such as audio latency, channel number, etc. + - graphics devices and their settings, such as resolution, FPS, anisotropy, shaders, etc. + - input devices and their settings, such as controllers, keyboards, mice, etc. + - information about the game window (position, size) + - PC-specific goodies, enhancements, fixes and settings. + - whatever else. + + If you do not want to include these PC things, you should exclude it from the build system. + + |# + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; constants +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(deftype pckernel-version (int64) + ((build int16 :offset 0) + (revision int16 :offset 16) + (minor int16 :offset 32) + (major int16 :offset 48) + ) + ) + +(defmacro static-pckernel-version (major minor rev build) + `(new 'static 'pckernel-version :major ,major :minor ,minor :revision ,rev :build ,build)) + ;; version: 0.0.0.1 +(defglobalconstant PC_KERNEL_VERSION (static-pckernel-version 0 0 0 1)) +(defconstant PC_KERNEL_VER_MAJOR (-> PC_KERNEL_VERSION major)) +(defconstant PC_KERNEL_VER_MINOR (-> PC_KERNEL_VERSION minor)) + + +;; All of the configuration for the PC port in GOAL. Access things from here! +;; Includes some methods to change parameters. +(deftype pc-settings (basic) + ((version pckernel-version) ;; version of this settings + + (discord-rpc? symbol) ;; enable discord rich presence integration + ) + + (:methods + (new (symbol type) _type_) + (update (_type_) none) + (update-to-os (_type_) none) + (reset (_type_) none) + (read-from-file (_type_ string) symbol) + (load-settings (_type_) int) + (write-to-file (_type_ string) symbol) + (commit-to-file (_type_) none) + ) + ) + +(deftype discord-info (structure) + ((orb-count (pointer float)) + (gem-count (pointer float)) + (death-count (pointer int32)) + (status string) + (level string) + (cutscene? symbol) + (time-of-day (pointer float)) + (percent-complete float))) + +(defconstant PC_TEMP_STRING_LEN 512) +(define *pc-temp-string* (new 'global 'string PC_TEMP_STRING_LEN (the string #f))) +(define *pc-settings* (the pc-settings #f)) +(format 0 "PC kernel version: ~D.~D~%" PC_KERNEL_VER_MAJOR PC_KERNEL_VER_MINOR) + +(define *pc-temp-string-1* (new 'global 'string 2048 (the string #f))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; resets +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defmethod reset pc-settings ((obj pc-settings)) + "Set the default settings" + (set! (-> obj version) PC_KERNEL_VERSION) + (set! (-> obj discord-rpc?) #t) + (none)) + +(defmacro with-pc (&rest body) + "encapsulates the code around PC-specific checks" + `(#when PC_PORT (when (and *pc-settings*) + ,@body + )) + ) diff --git a/goal_src/jak2/pc/pckernel.gc b/goal_src/jak2/pc/pckernel.gc new file mode 100644 index 000000000..20bb633af --- /dev/null +++ b/goal_src/jak2/pc/pckernel.gc @@ -0,0 +1,318 @@ +;;-*-Lisp-*- +(in-package goal) + +#| + + This file contains new code that we need for the PC port of the game specifically. + It should be included as part of the game engine package (engine.cgo). + + This file contains various types and functions to store PC-specific information + and also to communicate between the game (GOAL) and the operating system. + This way we can poll, change and display information about the system the game + is running on, such as: + - display devices and their settings, such as fullscreen, DPI, refresh rate, etc. + - audio devices and their settings, such as audio latency, channel number, etc. + - graphics devices and their settings, such as resolution, FPS, anisotropy, shaders, etc. + - input devices and their settings, such as controllers, keyboards, mice, etc. + - information about the game window (position, size) + - PC-specific goodies, enhancements, fixes and settings. + - whatever else. + + If you do not want to include these PC things, you should exclude it from the build system. + + |# + + +(defmethod update-to-os pc-settings ((obj pc-settings)) + "Update settings from GOAL to the C kernel." + (pc-discord-rpc-set (if (-> obj discord-rpc?) 1 0)) + (none)) + +(defmethod update pc-settings ((obj pc-settings)) + "Update settings to/from PC kernel. Call this at the start of every frame. + This will update things like the aspect-ratio, which will be used for graphics code later." + + (update-to-os obj) + + (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 2™") + ;; 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)) + (set! (-> info percent-complete) (calculate-percentage *game-info*)) + + ;; TODO - wrapping in `with-profiler` causes an error, fix it + (pc-discord-rpc-update info) + ) + + (none)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; file IO +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + +(defmacro file-stream-seek-until (fs func-name) + `(let ((done? #f) + (tell -1)) + + (until done? + + (let ((read (file-stream-read ,fs (-> *pc-temp-string* data) PC_TEMP_STRING_LEN))) + (cond + ((zero? read) + (set! (-> *pc-temp-string* data read) 0) + (true! done?) + ) + (else + (dotimes (i read) + (when (,func-name (-> *pc-temp-string* data i)) + (true! done?) + (set! tell (+ i (- (file-stream-tell ,fs) read))) + (set! i read) + ) + ) + ) + ) + + + ) + + ) + (if (!= tell -1) + (file-stream-seek ,fs tell SCE_SEEK_SET) + tell + ) + ) + ) + +(defmacro file-stream-read-until (fs func-name) + `(let ((read (file-stream-read ,fs (-> *pc-temp-string* data) PC_TEMP_STRING_LEN))) + (dotimes (i read) + (when (,func-name (-> *pc-temp-string* data i)) + (set! (-> *pc-temp-string* data i) 0) + (file-stream-seek ,fs (+ i (- (file-stream-tell ,fs) read)) SCE_SEEK_SET) + (set! i read) + ) + ) + *pc-temp-string* + ) + ) + +(defmacro is-whitespace-or-bracket? (c) + `(or (is-whitespace-char? ,c) (= #x28 ,c) (= #x29 ,c)) + ) + +(defun file-stream-seek-past-whitespace ((file file-stream)) + (file-stream-seek-until file not-whitespace-char?) + ) + +(defun file-stream-read-word ((file file-stream)) + (file-stream-read-until file is-whitespace-or-bracket?) + ;(format 0 "word ~A~%" *pc-temp-string*) + ) + +(defmacro file-stream-getc (fs) + `(let ((buf 255)) + (file-stream-read ,fs (& buf) 1) + ;(format 0 "getc got #x~X~%" buf) + buf + ) + ) + +(defun file-stream-read-int ((file file-stream)) + (file-stream-seek-past-whitespace file) + (file-stream-read-word file) + (string->int *pc-temp-string*) + ) + +(defun file-stream-read-float ((file file-stream)) + (file-stream-seek-past-whitespace file) + (file-stream-read-word file) + (string->float *pc-temp-string*) + ) + +(defun file-stream-read-symbol ((file file-stream)) + (file-stream-seek-past-whitespace file) + (file-stream-read-word file) + (string->symbol *pc-temp-string*) + ) + +(defmacro pc-settings-read-throw-error (fs msg) + "not an actual throw..." + `(begin + (format 0 "pc settings read error: ~S~%" ,msg) + (file-stream-close ,fs) + (return #f) + ) + ) + +(defmacro with-settings-scope (bindings &rest body) + (let ((fs (first bindings))) + `(begin + (file-stream-seek-past-whitespace ,fs) + (when (!= #x28 (file-stream-getc ,fs)) + (pc-settings-read-throw-error ,fs "invalid char, ( not found") + ) + + ,@body + + (file-stream-seek-past-whitespace ,fs) + (when (!= #x29 (file-stream-getc ,fs)) + (pc-settings-read-throw-error ,fs "invalid char, ) not found") + ) + ) + ) + ) + +(defmacro file-stream-get-next-char-ret (fs) + `(begin + (file-stream-seek-past-whitespace ,fs) + (let ((c (file-stream-getc ,fs))) + (file-stream-seek ,fs -1 SCE_SEEK_CUR) + c)) + ) + +(defmacro file-stream-get-next-char (fs) + `(begin + (file-stream-seek-past-whitespace ,fs) + (file-stream-getc ,fs) + ) + ) + +(defmacro dosettings (bindings &rest body) + "iterate over a list of key-value pairs like so: ( ) ( ) ... + the name of key is stored in *pc-temp-string*" + (let ((fs (first bindings))) + `(let ((c -1)) + (while (begin (file-stream-seek-past-whitespace ,fs) (set! c (file-stream-getc ,fs)) (= #x28 c)) + (file-stream-read-word ,fs) + + ,@body + + (set! c (file-stream-get-next-char ,fs)) + (when (!= #x29 c) + (pc-settings-read-throw-error ,fs (string-format "invalid char, ) not found, got #x~X ~A" c *pc-temp-string*)) + ) + ) + (file-stream-seek ,fs -1 SCE_SEEK_CUR) + ) + ) + ) + +(defmethod read-from-file pc-settings ((obj pc-settings) (filename string)) + "read settings from a file" + + (if (not filename) + (return #f)) + + (let ((file (new 'stack 'file-stream filename 'read))) + (when (not (file-stream-valid? file)) + (return #f)) + + (let ((version PC_KERNEL_VERSION)) + (with-settings-scope (file) + (case-str (file-stream-read-word file) + (("settings") + (set! version (the pckernel-version (file-stream-read-int file))) + (cond + ((and (= (-> version major) PC_KERNEL_VER_MAJOR) + (= (-> version minor) PC_KERNEL_VER_MINOR)) + ;; minor difference + ) + (else + ;; major difference + (format 0 "PC kernel version mismatch! Got ~D.~D vs ~D.~D~%" PC_KERNEL_VER_MAJOR PC_KERNEL_VER_MINOR (-> version major) (-> version minor)) + (file-stream-close file) + (return #f) + ) + ) + (dosettings (file) + (case-str *pc-temp-string* + (("discord-rpc?") (set! (-> obj discord-rpc?) (file-stream-read-symbol file))) + ) + ) + ) + ) + ) + + ) + + (file-stream-close file) + ) + + (format 0 "pc settings file read: ~A~%" filename) + + #t + ) + +(defmethod write-to-file pc-settings ((obj pc-settings) (filename string)) + "write settings to a file" + + (if (not filename) + (return #f)) + + (let ((file (new 'stack 'file-stream filename 'write))) + (if (not (file-stream-valid? file)) + (return #f)) + + (format file "(settings #x~X~%" (-> obj version)) + (format file " (discord-rpc? ~A)~%" (-> obj discord-rpc?)) + (format file " )~%") + (file-stream-close file) + ) + + (format 0 "pc settings file write: ~A~%" filename) + + #t + ) + +(defmethod commit-to-file pc-settings ((obj pc-settings)) + "commits the current settings to the file" + ;; auto load settings if available + + (format (clear *pc-temp-string-1*) "~S/pc-settings.gc" *pc-settings-folder*) + (pc-mkdir-file-path *pc-temp-string-1*) + ;; symbol -> string in C++ nyi for jak2 symbols + ;; (write-to-file obj *pc-temp-string-1*) + (none)) + +(defmethod load-settings pc-settings ((obj pc-settings)) + "load" + + (format (clear *pc-temp-string-1*) "~S/pc-settings.gc" *pc-settings-folder*) + (if (pc-filepath-exists? *pc-temp-string-1*) + (begin + (format 0 "[PC] PC Settings found at '~S'...loading!~%" *pc-temp-string-1*) + (unless (read-from-file obj *pc-temp-string-1*) + (format 0 "[PC] PC Settings found at '~S' but could not be loaded, using defaults!~%" *pc-temp-string-1*) + (reset obj))) + (format 0 "[PC] PC Settings not found at '~S'...initializing with defaults!~%" *pc-temp-string-1*)) + 0) + +(defmethod new pc-settings ((allocation symbol) (type-to-make type)) + "make a new pc-settings" + (let ((obj (object-new allocation type-to-make (the-as int (-> type-to-make size))))) + (reset obj) + ;; auto load settings if available + ;; if saved settings are corrupted or not found, use defaults + + (load-settings obj) + + obj)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;; PC settings +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(define *pc-settings* (new 'global 'pc-settings))