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
|
path = src/compat/sse2neon
|
||||||
url = https://github.com/DLTcollab/sse2neon.git
|
url = https://github.com/DLTcollab/sse2neon.git
|
||||||
branch = master
|
branch = master
|
||||||
|
[submodule "retroachievements/rcheevos"]
|
||||||
|
path = retroachievements/rcheevos
|
||||||
|
url = https://github.com/RetroAchievements/rcheevos.git
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo::rerun-if-changed=parallel-rdp");
|
println!("cargo::rerun-if-changed=parallel-rdp");
|
||||||
|
println!("cargo::rerun-if-changed=retroachievements");
|
||||||
println!("cargo::rerun-if-changed=src/compat");
|
println!("cargo::rerun-if-changed=src/compat");
|
||||||
|
|
||||||
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
|
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
|
||||||
@@ -69,6 +70,41 @@ fn main() {
|
|||||||
.join("include"),
|
.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 os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||||
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||||
let opt_flag = if arch == "x86_64" {
|
let opt_flag = if arch == "x86_64" {
|
||||||
@@ -84,6 +120,7 @@ fn main() {
|
|||||||
volk_build.flag(opt_flag);
|
volk_build.flag(opt_flag);
|
||||||
rdp_build.flag(opt_flag);
|
rdp_build.flag(opt_flag);
|
||||||
simd_build.flag(opt_flag);
|
simd_build.flag(opt_flag);
|
||||||
|
retroachievements_build.flag(opt_flag);
|
||||||
|
|
||||||
if os == "windows" {
|
if os == "windows" {
|
||||||
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
|
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
|
||||||
@@ -111,12 +148,41 @@ fn main() {
|
|||||||
volk_build.flag("-flto=thin");
|
volk_build.flag("-flto=thin");
|
||||||
rdp_build.flag("-flto=thin");
|
rdp_build.flag("-flto=thin");
|
||||||
simd_build.flag("-flto=thin");
|
simd_build.flag("-flto=thin");
|
||||||
|
retroachievements_build.flag("-flto=thin");
|
||||||
|
|
||||||
volk_build.compile("volk");
|
volk_build.compile("volk");
|
||||||
rdp_build.compile("parallel-rdp");
|
rdp_build.compile("parallel-rdp");
|
||||||
|
retroachievements_build.compile("retroachievements");
|
||||||
|
|
||||||
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
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()
|
let parallel_bindings = bindgen::Builder::default()
|
||||||
.header("parallel-rdp/interface.hpp")
|
.header("parallel-rdp/interface.hpp")
|
||||||
.allowlist_function("rdp_init")
|
.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 rand::{Rng, SeedableRng};
|
||||||
|
|
||||||
use crate::{cheats, netplay, ui};
|
use crate::{cheats, netplay, retroachievements, ui};
|
||||||
use std::{collections::HashMap, fs, io::Read};
|
use std::{collections::HashMap, fs, io::Read};
|
||||||
|
|
||||||
pub mod ai;
|
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 {
|
if game_settings.disable_expansion_pak {
|
||||||
device.rdram.size = 0x400000;
|
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.ui.storage.save_state_slot = slot;
|
||||||
device.load_state = true;
|
device.load_state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
init_rng(device);
|
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);
|
rdram::init(device);
|
||||||
|
|
||||||
ui::video::init(device);
|
ui::video::init(device);
|
||||||
ui::audio::init(&mut device.ui, device.ai.freq);
|
ui::audio::init(&mut device.ui, device.ai.freq);
|
||||||
ui::input::init(&mut device.ui);
|
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);
|
mi::init(device);
|
||||||
pif::init(device);
|
pif::init(device);
|
||||||
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
|
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::init(&mut device.ui, &device.cart.rom);
|
||||||
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
|
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);
|
cpu::run(device);
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ pub fn dma_write(
|
|||||||
device::pi::calculate_cycles(device, 1, length)
|
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();
|
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
|
||||||
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
|
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
|
||||||
if let Some(netplay) = &mut device.netplay {
|
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.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]);
|
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
|
||||||
set_cic(device);
|
set_cic(device);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::device;
|
use crate::device;
|
||||||
|
use crate::retroachievements;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use std::alloc::{Layout, alloc_zeroed};
|
use std::alloc::{Layout, alloc_zeroed};
|
||||||
|
|
||||||
@@ -151,6 +152,8 @@ pub fn init(device: &mut device::Device) {
|
|||||||
device.rdram.mem =
|
device.rdram.mem =
|
||||||
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
|
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
|
// hack, skip RDRAM initialization
|
||||||
device
|
device
|
||||||
.rdram
|
.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_STATUS_REG: u32 = 0;
|
||||||
const VI_ORIGIN_REG: u32 = 1;
|
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());
|
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retroachievements::do_frame();
|
||||||
|
|
||||||
ui::video::render_frame();
|
ui::video::render_frame();
|
||||||
|
|
||||||
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
|
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();
|
ui::video::update_screen();
|
||||||
device.vi.vi_counter += 1;
|
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);
|
ui::video::pause_loop(device.vi.frame_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
@@ -6,6 +6,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|||||||
mod cheats;
|
mod cheats;
|
||||||
mod device;
|
mod device;
|
||||||
mod netplay;
|
mod netplay;
|
||||||
|
mod retroachievements;
|
||||||
mod savestates;
|
mod savestates;
|
||||||
mod ui;
|
mod ui;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -82,6 +83,29 @@ struct Args {
|
|||||||
help = "Load savestate from slot 0-9 when starting the game"
|
help = "Load savestate from slot 0-9 when starting the game"
|
||||||
)]
|
)]
|
||||||
load_state: Option<u32>,
|
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)]
|
#[tokio::main(worker_threads = 4)]
|
||||||
@@ -134,6 +158,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
retroachievements::init_client(false); //args.ra_hardcore
|
||||||
let mut shutdown_tx = None;
|
let mut shutdown_tx = None;
|
||||||
|
|
||||||
if let Some(peer_addr) = args.netplay_peer_addr
|
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 {
|
if device.ui.config.emulation.usb {
|
||||||
(shutdown_tx, device.ui.usb) = ui::usb::init();
|
(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(
|
device::run_game(
|
||||||
@@ -169,6 +205,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
load_savestate_slot: args.load_state,
|
load_savestate_slot: args.load_state,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
retroachievements::shutdown_client();
|
||||||
|
|
||||||
if device.netplay.is_some() {
|
if device.netplay.is_some() {
|
||||||
netplay::close(&mut device);
|
netplay::close(&mut device);
|
||||||
@@ -217,7 +254,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
ui::input::bind_input_profile(&mut ui, profile, port);
|
ui::input::bind_input_profile(&mut ui, profile, port);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
retroachievements::init_client(false);
|
||||||
gui::app_window();
|
gui::app_window();
|
||||||
|
retroachievements::shutdown_client();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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")]
|
#[cfg(target_arch = "aarch64")]
|
||||||
use device::__m128i;
|
use device::__m128i;
|
||||||
use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
|
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()];
|
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
|
||||||
ui::video::save_state(rdp_state.as_mut_ptr());
|
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)] = &[
|
let data: &[(&[u8], &str)] = &[
|
||||||
(&postcard::to_stdvec(device).unwrap(), "device"),
|
(&postcard::to_stdvec(device).unwrap(), "device"),
|
||||||
(
|
(
|
||||||
@@ -80,6 +83,7 @@ pub fn create_savestate(device: &device::Device) {
|
|||||||
"saves",
|
"saves",
|
||||||
),
|
),
|
||||||
(&rdp_state, "rdp_state"),
|
(&rdp_state, "rdp_state"),
|
||||||
|
(&ra_state, "ra_state"),
|
||||||
];
|
];
|
||||||
let save_path = device.ui.storage.paths.savestate_file_path.clone();
|
let save_path = device.ui.storage.paths.savestate_file_path.clone();
|
||||||
let compressed_file = ui::storage::compress_file(data);
|
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 device_bytes = ui::storage::decompress_file(savestate, "device");
|
||||||
let save_bytes = ui::storage::decompress_file(savestate, "saves");
|
let save_bytes = ui::storage::decompress_file(savestate, "saves");
|
||||||
let rdp_state = ui::storage::decompress_file(savestate, "rdp_state");
|
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) {
|
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
|
||||||
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
|
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::audio::init_game_audio(&mut device.ui, device.ai.freq);
|
||||||
ui::video::load_state(device, rdp_state.as_ptr());
|
ui::video::load_state(device, rdp_state.as_ptr());
|
||||||
|
|
||||||
|
retroachievements::load_state(ra_state.as_ptr());
|
||||||
|
|
||||||
if device.cheats.enabled {
|
if device.cheats.enabled {
|
||||||
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
cheats::execute_cheats(device, device.cheats.cheats.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::retroachievements;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use slint::Model;
|
use slint::Model;
|
||||||
|
|
||||||
@@ -299,6 +300,7 @@ fn about_window(app: &AppWindow) {
|
|||||||
pub fn app_window() {
|
pub fn app_window() {
|
||||||
let app = AppWindow::new().unwrap();
|
let app = AppWindow::new().unwrap();
|
||||||
about_window(&app);
|
about_window(&app);
|
||||||
|
retroachievements::ra_window(&app);
|
||||||
let mut controller_paths;
|
let mut controller_paths;
|
||||||
{
|
{
|
||||||
let game_ui = ui::Ui::new();
|
let game_ui = ui::Ui::new();
|
||||||
@@ -321,6 +323,7 @@ pub fn run_rom(
|
|||||||
file_path: std::path::PathBuf,
|
file_path: std::path::PathBuf,
|
||||||
game_settings: GameSettings,
|
game_settings: GameSettings,
|
||||||
netplay: Option<NetplayDevice>,
|
netplay: Option<NetplayDevice>,
|
||||||
|
ra_hardcore: bool,
|
||||||
weak: slint::Weak<AppWindow>,
|
weak: slint::Weak<AppWindow>,
|
||||||
) {
|
) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -347,6 +350,16 @@ pub fn run_rom(
|
|||||||
"--cheats",
|
"--cheats",
|
||||||
cheats_path.to_str().unwrap(),
|
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 {
|
for i in 0..4 {
|
||||||
@@ -409,6 +422,7 @@ fn open_rom(app: &AppWindow) {
|
|||||||
|
|
||||||
let overclock = app.get_overclock_n64_cpu();
|
let overclock = app.get_overclock_n64_cpu();
|
||||||
let disable_expansion_pak = app.get_disable_expansion_pak();
|
let disable_expansion_pak = app.get_disable_expansion_pak();
|
||||||
|
let ra_hardcore = app.get_ra_hardcore();
|
||||||
|
|
||||||
let weak = app.as_weak();
|
let weak = app.as_weak();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -439,6 +453,7 @@ fn open_rom(app: &AppWindow) {
|
|||||||
load_savestate_slot: None,
|
load_savestate_slot: None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
ra_hardcore,
|
||||||
weak,
|
weak,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { StandardButton } from "std-widgets.slint";
|
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 { SideBar } from "side_bar.slint";
|
||||||
import { SettingsData } from "settings_page.slint";
|
import { SettingsData } from "settings_page.slint";
|
||||||
|
import { RAData } from "retroachievements_page.slint";
|
||||||
import { AboutData } from "about_page.slint";
|
import { AboutData } from "about_page.slint";
|
||||||
import { ControllerData } from "controller_page.slint";
|
import { ControllerData } from "controller_page.slint";
|
||||||
import { State } from "localgame_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_select_rom_clicked(string);
|
||||||
callback cheats_clear_clicked;
|
callback cheats_clear_clicked;
|
||||||
callback cheat_toggled(string, string, string, bool);
|
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 version <=> AboutData.version;
|
||||||
in property has_update <=> AboutData.has_update;
|
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 integer_scaling <=> SettingsData.integer_scaling;
|
||||||
in-out property fullscreen <=> SettingsData.fullscreen;
|
in-out property fullscreen <=> SettingsData.fullscreen;
|
||||||
in-out property widescreen <=> SettingsData.widescreen;
|
in-out property widescreen <=> SettingsData.widescreen;
|
||||||
@@ -71,6 +81,7 @@ export component AppWindow inherits Window {
|
|||||||
@tr("Menu" => "Controller Config"),
|
@tr("Menu" => "Controller Config"),
|
||||||
@tr("Menu" => "Netplay"),
|
@tr("Menu" => "Netplay"),
|
||||||
@tr("Menu" => "Cheats"),
|
@tr("Menu" => "Cheats"),
|
||||||
|
@tr("Menu" => "RetroAchievements"),
|
||||||
@tr("Menu" => "Settings"),
|
@tr("Menu" => "Settings"),
|
||||||
@tr("Menu" => "About")
|
@tr("Menu" => "About")
|
||||||
];
|
];
|
||||||
@@ -117,8 +128,19 @@ export component AppWindow inherits Window {
|
|||||||
cheat_toggled(game_crc,cheat_name,option,enabled);
|
cheat_toggled(game_crc,cheat_name,option,enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(side-bar.current-item == 4): Settings { }
|
if(side-bar.current-item == 4): RetroAchievements {
|
||||||
if(side-bar.current-item == 5): About {
|
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 => {
|
||||||
wiki_button_clicked();
|
wiki_button_clicked();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export { LocalGame } from "localgame_page.slint";
|
|||||||
export { ControllerConfig } from "controller_page.slint";
|
export { ControllerConfig } from "controller_page.slint";
|
||||||
export { Netplay } from "netplay_page.slint";
|
export { Netplay } from "netplay_page.slint";
|
||||||
export { Cheats } from "cheat_page.slint";
|
export { Cheats } from "cheat_page.slint";
|
||||||
|
export { RetroAchievements } from "retroachievements_page.slint";
|
||||||
export { Settings } from "settings_page.slint";
|
export { Settings } from "settings_page.slint";
|
||||||
export { About } from "about_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();
|
.unwrap();
|
||||||
broadcast_sock.set_broadcast(true).unwrap();
|
broadcast_sock.set_broadcast(true).unwrap();
|
||||||
let data: [u8; 1] = [1];
|
let data: [u8; 1] = [1];
|
||||||
broadcast_sock
|
let _ = broadcast_sock
|
||||||
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
|
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
|
||||||
.await
|
.await;
|
||||||
.unwrap();
|
|
||||||
let mut buffer = [0; 1024];
|
let mut buffer = [0; 1024];
|
||||||
if let Ok(Ok(result)) = tokio::time::timeout(
|
if let Ok(Ok(result)) = tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(200),
|
std::time::Duration::from_millis(200),
|
||||||
@@ -1108,6 +1107,7 @@ fn setup_wait_window(
|
|||||||
peer_addr: socket_addr,
|
peer_addr: socket_addr,
|
||||||
player_number: player_number as u8,
|
player_number: player_number as u8,
|
||||||
}),
|
}),
|
||||||
|
false,
|
||||||
weak_app,
|
weak_app,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
|
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
|
||||||
use crate::{device, ui};
|
use crate::{device, retroachievements, ui};
|
||||||
|
|
||||||
pub fn init(device: &mut device::Device) {
|
pub fn init(device: &mut device::Device) {
|
||||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||||
@@ -140,6 +140,7 @@ pub fn pause_loop(frame_time: f64) {
|
|||||||
while paused && !frame_advance {
|
while paused && !frame_advance {
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
|
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
|
||||||
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
unsafe { sdl3_sys::events::SDL_PumpEvents() };
|
||||||
|
retroachievements::do_idle();
|
||||||
let callback = unsafe { rdp_check_callback() };
|
let callback = unsafe { rdp_check_callback() };
|
||||||
paused = callback.paused;
|
paused = callback.paused;
|
||||||
frame_advance = callback.frame_advance;
|
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 device.netplay.is_none() {
|
||||||
if callback.save_state {
|
if callback.save_state {
|
||||||
device.save_state = true;
|
device.save_state = true;
|
||||||
} else if callback.load_state {
|
} else if callback.load_state && !retroachievements::get_hardcore() {
|
||||||
device.load_state = true;
|
device.load_state = true;
|
||||||
}
|
}
|
||||||
if callback.reset_game {
|
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 {
|
if device.ui.storage.save_state_slot != callback.save_state_slot {
|
||||||
ui::video::onscreen_message(
|
onscreen_message(
|
||||||
&device.ui,
|
&device.ui,
|
||||||
&format!("Switching savestate slot to {}", callback.save_state_slot,),
|
&format!("Switching savestate slot to {}", callback.save_state_slot,),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user