g/jak2: initial Discord RPC implementation (#2100)

Notable things:
- This assert is hit when trying to save the pc-settings file, NYI
e630b50690/game/kernel/common/Symbol4.h (L14)
so right now settings aren't persisted. But RPC defaults to on
- The existing functions can probably be made generic based off the game
version, but I didn't spend time refactoring them yet as they aren't
really ready to be used in jak 2 yet (we have no screenshots for the
levels for example)
This commit is contained in:
Tyler Wilding 2023-01-07 10:34:01 -05:00 committed by GitHub
parent bf10f1edd4
commit f699675ede
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 652 additions and 38 deletions

View file

@ -6,9 +6,17 @@
#include <sstream>
#include <string>
#include "common/log/log.h"
#include "game/runtime.h"
int gDiscordRpcEnabled;
int64_t gStartTime;
static const char* APPLICATION_ID = "938876425585434654";
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"},
@ -29,12 +37,34 @@ static const std::map<std::string, std::string> jak1_level_names = {
{"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);
}
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<int>(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*/) {}

View file

@ -2,8 +2,13 @@
#include <string>
#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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1657,6 +1657,8 @@
(with-profiler 'actors *profile-actors-color*
(suspend)
)
(#when PC_PORT
(update *pc-settings*))
)
)
(set! *dproc* #f)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: (<key> <value>) (<key> <value>) ...
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))