This repository has been archived on 2026-08-27. You can view files and clone it, but cannot push or open issues or pull requests.
gopher64-netplay/src/retroachievements.rs
Logan McNaughton a1da699580
add RA show profile button (#719)
* add RA show profile button

* lint
2026-04-09 09:37:57 +02:00

311 lines
9.9 KiB
Rust

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 is_user_logged_in() {
handle.set_ra_current_user_message(
format!("Logged in as {}", handle.get_ra_username()).into(),
);
handle.set_ra_show_profile(true);
} else {
handle.set_ra_current_user_message("Login failed".into());
handle.set_ra_show_profile(false);
}
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();
});
app.on_ra_show_profile_clicked(move |username| {
open::that_detached(format!("https://retroachievements.org/user/{}", username)).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) };
}