mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 01:25:20 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Submodule
+1
Submodule retroachievements/rcheevos added at e9ca3694c8
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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,),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user