challenge indicators (#739)

* Add Challenge Indicators

* more

* more

* use c++20

* shorten messages

* progress

* more

* more

* more

* more
This commit is contained in:
Logan McNaughton 2026-04-13 09:16:15 +02:00 committed by GitHub
parent 4240a99ad5
commit c4b86eb4b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 168 additions and 44 deletions

View File

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

Binary file not shown.

Binary file not shown.

BIN
data/RobotoMono-Regular.ttf Normal file

Binary file not shown.

View File

@ -97,6 +97,11 @@ static TTF_Font *message_font;
static std::queue<Message> messages;
static uint64_t message_timer;
static TTF_Font *achievement_challenge_indicator_font;
static std::vector<const char *> 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>();
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<Message>();
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();
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@
#include <stddef.h>
#include <stdint.h>
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();

View File

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

View File

@ -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::<RAConfig>(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() {

View File

@ -31,6 +31,11 @@ pub struct GameSettings {
pub load_savestate_slot: Option<u32>,
}
pub struct RASettings {
pub hardcore: bool,
pub challenge: bool,
}
fn check_latest_version(weak: slint::Weak<AppWindow>) {
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<NetplayDevice>,
ra_hardcore: bool,
ra_settings: RASettings,
weak: slint::Weak<AppWindow>,
) {
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,
);
}

View File

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

View File

@ -7,6 +7,7 @@ import { Page } from "page.slint";
export global RAData {
in-out property <bool> enabled;
in-out property <bool> hardcore;
in-out property <bool> challenge;
in-out property <bool> ra_logging_in;
in-out property <bool> ra_show_profile;
in-out property <string> 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);
}
}
}

View File

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

View File

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

View File

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