RetroAchievements (#710)

* retroachievements

* add rc_api_info

* disable hardcore

* clear password after login attempt

* proper check for hardcore

* more set_hardcore

* change checkbox

* add more stubs

* add more UI

* add button for RA website

* cleanup http callback
This commit is contained in:
Logan McNaughton
2026-04-07 00:59:03 -06:00
committed by GitHub
parent 5207bd4b27
commit e932239a63
18 changed files with 902 additions and 19 deletions
+3
View File
@@ -6,3 +6,6 @@
path = src/compat/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
branch = master
[submodule "retroachievements/rcheevos"]
path = retroachievements/rcheevos
url = https://github.com/RetroAchievements/rcheevos.git
+66
View File
@@ -1,5 +1,6 @@
fn main() {
println!("cargo::rerun-if-changed=parallel-rdp");
println!("cargo::rerun-if-changed=retroachievements");
println!("cargo::rerun-if-changed=src/compat");
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
@@ -69,6 +70,41 @@ fn main() {
.join("include"),
);
let mut retroachievements_build = cc::Build::new();
retroachievements_build
.flag("-Wno-unused-parameter")
.include("retroachievements/rcheevos/include")
.flag("-DRC_CLIENT_SUPPORTS_HASH")
.file("retroachievements/rcheevos/src/rc_client.c")
.file("retroachievements/rcheevos/src/rc_compat.c")
.file("retroachievements/rcheevos/src/rc_util.c")
.file("retroachievements/rcheevos/src/rcheevos/alloc.c")
.file("retroachievements/rcheevos/src/rcheevos/condition.c")
.file("retroachievements/rcheevos/src/rcheevos/condset.c")
.file("retroachievements/rcheevos/src/rcheevos/consoleinfo.c")
.file("retroachievements/rcheevos/src/rcheevos/format.c")
.file("retroachievements/rcheevos/src/rcheevos/lboard.c")
.file("retroachievements/rcheevos/src/rcheevos/memref.c")
.file("retroachievements/rcheevos/src/rcheevos/operand.c")
.file("retroachievements/rcheevos/src/rcheevos/richpresence.c")
.file("retroachievements/rcheevos/src/rcheevos/runtime.c")
.file("retroachievements/rcheevos/src/rcheevos/runtime_progress.c")
.file("retroachievements/rcheevos/src/rcheevos/trigger.c")
.file("retroachievements/rcheevos/src/rcheevos/value.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_common.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_info.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_runtime.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_user.c")
.file("retroachievements/rcheevos/src/rhash/aes.c")
.file("retroachievements/rcheevos/src/rhash/cdreader.c")
.file("retroachievements/rcheevos/src/rhash/md5.c")
.file("retroachievements/rcheevos/src/rhash/hash.c")
.file("retroachievements/rcheevos/src/rhash/hash_disc.c")
.file("retroachievements/rcheevos/src/rhash/hash_encrypted.c")
.file("retroachievements/rcheevos/src/rhash/hash_rom.c")
.file("retroachievements/rcheevos/src/rhash/hash_zip.c")
.file("retroachievements/retroachievements.c");
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let opt_flag = if arch == "x86_64" {
@@ -84,6 +120,7 @@ fn main() {
volk_build.flag(opt_flag);
rdp_build.flag(opt_flag);
simd_build.flag(opt_flag);
retroachievements_build.flag(opt_flag);
if os == "windows" {
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
@@ -111,12 +148,41 @@ fn main() {
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
retroachievements_build.flag("-flto=thin");
volk_build.compile("volk");
rdp_build.compile("parallel-rdp");
retroachievements_build.compile("retroachievements");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let retroachievements_bindings = bindgen::Builder::default()
.header("retroachievements/retroachievements.h")
.allowlist_function("ra_init_client")
.allowlist_function("ra_shutdown_client")
.allowlist_function("ra_get_hardcore")
.allowlist_function("ra_load_game")
.allowlist_function("ra_set_dmem")
.allowlist_function("ra_do_frame")
.allowlist_function("ra_do_idle")
.allowlist_function("ra_http_callback")
.allowlist_function("ra_logout_user")
.allowlist_function("ra_login_user")
.allowlist_function("ra_login_token_user")
.allowlist_function("ra_is_user_logged_in")
.allowlist_function("ra_get_username")
.allowlist_function("ra_get_token")
.allowlist_function("ra_state_size")
.allowlist_function("ra_save_state")
.allowlist_function("ra_load_state")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
retroachievements_bindings
.write_to_file(out_path.join("retroachievements_bindings.rs"))
.expect("Couldn't write bindings!");
let parallel_bindings = bindgen::Builder::default()
.header("parallel-rdp/interface.hpp")
.allowlist_function("rdp_init")
+284
View File
@@ -0,0 +1,284 @@
#include "retroachievements.h"
#include "../parallel-rdp/interface.hpp"
#include <rc_client.h>
#include <rc_consoles.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void rust_server_call(const char *url, const char *post_data,
rc_client_server_callback_t callback,
void *callback_data);
void store_retroachievements_credentials(const char *username,
const char *token, void *userdata);
rc_client_t *g_client = NULL;
uint8_t *g_dmem = NULL;
size_t g_dmem_size = 0;
bool g_game_loaded = false;
bool g_user_logged_in = false;
const char *g_username = NULL;
const char *g_token = NULL;
static uint32_t read_memory(uint32_t address, uint8_t *buffer,
uint32_t num_bytes, rc_client_t *client) {
if (address + num_bytes >= g_dmem_size)
return 0;
memcpy(buffer, &g_dmem[address], num_bytes);
return num_bytes;
}
static void server_call(const rc_api_request_t *request,
rc_client_server_callback_t callback,
void *callback_data, rc_client_t *client) {
rust_server_call(request->url, request->post_data, callback, callback_data);
}
void ra_http_callback(const char *content, size_t content_size, int status_code,
void *callback, void *callback_data) {
// Prepare a data object to pass the HTTP response to the callback
rc_api_server_response_t server_response;
memset(&server_response, 0, sizeof(server_response));
server_response.body = content;
server_response.body_length = content_size;
server_response.http_status_code = status_code;
// handle non-http errors (socket timeout, no internet available, etc)
if (status_code == 0) {
// Let rc_client know the error was not catastrophic and could be retried.
// It may decide to retry or just immediately pass the error to the
// callback. To prevent possible retries, use
// RC_API_SERVER_RESPONSE_CLIENT_ERROR.
server_response.http_status_code =
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR;
}
((rc_client_server_callback_t)callback)(&server_response, callback_data);
}
static void log_message(const char *message, const rc_client_t *client) {
printf("RetroAchievements: %s\n", message);
}
static void login_callback(int result, const char *error_message,
rc_client_t *client, void *userdata) {
// If not successful, just report the error and bail.
if (result != RC_OK) {
printf("RetroAchievements: Login failed: %s\n", error_message);
store_retroachievements_credentials(NULL, NULL, userdata);
return;
}
// Login was successful. Capture the token for future logins so we don't have
// to store the password anywhere.
const rc_client_user_t *user = rc_client_get_user_info(client);
store_retroachievements_credentials(user->username, user->token, userdata);
g_username = user->username;
g_token = user->token;
g_user_logged_in = true;
}
bool ra_is_user_logged_in() { return g_user_logged_in; }
const char *ra_get_username() { return g_username; }
const char *ra_get_token() { return g_token; }
void ra_logout_user() {
g_user_logged_in = false;
rc_client_logout(g_client);
}
void ra_login_user(const char *username, const char *password, void *userdata) {
// This will generate an HTTP payload and call the server_call chain above.
// Eventually, login_callback will be called to let us know if the login was
// successful.
g_user_logged_in = false;
rc_client_begin_login_with_password(g_client, username, password,
login_callback, userdata);
}
void ra_login_token_user(const char *username, const char *token,
void *userdata) {
// This is exactly the same functionality as
// rc_client_begin_login_with_password, but uses the token captured from the
// first login instead of a password. Note that it uses the same callback.
g_user_logged_in = false;
rc_client_begin_login_with_token(g_client, username, token, login_callback,
userdata);
}
static void load_game_callback(int result, const char *error_message,
rc_client_t *client, void *userdata) {
if (result != RC_OK) {
printf("RetroAchievements: game load failed: %s\n", error_message);
return;
}
char buffer[512];
const rc_client_game_t *game = rc_client_get_game_info(client);
rc_client_user_game_summary_t summary;
rc_client_get_user_game_summary(client, &summary);
snprintf(buffer, sizeof(buffer), "RA Loaded: %s", game->title);
rdp_onscreen_message(buffer);
int hardcore_enabled = rc_client_get_hardcore_enabled(client);
snprintf(buffer, sizeof(buffer), "RA mode: %s",
hardcore_enabled ? "Hardcore" : "Softcore");
rdp_onscreen_message(buffer);
snprintf(buffer, sizeof(buffer), "%u/%u achievements unlocked",
summary.num_unlocked_achievements, summary.num_core_achievements);
rdp_onscreen_message(buffer);
g_game_loaded = true;
}
void ra_load_game(const uint8_t *rom, size_t rom_size) {
if (!g_user_logged_in)
return;
rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO_64, NULL,
rom, rom_size, load_game_callback,
NULL);
}
void ra_set_dmem(uint8_t *dmem, size_t dmem_size) {
g_dmem = dmem;
g_dmem_size = dmem_size;
}
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 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",
game->title);
rdp_onscreen_message(buffer);
}
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",
rc_client_get_hardcore_enabled(client) ? "mastered" : "completed",
subset->title);
rdp_onscreen_message(buffer);
}
static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
switch (event->type) {
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
achievement_triggered(event->achievement);
break;
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
break;
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
break;
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
break;
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD:
break;
case RC_CLIENT_EVENT_GAME_COMPLETED:
game_completed(client);
break;
case RC_CLIENT_EVENT_SUBSET_COMPLETED:
subset_completed(event->subset, client);
break;
default:
printf("RetroAchievements: Unhandled event %d\n", event->type);
break;
}
}
void ra_init_client(bool hardcore) {
// Create the client instance (using a global variable simplifies this
// example)
g_client = rc_client_create(read_memory, server_call);
g_game_loaded = false;
g_user_logged_in = false;
g_dmem = NULL;
g_dmem_size = 0;
// Provide a logging function to simplify debugging
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
rc_client_set_event_handler(g_client, event_handler);
rc_client_set_hardcore_enabled(g_client, hardcore);
}
bool ra_get_hardcore() {
if (!g_user_logged_in)
return false;
return rc_client_get_hardcore_enabled(g_client);
}
void ra_shutdown_client() {
if (g_client) {
// Release resources associated to the client instance
rc_client_destroy(g_client);
g_client = NULL;
}
}
void ra_do_frame() {
if (!g_game_loaded)
return;
rc_client_do_frame(g_client);
}
void ra_do_idle() {
if (!g_game_loaded)
return;
rc_client_idle(g_client);
}
size_t ra_state_size() {
if (!g_game_loaded)
return 0;
return rc_client_progress_size(g_client);
}
void ra_save_state(uint8_t *state) {
if (!g_game_loaded)
return;
if (rc_client_serialize_progress(g_client, state) != RC_OK) {
printf("RetroAchievements: Failed to serialize progress\n");
}
}
void ra_load_state(const uint8_t *state) {
if (!g_game_loaded)
return;
if (rc_client_deserialize_progress(g_client, state) != RC_OK) {
printf("RetroAchievements: Failed to deserialize progress\n");
}
}
+23
View File
@@ -0,0 +1,23 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
void ra_init_client(bool hardcore);
void ra_shutdown_client();
bool ra_get_hardcore();
void ra_load_game(const uint8_t *rom, size_t rom_size);
void ra_set_dmem(uint8_t *dmem, size_t dmem_size);
void ra_do_frame();
void ra_do_idle();
void ra_http_callback(const char *content, size_t content_size, int status_code,
void *callback, void *callback_data);
void ra_logout_user();
void ra_login_user(const char *username, const char *password, void *userdata);
void ra_login_token_user(const char *username, const char *token,
void *userdata);
bool ra_is_user_logged_in();
const char *ra_get_username();
const char *ra_get_token();
size_t ra_state_size();
void ra_save_state(uint8_t *state);
void ra_load_state(const uint8_t *state);
+12 -5
View File
@@ -12,7 +12,7 @@ use std::arch::x86_64::*;
use rand::{Rng, SeedableRng};
use crate::{cheats, netplay, ui};
use crate::{cheats, netplay, retroachievements, ui};
use std::{collections::HashMap, fs, io::Read};
pub mod ai;
@@ -49,22 +49,27 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::g
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;
}
if let Some(slot) = game_settings.load_savestate_slot {
if let Some(slot) = game_settings.load_savestate_slot
&& !retroachievements::get_hardcore()
{
device.ui.storage.save_state_slot = slot;
device.load_state = true;
}
init_rng(device);
cart::rom::init(device, rom_contents); // cart needs to come before rdram
cart::rom::init(device, &rom_contents); // cart needs to come before rdram
// rdram pointer is shared with parallel-rdp
// rdram pointer is shared with parallel-rdp and retroachievements
rdram::init(device);
ui::video::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::input::init(&mut device.ui);
// shows messages, needs to be after video init
retroachievements::load_game(&rom_contents, rom_contents.len());
mi::init(device);
pif::init(device);
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
@@ -80,7 +85,9 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::g
ui::storage::init(&mut device.ui, &device.cart.rom);
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
cheats::init(device, game_settings.cheats);
if !retroachievements::get_hardcore() {
cheats::init(device, game_settings.cheats);
}
cpu::run(device);
+2 -2
View File
@@ -133,7 +133,7 @@ pub fn dma_write(
device::pi::calculate_cycles(device, 1, length)
}
pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
if let Some(netplay) = &mut device.netplay {
@@ -146,7 +146,7 @@ pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
device.cart.rom = rom_file;
device.cart.rom = rom_file.to_vec();
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
set_cic(device);
+3
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::retroachievements;
use crate::ui;
use std::alloc::{Layout, alloc_zeroed};
@@ -151,6 +152,8 @@ pub fn init(device: &mut device::Device) {
device.rdram.mem =
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
retroachievements::set_dmem(device.rdram.mem.as_mut_ptr(), device.rdram.size as usize);
// hack, skip RDRAM initialization
device
.rdram
+4 -2
View File
@@ -1,4 +1,4 @@
use crate::{cheats, device, netplay, ui};
use crate::{cheats, device, netplay, retroachievements, ui};
const VI_STATUS_REG: u32 = 0;
const VI_ORIGIN_REG: u32 = 1;
@@ -128,6 +128,8 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
retroachievements::do_frame();
ui::video::render_frame();
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
@@ -147,7 +149,7 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
ui::video::update_screen();
device.vi.vi_counter += 1;
if device.netplay.is_none() && paused {
if device.netplay.is_none() && !retroachievements::get_hardcore() && paused {
ui::video::pause_loop(device.vi.frame_time);
}
+39
View File
@@ -6,6 +6,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
@@ -82,6 +83,29 @@ struct Args {
help = "Load savestate from slot 0-9 when starting the game"
)]
load_state: Option<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
ra_username: Option<String>,
#[arg(
long = "ra-token",
value_name = "TOKEN",
help = "Token for RetroAchievements"
)]
ra_token: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
ra_password: Option<String>,
#[arg(
long = "ra-hardcore",
help = "Enable Hardcore mode for RetroAchievements"
)]
ra_hardcore: bool,
}
#[tokio::main(worker_threads = 4)]
@@ -134,6 +158,7 @@ async fn main() -> std::io::Result<()> {
.unwrap_or_default()
};
retroachievements::init_client(false); //args.ra_hardcore
let mut shutdown_tx = None;
if let Some(peer_addr) = args.netplay_peer_addr
@@ -157,6 +182,17 @@ async fn main() -> std::io::Result<()> {
if device.ui.config.emulation.usb {
(shutdown_tx, device.ui.usb) = ui::usb::init();
}
if let Some(username) = args.ra_username {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
if let Some(password) = args.ra_password {
retroachievements::login_user(username, password, tx);
} else if let Some(token) = args.ra_token {
retroachievements::login_token_user(username, token, tx);
}
rx.await.unwrap();
}
}
device::run_game(
@@ -169,6 +205,7 @@ async fn main() -> std::io::Result<()> {
load_savestate_slot: args.load_state,
},
);
retroachievements::shutdown_client();
if device.netplay.is_some() {
netplay::close(&mut device);
@@ -217,7 +254,9 @@ async fn main() -> std::io::Result<()> {
ui::input::bind_input_profile(&mut ui, profile, port);
}
} else {
retroachievements::init_client(false);
gui::app_window();
retroachievements::shutdown_client();
}
Ok(())
+304
View File
@@ -0,0 +1,304 @@
include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
use crate::ui;
use slint::ComponentHandle;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RAConfig {
pub username: String,
pub token: String,
pub enabled: bool,
pub hardcore: bool,
}
#[unsafe(no_mangle)]
pub extern "C" fn store_retroachievements_credentials(
c_username: *const std::ffi::c_char,
c_token: *const std::ffi::c_char,
ctx: *mut std::ffi::c_void,
) {
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
if c_username.is_null() || c_token.is_null() {
tx.send(false).unwrap();
return;
}
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())
{
RAConfig {
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
enabled: result.enabled,
hardcore: result.hardcore,
}
} else {
RAConfig {
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
enabled: false,
hardcore: false,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
tx.send(true).unwrap();
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_server_call(
c_url: *const std::ffi::c_char,
c_post_data: *const std::ffi::c_char,
c_callback: *mut std::ffi::c_void,
c_callback_data: *mut std::ffi::c_void,
) {
let url = unsafe { std::ffi::CStr::from_ptr(c_url).to_str().unwrap() };
let client = reqwest::Client::builder()
.user_agent(format!(
"{}/{}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
))
.build()
.unwrap();
let task = if !c_post_data.is_null() {
let post_data = unsafe { std::ffi::CStr::from_ptr(c_post_data).to_str().unwrap() };
let query_string =
reqwest::Url::parse(&format!("http://nothing.com?{}", post_data)).unwrap();
client
.post(url)
.query(
&query_string
.query_pairs()
.into_owned()
.collect::<Vec<(String, String)>>(),
)
.send()
} else {
client.get(url).send()
};
let callback = c_callback.addr();
let callback_data = c_callback_data.addr();
tokio::spawn(async move {
let response = task.await;
match response {
Ok(response) => {
let status = response.status().as_u16() as i32;
match response.text().await {
Ok(text) => {
let c_text = std::ffi::CString::new(text).unwrap();
unsafe {
ra_http_callback(
c_text.as_ptr(),
c_text.count_bytes(),
status,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
Err(error) => {
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
unsafe {
ra_http_callback(
c_error.as_ptr(),
c_error.count_bytes(),
status,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
}
}
Err(error) => {
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
unsafe {
ra_http_callback(
c_error.as_ptr(),
c_error.count_bytes(),
0,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
}
});
}
fn set_current_user_message(app: &ui::gui::AppWindow, rx: tokio::sync::oneshot::Receiver<bool>) {
app.set_ra_current_user_message("Logging in...".into());
let weak_app = app.as_weak();
tokio::spawn(async move {
rx.await.unwrap();
weak_app
.upgrade_in_event_loop(move |handle| {
if unsafe { ra_is_user_logged_in() } {
handle.set_ra_current_user_message(
format!("Logged in as {}", handle.get_ra_username()).into(),
);
} else {
handle.set_ra_current_user_message("Login failed".into());
}
handle.set_ra_logging_in(false);
})
.unwrap();
});
}
pub fn ra_window(app: &ui::gui::AppWindow) {
let mut token = String::new();
if let Ok(ra_config) = std::fs::read(ui::get_dirs().config_dir.join("retroachievements.json"))
&& let Ok(result) = serde_json::from_slice::<RAConfig>(ra_config.as_ref())
{
app.set_ra_username(result.username.into());
app.set_ra_enabled(result.enabled);
app.set_ra_hardcore(result.hardcore);
token = result.token;
} else {
app.set_ra_hardcore(true);
}
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
login_token_user(app.get_ra_username().to_string(), token, tx);
set_current_user_message(app, rx);
} else {
app.set_ra_current_user_message("Not currently logged in".into());
}
let weak_app2 = app.as_weak();
app.on_ra_button_clicked(move |password| {
weak_app2
.upgrade_in_event_loop(move |handle| {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
login_user(
handle.get_ra_username().to_string(),
password.to_string(),
tx,
);
set_current_user_message(&handle, rx);
})
.unwrap();
});
app.on_ra_toggled(move |enabled, hardcore| {
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())
{
if !enabled {
unsafe { ra_logout_user() };
RAConfig {
username: "".into(),
token: "".into(),
enabled,
hardcore,
}
} else {
RAConfig {
username: result.username,
token: result.token,
enabled,
hardcore,
}
}
} else {
RAConfig {
username: "".into(),
token: "".into(),
enabled,
hardcore,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
});
app.on_ra_games_clicked(move || {
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games").unwrap();
});
}
pub fn load_game(rom: &[u8], rom_size: usize) {
unsafe { ra_load_game(rom.as_ptr(), rom_size) };
}
pub fn set_dmem(dmem: *mut u8, dmem_size: usize) {
unsafe { ra_set_dmem(dmem, dmem_size) };
}
pub fn do_frame() {
unsafe { ra_do_frame() };
}
pub fn do_idle() {
unsafe { ra_do_idle() };
}
pub fn init_client(hardcore: bool) {
unsafe { ra_init_client(hardcore) };
}
pub fn shutdown_client() {
unsafe { ra_shutdown_client() };
}
pub fn get_hardcore() -> bool {
unsafe { ra_get_hardcore() }
}
pub fn login_user(username: String, password: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
ra_login_user(
std::ffi::CString::new(username).unwrap().as_ptr(),
std::ffi::CString::new(password).unwrap().as_ptr(),
tx_ptr,
)
};
}
pub fn login_token_user(username: String, token: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
ra_login_token_user(
std::ffi::CString::new(username).unwrap().as_ptr(),
std::ffi::CString::new(token).unwrap().as_ptr(),
tx_ptr,
)
};
}
pub fn is_user_logged_in() -> bool {
unsafe { ra_is_user_logged_in() }
}
pub fn get_username() -> &'static str {
unsafe {
std::ffi::CStr::from_ptr(ra_get_username())
.to_str()
.unwrap()
}
}
pub fn get_token() -> &'static str {
unsafe { std::ffi::CStr::from_ptr(ra_get_token()).to_str().unwrap() }
}
pub fn state_size() -> usize {
unsafe { ra_state_size() }
}
pub fn save_state(state: *mut u8) {
unsafe { ra_save_state(state) };
}
pub fn load_state(state: *const u8) {
unsafe { ra_load_state(state) };
}
+8 -1
View File
@@ -1,4 +1,4 @@
use crate::{cheats, device, ui};
use crate::{cheats, device, retroachievements, ui};
#[cfg(target_arch = "aarch64")]
use device::__m128i;
use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
@@ -73,6 +73,9 @@ pub fn create_savestate(device: &device::Device) {
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
ui::video::save_state(rdp_state.as_mut_ptr());
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr());
let data: &[(&[u8], &str)] = &[
(&postcard::to_stdvec(device).unwrap(), "device"),
(
@@ -80,6 +83,7 @@ pub fn create_savestate(device: &device::Device) {
"saves",
),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
];
let save_path = device.ui.storage.paths.savestate_file_path.clone();
let compressed_file = ui::storage::compress_file(data);
@@ -101,6 +105,7 @@ pub fn load_savestate(device: &mut device::Device) {
let device_bytes = ui::storage::decompress_file(savestate, "device");
let save_bytes = ui::storage::decompress_file(savestate, "saves");
let rdp_state = ui::storage::decompress_file(savestate, "rdp_state");
let ra_state = ui::storage::decompress_file(savestate, "ra_state");
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
@@ -198,6 +203,8 @@ pub fn load_savestate(device: &mut device::Device) {
ui::audio::init_game_audio(&mut device.ui, device.ai.freq);
ui::video::load_state(device, rdp_state.as_ptr());
retroachievements::load_state(ra_state.as_ptr());
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
+15
View File
@@ -1,3 +1,4 @@
use crate::retroachievements;
use crate::ui;
use slint::Model;
@@ -299,6 +300,7 @@ fn about_window(app: &AppWindow) {
pub fn app_window() {
let app = AppWindow::new().unwrap();
about_window(&app);
retroachievements::ra_window(&app);
let mut controller_paths;
{
let game_ui = ui::Ui::new();
@@ -321,6 +323,7 @@ pub fn run_rom(
file_path: std::path::PathBuf,
game_settings: GameSettings,
netplay: Option<NetplayDevice>,
ra_hardcore: bool,
weak: slint::Weak<AppWindow>,
) {
tokio::spawn(async move {
@@ -347,6 +350,16 @@ pub fn run_rom(
"--cheats",
cheats_path.to_str().unwrap(),
]);
} else if retroachievements::is_user_logged_in() {
command.args([
"--ra-username",
retroachievements::get_username(),
"--ra-token",
retroachievements::get_token(),
]);
if ra_hardcore {
command.args(["--ra-hardcore"]);
}
}
for i in 0..4 {
@@ -409,6 +422,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 weak = app.as_weak();
tokio::spawn(async move {
@@ -439,6 +453,7 @@ fn open_rom(app: &AppWindow) {
load_savestate_slot: None,
},
None,
ra_hardcore,
weak,
);
}
+25 -3
View File
@@ -2,9 +2,10 @@
// SPDX-License-Identifier: MIT
import { StandardButton } from "std-widgets.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, RetroAchievements, Settings, About } from "pages.slint";
import { SideBar } from "side_bar.slint";
import { SettingsData } from "settings_page.slint";
import { RAData } from "retroachievements_page.slint";
import { AboutData } from "about_page.slint";
import { ControllerData } from "controller_page.slint";
import { State } from "localgame_page.slint";
@@ -34,8 +35,17 @@ export component AppWindow inherits Window {
callback cheats_select_rom_clicked(string);
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
callback ra_button_clicked(string);
callback ra_games_clicked;
callback ra_toggled(bool,bool);
in property version <=> AboutData.version;
in property has_update <=> AboutData.has_update;
in-out property ra_username <=> RAData.ra_username;
in-out property ra_password <=> RAData.ra_password;
in-out property ra_logging_in <=> RAData.ra_logging_in;
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 integer_scaling <=> SettingsData.integer_scaling;
in-out property fullscreen <=> SettingsData.fullscreen;
in-out property widescreen <=> SettingsData.widescreen;
@@ -71,6 +81,7 @@ export component AppWindow inherits Window {
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "RetroAchievements"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
@@ -117,8 +128,19 @@ export component AppWindow inherits Window {
cheat_toggled(game_crc,cheat_name,option,enabled);
}
}
if(side-bar.current-item == 4): Settings { }
if(side-bar.current-item == 5): About {
if(side-bar.current-item == 4): RetroAchievements {
ra_button_clicked(password) => {
ra_button_clicked(password);
}
ra_toggled(enabled,hardcore) => {
ra_toggled(enabled,hardcore);
}
ra_games_clicked => {
ra_games_clicked();
}
}
if(side-bar.current-item == 5): Settings { }
if(side-bar.current-item == 6): About {
wiki_button_clicked => {
wiki_button_clicked();
}
+1
View File
@@ -2,5 +2,6 @@ export { LocalGame } from "localgame_page.slint";
export { ControllerConfig } from "controller_page.slint";
export { Netplay } from "netplay_page.slint";
export { Cheats } from "cheat_page.slint";
export { RetroAchievements } from "retroachievements_page.slint";
export { Settings } from "settings_page.slint";
export { About } from "about_page.slint";
+105
View File
@@ -0,0 +1,105 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { LineEdit, CheckBox, Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global RAData {
in-out property <bool> enabled;
in-out property <bool> hardcore;
in-out property <bool> ra_logging_in;
in-out property <string> ra_username;
in-out property <string> ra_password;
in-out property <string> current_user_message;
}
export component RetroAchievements inherits Page {
callback ra_button_clicked(string);
callback ra_toggled(bool,bool);
callback ra_games_clicked;
title: @tr("RetroAchievements");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: center;
CheckBox {
text: @tr("Enabled");
checked: RAData.enabled;
toggled => {
RAData.enabled = self.checked;
if (!RAData.enabled) {
RAData.ra_username = "";
username_widget.text = "";
password_widget.text = "";
RAData.current_user_message = "Not currently logged in";
}
ra_toggled(RAData.enabled,RAData.hardcore);
}
}
}
HorizontalBox {
alignment: center;
CheckBox {
text: @tr("Hardcore mode (not currently supported)");
checked: RAData.hardcore;
enabled: RAData.enabled;
toggled => {
RAData.hardcore = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore);
}
}
}
HorizontalBox {
alignment: center;
Text {
text: RAData.current_user_message;
}
}
HorizontalBox {
alignment: center;
username_widget := LineEdit {
enabled: RAData.enabled;
placeholder-text: @tr("Username");
text: RAData.ra_username;
edited(text) => {
RAData.ra_username = text;
}
}
password_widget := LineEdit {
enabled: RAData.enabled;
input-type: password;
placeholder-text: @tr("Password");
edited(text) => {
RAData.ra_password = text;
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Login");
enabled: RAData.enabled && !RAData.ra_logging_in && RAData.ra_username != "" && RAData.ra_password != "";
clicked => {
RAData.ra_logging_in = true;
ra_button_clicked(RAData.ra_password);
RAData.ra_password = "";
password_widget.text = "";
}
}
}
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: center;
Button {
text: @tr("View Supported Games");
clicked => {
ra_games_clicked();
}
}
}
}
}
+3 -3
View File
@@ -121,10 +121,9 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
.unwrap();
broadcast_sock.set_broadcast(true).unwrap();
let data: [u8; 1] = [1];
broadcast_sock
let _ = broadcast_sock
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.await
.unwrap();
.await;
let mut buffer = [0; 1024];
if let Ok(Ok(result)) = tokio::time::timeout(
std::time::Duration::from_millis(200),
@@ -1108,6 +1107,7 @@ fn setup_wait_window(
peer_addr: socket_addr,
player_number: player_number as u8,
}),
false,
weak_app,
);
})
+4 -3
View File
@@ -1,6 +1,6 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
use crate::{device, ui};
use crate::{device, retroachievements, ui};
pub fn init(device: &mut device::Device) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
@@ -140,6 +140,7 @@ pub fn pause_loop(frame_time: f64) {
while paused && !frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
retroachievements::do_idle();
let callback = unsafe { rdp_check_callback() };
paused = callback.paused;
frame_advance = callback.frame_advance;
@@ -153,7 +154,7 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
if device.netplay.is_none() {
if callback.save_state {
device.save_state = true;
} else if callback.load_state {
} else if callback.load_state && !retroachievements::get_hardcore() {
device.load_state = true;
}
if callback.reset_game {
@@ -175,7 +176,7 @@ pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
}
if device.ui.storage.save_state_slot != callback.save_state_slot {
ui::video::onscreen_message(
onscreen_message(
&device.ui,
&format!("Switching savestate slot to {}", callback.save_state_slot,),
);