diff --git a/build.rs b/build.rs index 27fae49..7b2051d 100644 --- a/build.rs +++ b/build.rs @@ -15,7 +15,7 @@ fn main() { let mut rdp_build = cc::Build::new(); rdp_build .cpp(true) - .std("c++23") + .std("c++20") .flag("-Wno-unused-parameter") .flag("-Wno-missing-field-initializers") .file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/command_ring.cpp") diff --git a/data/NotoSansJP-Regular.ttf b/data/NotoSansJP-Regular.ttf deleted file mode 100644 index b2dad73..0000000 Binary files a/data/NotoSansJP-Regular.ttf and /dev/null differ diff --git a/data/Roboto-Regular.ttf b/data/Roboto-Regular.ttf deleted file mode 100644 index ddf4bfa..0000000 Binary files a/data/Roboto-Regular.ttf and /dev/null differ diff --git a/data/RobotoMono-Regular.ttf b/data/RobotoMono-Regular.ttf new file mode 100644 index 0000000..3806bfb Binary files /dev/null and b/data/RobotoMono-Regular.ttf differ diff --git a/parallel-rdp/interface.cpp b/parallel-rdp/interface.cpp index 904db02..4e79932 100644 --- a/parallel-rdp/interface.cpp +++ b/parallel-rdp/interface.cpp @@ -97,6 +97,11 @@ static TTF_Font *message_font; static std::queue messages; static uint64_t message_timer; +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; + #define MESSAGE_TIME 3000 // 3 seconds typedef struct { @@ -212,11 +217,14 @@ void rdp_new_processor(GFX_INFO _gfx_info) { } static ImageHandle create_message_image(Vulkan::Device &device, int width, - const char *message) { + TTF_Font *font, const char *message) { + if (strstr(message, "\n")) + width = 0; + SDL_Color fg = {255, 255, 255, 255}; SDL_Color bg = {0, 0, 0, 0}; SDL_Surface *surface = - TTF_RenderText_LCD_Wrapped(message_font, message, 0, fg, bg, width); + TTF_RenderText_LCD_Wrapped(font, message, 0, fg, bg, width); ImageCreateInfo info = ImageCreateInfo::immutable_2d_image( surface->w, surface->h, VK_FORMAT_B8G8R8A8_UNORM, false); ImageInitialData initial_data = {}; @@ -279,7 +287,9 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, message_font = TTF_OpenFontIO(SDL_IOFromConstMem(font, font_size), true, 25.0); - if (!message_font) { + achievement_challenge_indicator_font = + TTF_OpenFontIO(SDL_IOFromConstMem(font, font_size), true, 12.0); + if (!message_font || !achievement_challenge_indicator_font) { rdp_close(); return; } @@ -294,10 +304,16 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, messages = std::queue(); message_timer = 0; + + achievement_challenge_indicators.clear(); + achievement_challenge_indicator_image = Vulkan::ImageHandle(); + achievement_progress_indicator_image = Vulkan::ImageHandle(); } void rdp_close() { messages = std::queue(); + achievement_challenge_indicator_image = Vulkan::ImageHandle(); + achievement_progress_indicator_image = Vulkan::ImageHandle(); if (wsi) wsi->end_frame(); @@ -306,6 +322,10 @@ void rdp_close() { TTF_CloseFont(message_font); message_font = nullptr; } + if (achievement_challenge_indicator_font) { + TTF_CloseFont(achievement_challenge_indicator_font); + achievement_challenge_indicator_font = nullptr; + } if (processor) { delete processor; processor = nullptr; @@ -364,6 +384,20 @@ static void calculate_viewport(float *x, float *y, float *width, float *height, } } +static void draw_indicator(CommandBufferHandle cmd, + Vulkan::ImageHandle indicator_image, + VkViewport &vp) { + cmd->set_texture(0, 0, indicator_image->get_view(), + Vulkan::StockSampler::NearestClamp); + vp.x = vp.x + vp.width - indicator_image->get_width(); + vp.y = vp.y + vp.height - indicator_image->get_height(); + vp.height = indicator_image->get_height(); + vp.width = indicator_image->get_width(); + cmd->set_viewport(vp); + + cmd->draw(3); +} + static void render_frame(Vulkan::Device &device) { RDP::ScanoutOptions options = {}; options.persist_frame_on_invalid_input = true; @@ -437,8 +471,8 @@ static void render_frame(Vulkan::Device &device) { if (!messages.empty()) { Message *message = &messages.front(); if (!message->image) { - message->image = - create_message_image(device, vp.width, message->message.c_str()); + message->image = create_message_image(device, vp.width, message_font, + message->message.c_str()); } cmd->set_texture(0, 0, message->image->get_view(), Vulkan::StockSampler::NearestClamp); @@ -454,6 +488,10 @@ static void render_frame(Vulkan::Device &device) { messages.pop(); message_timer = SDL_GetTicks() + MESSAGE_TIME; } + } else if (achievement_progress_indicator_image) { + draw_indicator(cmd, achievement_progress_indicator_image, vp); + } else if (achievement_challenge_indicator_image) { + draw_indicator(cmd, achievement_challenge_indicator_image, vp); } } @@ -753,3 +791,40 @@ uint64_t rdp_process_commands() { return interrupt_timer; } + +static void update_challenge_indicator() { + std::string message; + for (const auto &achievement_title : achievement_challenge_indicators) { + message += achievement_title; + message += '\n'; + } + achievement_challenge_indicator_image = create_message_image( + wsi->get_device(), 0, achievement_challenge_indicator_font, + message.c_str()); +} + +void achievement_challenge_indicator_add(const char *achievement_title) { + achievement_challenge_indicators.push_back(achievement_title); + update_challenge_indicator(); +} + +void achievement_challenge_indicator_remove(const char *achievement_title) { + std::erase(achievement_challenge_indicators, achievement_title); + if (achievement_challenge_indicators.empty()) { + achievement_challenge_indicator_image = Vulkan::ImageHandle(); + } else { + update_challenge_indicator(); + } +} + +void achievement_progress_add(const char *achievement_title, + const char *progress) { + char message[512]; + snprintf(message, sizeof(message), "%s: %s", achievement_title, progress); + achievement_progress_indicator_image = + create_message_image(wsi->get_device(), 0, message_font, message); +} + +void achievement_progress_remove(const char *achievement_title) { + achievement_progress_indicator_image = Vulkan::ImageHandle(); +} diff --git a/parallel-rdp/interface.hpp b/parallel-rdp/interface.hpp index 06125dc..51a6a5c 100644 --- a/parallel-rdp/interface.hpp +++ b/parallel-rdp/interface.hpp @@ -51,6 +51,12 @@ size_t rdp_state_size(); void rdp_save_state(uint8_t *state); void rdp_load_state(const uint8_t *state); +void achievement_challenge_indicator_add(const char *achievement_title); +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); + #ifdef __cplusplus } #endif diff --git a/retroachievements/retroachievements.c b/retroachievements/retroachievements.c index 785fd60..c6016eb 100644 --- a/retroachievements/retroachievements.c +++ b/retroachievements/retroachievements.c @@ -19,6 +19,7 @@ static const uint8_t *g_dmem = NULL; 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 const char *g_username = NULL; static const char *g_token = NULL; static char load_game_error_message[512]; @@ -180,7 +181,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), "RA leaderboard attempt started: %s", + snprintf(buffer, sizeof(buffer), "Lboard attempt started: %s", leaderboard->title); rdp_onscreen_message(buffer); } @@ -188,7 +189,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), "RA leaderboard attempt failed: %s", + snprintf(buffer, sizeof(buffer), "Lboard attempt failed: %s", leaderboard->title); rdp_onscreen_message(buffer); } @@ -196,7 +197,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), "RA leaderboard submitted: %s - %s", + snprintf(buffer, sizeof(buffer), "Lboard submitted: %s - %s", leaderboard->title, leaderboard->tracker_value); rdp_onscreen_message(buffer); } @@ -204,16 +205,7 @@ static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) { static void achievement_triggered(const rc_client_achievement_t *achievement) { char buffer[512]; - snprintf(buffer, sizeof(buffer), "RA unlocked: %s", achievement->title); - rdp_onscreen_message(buffer); -} - -static void -achievement_progress_updated(const rc_client_achievement_t *achievement) { - char buffer[512]; - - snprintf(buffer, sizeof(buffer), "RA updated: %s - %s", achievement->title, - achievement->measured_progress); + snprintf(buffer, sizeof(buffer), "Unlocked: %s", achievement->title); rdp_onscreen_message(buffer); } @@ -221,8 +213,8 @@ static void game_completed(rc_client_t *client) { char buffer[512]; const rc_client_game_t *game = rc_client_get_game_info(client); - snprintf(buffer, sizeof(buffer), "RA %s: %s", - rc_client_get_hardcore_enabled(client) ? "mastered" : "completed", + snprintf(buffer, sizeof(buffer), "%s: %s", + rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed", game->title); rdp_onscreen_message(buffer); } @@ -231,7 +223,7 @@ static void subset_completed(const rc_client_subset_t *subset, rc_client_t *client) { char buffer[512]; - snprintf(buffer, sizeof(buffer), "RA subset %s: %s", + snprintf(buffer, sizeof(buffer), "Subset %s: %s", rc_client_get_hardcore_enabled(client) ? "mastered" : "completed", subset->title); rdp_onscreen_message(buffer); @@ -260,15 +252,23 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) { leaderboard_submitted(event->leaderboard); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + if (g_challenge) + achievement_challenge_indicator_add(event->achievement->title); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + if (g_challenge) + achievement_challenge_indicator_remove(event->achievement->title); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + achievement_progress_add(event->achievement->title, + event->achievement->measured_progress); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + achievement_progress_remove(event->achievement->title); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: - achievement_progress_updated(event->achievement); + achievement_progress_add(event->achievement->title, + event->achievement->measured_progress); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: break; @@ -293,7 +293,7 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) { } } -void ra_init_client(bool hardcore) { +void ra_init_client(bool hardcore, bool challenge) { // Create the client instance (using a global variable simplifies this // example) g_client = rc_client_create(read_memory, server_call); @@ -309,6 +309,8 @@ void ra_init_client(bool hardcore) { rc_client_set_event_handler(g_client, event_handler); rc_client_set_hardcore_enabled(g_client, hardcore); + + g_challenge = challenge; } bool ra_get_hardcore() { diff --git a/retroachievements/retroachievements.h b/retroachievements/retroachievements.h index f7377b1..23292c5 100644 --- a/retroachievements/retroachievements.h +++ b/retroachievements/retroachievements.h @@ -2,7 +2,7 @@ #include #include -void ra_init_client(bool hardcore); +void ra_init_client(bool hardcore, bool challenge); void ra_shutdown_client(); void ra_welcome(); bool ra_get_hardcore(); diff --git a/src/main.rs b/src/main.rs index 291e2ee..78c3d4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,11 @@ struct Args { help = "Enable Hardcore mode for RetroAchievements" )] ra_hardcore: bool, + #[arg( + long = "ra-challenge", + help = "Enable Challenge Indicators for RetroAchievements" + )] + ra_challenge: bool, } #[tokio::main(worker_threads = 4)] @@ -159,9 +164,9 @@ async fn main() -> std::io::Result<()> { }; if cfg!(ra_hardcore_enabled) { - retroachievements::init_client(args.ra_hardcore); + retroachievements::init_client(args.ra_hardcore, args.ra_challenge); } else { - retroachievements::init_client(false); + retroachievements::init_client(false, args.ra_challenge); } let mut shutdown_tx = None; @@ -259,7 +264,7 @@ async fn main() -> std::io::Result<()> { ui::input::bind_input_profile(&mut ui, profile, port); } } else { - retroachievements::init_client(false); + retroachievements::init_client(false, false); gui::app_window(); retroachievements::shutdown_client(); } diff --git a/src/retroachievements.rs b/src/retroachievements.rs index df88c87..8e0aa4b 100644 --- a/src/retroachievements.rs +++ b/src/retroachievements.rs @@ -9,6 +9,7 @@ pub struct RAConfig { pub token: String, pub enabled: bool, pub hardcore: bool, + pub challenge: bool, } #[unsafe(no_mangle)] @@ -39,6 +40,7 @@ pub extern "C" fn store_retroachievements_credentials( token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(), enabled: result.enabled, hardcore: result.hardcore, + challenge: result.challenge, } } else { RAConfig { @@ -46,6 +48,7 @@ pub extern "C" fn store_retroachievements_credentials( token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(), enabled: false, hardcore: false, + challenge: false, } }; let f = std::fs::File::create(&file_path).unwrap(); @@ -166,6 +169,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { app.set_ra_username(result.username.into()); app.set_ra_enabled(result.enabled); app.set_ra_hardcore(result.hardcore); + app.set_ra_challenge(result.challenge); token = result.token; } else { app.set_ra_hardcore(true); @@ -195,7 +199,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { .unwrap(); }); - app.on_ra_toggled(move |enabled, hardcore| { + app.on_ra_toggled(move |enabled, hardcore, challenge| { 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()) @@ -207,6 +211,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { token: "".into(), enabled, hardcore, + challenge, } } else { RAConfig { @@ -214,6 +219,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { token: result.token, enabled, hardcore, + challenge, } } } else { @@ -222,6 +228,7 @@ pub fn ra_window(app: &ui::gui::AppWindow) { token: "".into(), enabled, hardcore, + challenge, } }; let f = std::fs::File::create(&file_path).unwrap(); @@ -262,8 +269,8 @@ pub fn do_idle() { unsafe { ra_do_idle() }; } -pub fn init_client(hardcore: bool) { - unsafe { ra_init_client(hardcore) }; +pub fn init_client(hardcore: bool, challenge: bool) { + unsafe { ra_init_client(hardcore, challenge) }; } pub fn shutdown_client() { diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 718a293..63dcef4 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -31,6 +31,11 @@ pub struct GameSettings { pub load_savestate_slot: Option, } +pub struct RASettings { + pub hardcore: bool, + pub challenge: bool, +} + fn check_latest_version(weak: slint::Weak) { let client = reqwest::Client::builder() .user_agent(env!("CARGO_PKG_NAME")) @@ -325,7 +330,7 @@ pub fn run_rom( file_path: std::path::PathBuf, game_settings: GameSettings, netplay: Option, - ra_hardcore: bool, + ra_settings: RASettings, weak: slint::Weak, ) { tokio::spawn(async move { @@ -359,9 +364,12 @@ pub fn run_rom( "--ra-token", retroachievements::get_token(), ]); - if ra_hardcore { + if ra_settings.hardcore { command.args(["--ra-hardcore"]); } + if ra_settings.challenge { + command.args(["--ra-challenge"]); + } } for i in 0..4 { @@ -425,6 +433,7 @@ fn open_rom(app: &AppWindow) { let overclock = app.get_overclock_n64_cpu(); let disable_expansion_pak = app.get_disable_expansion_pak(); let ra_hardcore = app.get_ra_hardcore(); + let ra_challenge = app.get_ra_challenge(); let weak = app.as_weak(); tokio::spawn(async move { @@ -455,7 +464,10 @@ fn open_rom(app: &AppWindow) { load_savestate_slot: None, }, None, - ra_hardcore, + RASettings { + hardcore: ra_hardcore, + challenge: ra_challenge, + }, weak, ); } diff --git a/src/ui/gui/appwindow.slint b/src/ui/gui/appwindow.slint index 5e96712..299526a 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); + callback ra_toggled(bool,bool,bool); in property version <=> AboutData.version; in property has_update <=> AboutData.has_update; in-out property ra_username <=> RAData.ra_username; @@ -48,6 +48,7 @@ export component AppWindow inherits Window { in-out property ra_current_user_message <=> RAData.current_user_message; in-out property ra_enabled <=> RAData.enabled; in-out property ra_hardcore <=> RAData.hardcore; + in-out property ra_challenge <=> RAData.challenge; in-out property integer_scaling <=> SettingsData.integer_scaling; in-out property fullscreen <=> SettingsData.fullscreen; in-out property widescreen <=> SettingsData.widescreen; @@ -134,8 +135,8 @@ export component AppWindow inherits Window { ra_button_clicked(password) => { ra_button_clicked(password); } - ra_toggled(enabled,hardcore) => { - ra_toggled(enabled,hardcore); + ra_toggled(enabled,hardcore,challenge) => { + ra_toggled(enabled,hardcore,challenge); } ra_games_clicked => { ra_games_clicked(); diff --git a/src/ui/gui/retroachievements_page.slint b/src/ui/gui/retroachievements_page.slint index 84f2640..0784f90 100644 --- a/src/ui/gui/retroachievements_page.slint +++ b/src/ui/gui/retroachievements_page.slint @@ -7,6 +7,7 @@ import { Page } from "page.slint"; export global RAData { in-out property enabled; in-out property hardcore; + in-out property challenge; in-out property ra_logging_in; in-out property ra_show_profile; in-out property ra_username; @@ -16,7 +17,7 @@ export global RAData { export component RetroAchievements inherits Page { callback ra_button_clicked(string); - callback ra_toggled(bool,bool); + callback ra_toggled(bool,bool,bool); callback ra_games_clicked; callback ra_show_profile_clicked(string); title: @tr("RetroAchievements"); @@ -37,7 +38,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); + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge); } } } @@ -49,7 +50,19 @@ export component RetroAchievements inherits Page { enabled: RAData.enabled; toggled => { RAData.hardcore = self.checked; - ra_toggled(RAData.enabled,RAData.hardcore); + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge); + } + } + } + HorizontalBox { + alignment: center; + CheckBox { + text: @tr("Challenge Indicators"); + checked: RAData.challenge; + enabled: RAData.enabled; + toggled => { + RAData.challenge = self.checked; + ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge); } } } diff --git a/src/ui/input.rs b/src/ui/input.rs index 2f7ee7e..a989a61 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -614,7 +614,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, d let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) }; let font = unsafe { - let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf"); + let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf"); sdl3_ttf_sys::ttf::TTF_OpenFontIO( sdl3_sys::everything::SDL_IOFromConstMem( font_bytes.as_ptr() as *const std::ffi::c_void, diff --git a/src/ui/netplay.rs b/src/ui/netplay.rs index dfde901..a142526 100644 --- a/src/ui/netplay.rs +++ b/src/ui/netplay.rs @@ -2,7 +2,7 @@ use crate::device; use crate::ui; use crate::ui::gui::{ AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, GbPaths, - NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, run_rom, save_settings, + NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, RASettings, run_rom, save_settings, }; use futures::{SinkExt, StreamExt}; use sha2::{Digest, Sha256}; @@ -1107,7 +1107,10 @@ fn setup_wait_window( peer_addr: socket_addr, player_number: player_number as u8, }), - false, + RASettings { + hardcore: false, + challenge: false, + }, weak_app, ); }) diff --git a/src/ui/video.rs b/src/ui/video.rs index 3208647..74cc61c 100644 --- a/src/ui/video.rs +++ b/src/ui/video.rs @@ -80,7 +80,7 @@ pub fn init(device: &mut device::Device) { }; unsafe { - let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf"); + let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf"); rdp_init( device.ui.video.window as *mut std::ffi::c_void, gfx_info,