From 595089187d430d43270e2cecd078c5137332d11d Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:30:18 +0200 Subject: [PATCH] leaderboard tracker (#740) * leaderboard tracker * more * more * more * more --- parallel-rdp/interface.cpp | 38 +++++++++++++++++++++++-- parallel-rdp/interface.hpp | 2 ++ retroachievements/retroachievements.c | 27 +++++++++++++----- retroachievements/retroachievements.h | 2 +- src/device/vi.rs | 4 +-- src/main.rs | 15 ++++++++-- src/retroachievements.rs | 13 +++++++-- src/ui/gui.rs | 6 ++++ src/ui/gui/appwindow.slint | 7 +++-- src/ui/gui/retroachievements_page.slint | 21 +++++++++++--- src/ui/netplay.rs | 1 + 11 files changed, 111 insertions(+), 25 deletions(-) diff --git a/parallel-rdp/interface.cpp b/parallel-rdp/interface.cpp index 4e79932..f4ad2e0 100644 --- a/parallel-rdp/interface.cpp +++ b/parallel-rdp/interface.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace Vulkan; @@ -101,6 +102,8 @@ static TTF_Font *achievement_challenge_indicator_font; static std::vector achievement_challenge_indicators; static Vulkan::ImageHandle achievement_challenge_indicator_image; static Vulkan::ImageHandle achievement_progress_indicator_image; +static std::map leaderboard_trackers; +static Vulkan::ImageHandle leaderboard_tracker_image; #define MESSAGE_TIME 3000 // 3 seconds @@ -308,12 +311,15 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, achievement_challenge_indicators.clear(); achievement_challenge_indicator_image = Vulkan::ImageHandle(); achievement_progress_indicator_image = Vulkan::ImageHandle(); + leaderboard_trackers.clear(); + leaderboard_tracker_image = Vulkan::ImageHandle(); } void rdp_close() { messages = std::queue(); achievement_challenge_indicator_image = Vulkan::ImageHandle(); achievement_progress_indicator_image = Vulkan::ImageHandle(); + leaderboard_tracker_image = Vulkan::ImageHandle(); if (wsi) wsi->end_frame(); @@ -490,6 +496,8 @@ static void render_frame(Vulkan::Device &device) { } } else if (achievement_progress_indicator_image) { draw_indicator(cmd, achievement_progress_indicator_image, vp); + } else if (leaderboard_tracker_image) { + draw_indicator(cmd, leaderboard_tracker_image, vp); } else if (achievement_challenge_indicator_image) { draw_indicator(cmd, achievement_challenge_indicator_image, vp); } @@ -794,8 +802,9 @@ uint64_t rdp_process_commands() { static void update_challenge_indicator() { std::string message; - for (const auto &achievement_title : achievement_challenge_indicators) { - message += achievement_title; + const auto &v = achievement_challenge_indicators; + for (size_t i = 0; i < std::min(v.size(), 5); ++i) { + message += v[i]; message += '\n'; } achievement_challenge_indicator_image = create_message_image( @@ -803,6 +812,16 @@ static void update_challenge_indicator() { message.c_str()); } +static void update_leaderboard_tracker() { + std::string message; + for (const auto &leaderboard_tracker : leaderboard_trackers) { + message += leaderboard_tracker.second; + message += '\n'; + } + leaderboard_tracker_image = + create_message_image(wsi->get_device(), 0, message_font, message.c_str()); +} + void achievement_challenge_indicator_add(const char *achievement_title) { achievement_challenge_indicators.push_back(achievement_title); update_challenge_indicator(); @@ -828,3 +847,18 @@ void achievement_progress_add(const char *achievement_title, void achievement_progress_remove(const char *achievement_title) { achievement_progress_indicator_image = Vulkan::ImageHandle(); } + +void leaderboard_tracker_add(uint32_t id, const char *display) { + std::string message = display; + leaderboard_trackers[id] = message; + update_leaderboard_tracker(); +} + +void leaderboard_tracker_remove(uint32_t id) { + leaderboard_trackers.erase(id); + if (leaderboard_trackers.empty()) { + leaderboard_tracker_image = Vulkan::ImageHandle(); + } else { + update_leaderboard_tracker(); + } +} diff --git a/parallel-rdp/interface.hpp b/parallel-rdp/interface.hpp index 51a6a5c..4629067 100644 --- a/parallel-rdp/interface.hpp +++ b/parallel-rdp/interface.hpp @@ -56,6 +56,8 @@ void achievement_challenge_indicator_remove(const char *achievement_title); void achievement_progress_add(const char *achievement_title, const char *progress); void achievement_progress_remove(const char *achievement_title); +void leaderboard_tracker_add(uint32_t id, const char *display); +void leaderboard_tracker_remove(uint32_t id); #ifdef __cplusplus } diff --git a/retroachievements/retroachievements.c b/retroachievements/retroachievements.c index c6016eb..29432d7 100644 --- a/retroachievements/retroachievements.c +++ b/retroachievements/retroachievements.c @@ -20,6 +20,7 @@ static size_t g_dmem_size = 0; static bool g_game_loaded = false; static bool g_user_logged_in = false; static bool g_challenge = false; +static bool g_leaderboard = false; static const char *g_username = NULL; static const char *g_token = NULL; static char load_game_error_message[512]; @@ -181,7 +182,7 @@ void ra_set_dmem(const uint8_t *dmem, size_t dmem_size) { static void leaderboard_started(const rc_client_leaderboard_t *leaderboard) { char buffer[512]; - snprintf(buffer, sizeof(buffer), "Lboard attempt started: %s", + snprintf(buffer, sizeof(buffer), "Leaderboard started: %s", leaderboard->title); rdp_onscreen_message(buffer); } @@ -189,7 +190,7 @@ static void leaderboard_started(const rc_client_leaderboard_t *leaderboard) { static void leaderboard_failed(const rc_client_leaderboard_t *leaderboard) { char buffer[512]; - snprintf(buffer, sizeof(buffer), "Lboard attempt failed: %s", + snprintf(buffer, sizeof(buffer), "Leaderboard failed: %s", leaderboard->title); rdp_onscreen_message(buffer); } @@ -197,7 +198,7 @@ static void leaderboard_failed(const rc_client_leaderboard_t *leaderboard) { static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) { char buffer[512]; - snprintf(buffer, sizeof(buffer), "Lboard submitted: %s - %s", + snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s", leaderboard->title, leaderboard->tracker_value); rdp_onscreen_message(buffer); } @@ -243,13 +244,16 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) { achievement_triggered(event->achievement); break; case RC_CLIENT_EVENT_LEADERBOARD_STARTED: - leaderboard_started(event->leaderboard); + if (g_leaderboard) + leaderboard_started(event->leaderboard); break; case RC_CLIENT_EVENT_LEADERBOARD_FAILED: - leaderboard_failed(event->leaderboard); + if (g_leaderboard) + leaderboard_failed(event->leaderboard); break; case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: - leaderboard_submitted(event->leaderboard); + if (g_leaderboard) + leaderboard_submitted(event->leaderboard); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: if (g_challenge) @@ -271,10 +275,18 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) { event->achievement->measured_progress); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + if (g_leaderboard) + leaderboard_tracker_add(event->leaderboard_tracker->id, + event->leaderboard_tracker->display); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + if (g_leaderboard) + leaderboard_tracker_remove(event->leaderboard_tracker->id); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + if (g_leaderboard) + leaderboard_tracker_add(event->leaderboard_tracker->id, + event->leaderboard_tracker->display); break; case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: break; @@ -293,7 +305,7 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) { } } -void ra_init_client(bool hardcore, bool challenge) { +void ra_init_client(bool hardcore, bool challenge, bool leaderboard) { // Create the client instance (using a global variable simplifies this // example) g_client = rc_client_create(read_memory, server_call); @@ -311,6 +323,7 @@ void ra_init_client(bool hardcore, bool challenge) { rc_client_set_hardcore_enabled(g_client, hardcore); g_challenge = challenge; + g_leaderboard = leaderboard; } bool ra_get_hardcore() { diff --git a/retroachievements/retroachievements.h b/retroachievements/retroachievements.h index 23292c5..5c2e532 100644 --- a/retroachievements/retroachievements.h +++ b/retroachievements/retroachievements.h @@ -2,7 +2,7 @@ #include #include -void ra_init_client(bool hardcore, bool challenge); +void ra_init_client(bool hardcore, bool challenge, bool leaderboard); void ra_shutdown_client(); void ra_welcome(); bool ra_get_hardcore(); diff --git a/src/device/vi.rs b/src/device/vi.rs index 4d5bdc7..6096b7e 100644 --- a/src/device/vi.rs +++ b/src/device/vi.rs @@ -128,10 +128,10 @@ pub fn vertical_interrupt_event(device: &mut device::Device) { cheats::execute_cheats(device, device.cheats.cheats.clone()); } - retroachievements::do_frame(); - ui::video::render_frame(); + retroachievements::do_frame(); + let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device); if let Some(netplay) = &mut device.netplay { diff --git a/src/main.rs b/src/main.rs index 78c3d4f..24a0553 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,6 +111,11 @@ struct Args { help = "Enable Challenge Indicators for RetroAchievements" )] ra_challenge: bool, + #[arg( + long = "ra-leaderboard", + help = "Enable Leaderboard Trackers for RetroAchievements" + )] + ra_leaderboard: bool, } #[tokio::main(worker_threads = 4)] @@ -164,9 +169,13 @@ async fn main() -> std::io::Result<()> { }; if cfg!(ra_hardcore_enabled) { - retroachievements::init_client(args.ra_hardcore, args.ra_challenge); + retroachievements::init_client( + args.ra_hardcore, + args.ra_challenge, + args.ra_leaderboard, + ); } else { - retroachievements::init_client(false, args.ra_challenge); + retroachievements::init_client(false, args.ra_challenge, args.ra_leaderboard); } let mut shutdown_tx = None; @@ -264,7 +273,7 @@ async fn main() -> std::io::Result<()> { ui::input::bind_input_profile(&mut ui, profile, port); } } else { - retroachievements::init_client(false, false); + retroachievements::init_client(false, false, false); gui::app_window(); retroachievements::shutdown_client(); } diff --git a/src/retroachievements.rs b/src/retroachievements.rs index 8e0aa4b..4b2e5a0 100644 --- a/src/retroachievements.rs +++ b/src/retroachievements.rs @@ -10,6 +10,7 @@ pub struct RAConfig { pub enabled: bool, pub hardcore: bool, pub challenge: bool, + pub leaderboard: bool, } #[unsafe(no_mangle)] @@ -41,6 +42,7 @@ pub extern "C" fn store_retroachievements_credentials( enabled: result.enabled, hardcore: result.hardcore, challenge: result.challenge, + leaderboard: result.leaderboard, } } else { RAConfig { @@ -49,6 +51,7 @@ pub extern "C" fn store_retroachievements_credentials( enabled: false, hardcore: false, challenge: false, + leaderboard: false, } }; let f = std::fs::File::create(&file_path).unwrap(); @@ -170,6 +173,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { app.set_ra_enabled(result.enabled); app.set_ra_hardcore(result.hardcore); app.set_ra_challenge(result.challenge); + app.set_ra_leaderboard(result.leaderboard); token = result.token; } else { app.set_ra_hardcore(true); @@ -199,7 +203,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { .unwrap(); }); - app.on_ra_toggled(move |enabled, hardcore, challenge| { + app.on_ra_toggled(move |enabled, hardcore, challenge, leaderboard| { let file_path = ui::get_dirs().config_dir.join("retroachievements.json"); let raconfig = if let Ok(ra_config) = std::fs::read(&file_path) && let Ok(result) = serde_json::from_slice::(ra_config.as_ref()) @@ -212,6 +216,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { enabled, hardcore, challenge, + leaderboard, } } else { RAConfig { @@ -220,6 +225,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { enabled, hardcore, challenge, + leaderboard, } } } else { @@ -229,6 +235,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { enabled, hardcore, challenge, + leaderboard, } }; let f = std::fs::File::create(&file_path).unwrap(); @@ -269,8 +276,8 @@ pub fn do_idle() { unsafe { ra_do_idle() }; } -pub fn init_client(hardcore: bool, challenge: bool) { - unsafe { ra_init_client(hardcore, challenge) }; +pub fn init_client(hardcore: bool, challenge: bool, leaderboard: bool) { + unsafe { ra_init_client(hardcore, challenge, leaderboard) }; } pub fn shutdown_client() { diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 63dcef4..4009434 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -34,6 +34,7 @@ pub struct GameSettings { pub struct RASettings { pub hardcore: bool, pub challenge: bool, + pub leaderboard: bool, } fn check_latest_version(weak: slint::Weak) { @@ -370,6 +371,9 @@ pub fn run_rom( if ra_settings.challenge { command.args(["--ra-challenge"]); } + if ra_settings.leaderboard { + command.args(["--ra-leaderboard"]); + } } for i in 0..4 { @@ -434,6 +438,7 @@ fn open_rom(app: &AppWindow) { let disable_expansion_pak = app.get_disable_expansion_pak(); let ra_hardcore = app.get_ra_hardcore(); let ra_challenge = app.get_ra_challenge(); + let ra_leaderboard = app.get_ra_leaderboard(); let weak = app.as_weak(); tokio::spawn(async move { @@ -467,6 +472,7 @@ fn open_rom(app: &AppWindow) { RASettings { hardcore: ra_hardcore, challenge: ra_challenge, + leaderboard: ra_leaderboard, }, weak, ); diff --git a/src/ui/gui/appwindow.slint b/src/ui/gui/appwindow.slint index 299526a..dc7504f 100644 --- a/src/ui/gui/appwindow.slint +++ b/src/ui/gui/appwindow.slint @@ -38,7 +38,7 @@ export component AppWindow inherits Window { callback ra_button_clicked(string); callback ra_games_clicked; callback ra_show_profile_clicked(string); - callback ra_toggled(bool,bool,bool); + callback ra_toggled(bool,bool,bool,bool); in property version <=> AboutData.version; in property has_update <=> AboutData.has_update; in-out property ra_username <=> RAData.ra_username; @@ -49,6 +49,7 @@ export component AppWindow inherits Window { in-out property ra_enabled <=> RAData.enabled; in-out property ra_hardcore <=> RAData.hardcore; in-out property ra_challenge <=> RAData.challenge; + in-out property ra_leaderboard <=> RAData.leaderboard; in-out property integer_scaling <=> SettingsData.integer_scaling; in-out property fullscreen <=> SettingsData.fullscreen; in-out property widescreen <=> SettingsData.widescreen; @@ -135,8 +136,8 @@ export component AppWindow inherits Window { ra_button_clicked(password) => { ra_button_clicked(password); } - ra_toggled(enabled,hardcore,challenge) => { - ra_toggled(enabled,hardcore,challenge); + ra_toggled(enabled,hardcore,challenge,leaderboard) => { + ra_toggled(enabled,hardcore,challenge,leaderboard); } ra_games_clicked => { ra_games_clicked(); diff --git a/src/ui/gui/retroachievements_page.slint b/src/ui/gui/retroachievements_page.slint index 0784f90..5095934 100644 --- a/src/ui/gui/retroachievements_page.slint +++ b/src/ui/gui/retroachievements_page.slint @@ -8,6 +8,7 @@ export global RAData { in-out property enabled; in-out property hardcore; in-out property challenge; + in-out property leaderboard; in-out property ra_logging_in; in-out property ra_show_profile; in-out property ra_username; @@ -17,7 +18,7 @@ export global RAData { export component RetroAchievements inherits Page { callback ra_button_clicked(string); - callback ra_toggled(bool,bool,bool); + callback ra_toggled(bool,bool,bool,bool); callback ra_games_clicked; callback ra_show_profile_clicked(string); title: @tr("RetroAchievements"); @@ -38,7 +39,7 @@ export component RetroAchievements inherits Page { RAData.current_user_message = "Not currently logged in"; RAData.ra_show_profile = false; } - ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge); + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard); } } } @@ -50,7 +51,7 @@ export component RetroAchievements inherits Page { enabled: RAData.enabled; toggled => { RAData.hardcore = self.checked; - ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge); + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard); } } } @@ -62,7 +63,19 @@ export component RetroAchievements inherits Page { enabled: RAData.enabled; toggled => { RAData.challenge = self.checked; - ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge); + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard); + } + } + } + HorizontalBox { + alignment: center; + CheckBox { + text: @tr("Leaderboard Trackers"); + checked: RAData.leaderboard; + enabled: RAData.enabled; + toggled => { + RAData.leaderboard = self.checked; + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard); } } } diff --git a/src/ui/netplay.rs b/src/ui/netplay.rs index a142526..7eafc3d 100644 --- a/src/ui/netplay.rs +++ b/src/ui/netplay.rs @@ -1110,6 +1110,7 @@ fn setup_wait_window( RASettings { hardcore: false, challenge: false, + leaderboard: false, }, weak_app, );