#include "retroachievements.h" #include "../parallel-rdp/interface.hpp" #include #include #include #include #include void rust_server_call(const char *url, const char *post_data, const char *content_type, rc_client_server_callback_t callback, void *callback_data); void store_retroachievements_credentials(const char *username, const char *token, void *userdata); void notify_load_game(void *userdata); static rc_client_t *g_client = NULL; static const uint8_t *g_rdram = NULL; static size_t g_rdram_size = 0; static bool g_challenge = false; static bool g_leaderboard = false; static rc_client_leaderboard_list_t *g_leaderboard_list = NULL; static char g_rich_presence_buffer[512]; static uint32_t read_memory(uint32_t address, uint8_t *buffer, uint32_t num_bytes, rc_client_t *client) { if (address + num_bytes > g_rdram_size) memset(buffer, 0, num_bytes); else memcpy(buffer, &g_rdram[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, request->content_type, 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) { if (client && !rc_client_get_userdata(client)) rc_client_set_userdata(client, strdup(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); } const char *ra_get_username() { const rc_client_user_t *user = rc_client_get_user_info(g_client); return user ? user->username : NULL; } void ra_logout_user() { 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. 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. 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) { if (client && !rc_client_get_userdata(client)) rc_client_set_userdata(client, strdup(error_message)); rc_client_set_hardcore_enabled(client, false); notify_load_game(userdata); return; } if (!rc_client_is_processing_required(client)) { rc_client_set_hardcore_enabled(client, false); } g_leaderboard_list = rc_client_create_leaderboard_list( client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); notify_load_game(userdata); } void ra_welcome(const char **game_title, const char **game_image_url) { if (!g_client) { *game_title = NULL; *game_image_url = NULL; return; } char buffer[512]; char *error_message = rc_client_get_userdata(g_client); if (error_message) { snprintf(buffer, sizeof(buffer), "RA Error: %s", error_message); rdp_onscreen_message(buffer, MESSAGE_LONG); free(error_message); rc_client_set_userdata(g_client, NULL); *game_title = NULL; *game_image_url = NULL; return; } const rc_client_game_t *game = rc_client_get_game_info(g_client); if (!game) { *game_title = NULL; *game_image_url = NULL; return; } rc_client_user_game_summary_t summary; rc_client_get_user_game_summary(g_client, &summary); int message_length = snprintf( buffer, sizeof(buffer), "%s\nMode: %s\n", game->title, rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Casual"); if (summary.num_core_achievements != 0) { snprintf(buffer + message_length, sizeof(buffer) - message_length, "%u/%u achievements unlocked", summary.num_unlocked_achievements, summary.num_core_achievements); } else { snprintf(buffer + message_length, sizeof(buffer) - message_length, "Game has no achievements"); } rdp_onscreen_message(buffer, MESSAGE_LONG); *game_title = game->title; *game_image_url = game->badge_url; } void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) { rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO_64, NULL, rom, rom_size, load_game_callback, userdata); } void ra_unload_game() { if (g_leaderboard_list) { rc_client_destroy_leaderboard_list(g_leaderboard_list); g_leaderboard_list = NULL; } rc_client_unload_game(g_client); } void ra_set_rdram(const uint8_t *rdram, size_t rdram_size) { g_rdram = rdram; g_rdram_size = rdram_size; } static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) { char buffer[512]; snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s", leaderboard->title, leaderboard->tracker_value); rdp_onscreen_message(buffer, MESSAGE_SHORT); } static void achievement_triggered(const rc_client_achievement_t *achievement) { char buffer[512]; snprintf(buffer, sizeof(buffer), "Unlocked: %s", achievement->title); rdp_onscreen_message(buffer, MESSAGE_SHORT); } 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), "%s: %s", rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed", game->title); rdp_onscreen_message(buffer, MESSAGE_SHORT); } static void subset_completed(const rc_client_subset_t *subset, rc_client_t *client) { char buffer[512]; snprintf(buffer, sizeof(buffer), "Subset %s: %s", rc_client_get_hardcore_enabled(client) ? "mastered" : "completed", subset->title); rdp_onscreen_message(buffer, MESSAGE_SHORT); } static void server_error(const rc_client_server_error_t *server_error) { char buffer[512]; snprintf(buffer, sizeof(buffer), "RA server error: %s", server_error->error_message); rdp_onscreen_message(buffer, MESSAGE_SHORT); } static const char *get_leaderboard_title(const char *display) { if (g_leaderboard_list == NULL) return NULL; for (uint32_t i = 0; i < g_leaderboard_list->num_buckets; i++) { for (uint32_t j = 0; j < g_leaderboard_list->buckets[i].num_leaderboards; j++) { // this looks like a mistake, but it is intentional. // Leaderboard trackers don't contain the title, // but we take advantage of the fact that tracker_value and display point // to the same char* to find a matching leaderboard. if (g_leaderboard_list->buckets[i].leaderboards[j]->tracker_value == display) { return g_leaderboard_list->buckets[i].leaderboards[j]->title; } } } return NULL; } 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: if (g_leaderboard) leaderboard_submitted(event->leaderboard); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: if (g_challenge) achievement_challenge_indicator_add(event->achievement->title); break; case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: if (g_challenge) achievement_challenge_indicator_remove(event->achievement->title); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: achievement_progress_add(event->achievement->title, event->achievement->measured_progress); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: achievement_progress_remove(); break; case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: achievement_progress_add(event->achievement->title, event->achievement->measured_progress); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: if (g_leaderboard) leaderboard_tracker_add( event->leaderboard_tracker->id, get_leaderboard_title(event->leaderboard_tracker->display), event->leaderboard_tracker->display); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: if (g_leaderboard) leaderboard_tracker_remove(event->leaderboard_tracker->id); break; case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: if (g_leaderboard) leaderboard_tracker_add( event->leaderboard_tracker->id, get_leaderboard_title(event->leaderboard_tracker->display), event->leaderboard_tracker->display); 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; case RC_CLIENT_EVENT_SERVER_ERROR: server_error(event->server_error); break; default: printf("RetroAchievements: Unhandled event %d\n", event->type); break; } } void ra_init_client(bool hardcore, bool challenge, bool leaderboard) { // Create the client instance (using a global variable simplifies this // example) if (!g_client) g_client = rc_client_create(read_memory, server_call); g_rdram = NULL; g_rdram_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); g_challenge = challenge; g_leaderboard = leaderboard; } bool ra_get_hardcore() { 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() { rc_client_do_frame(g_client); } void ra_do_idle() { rc_client_idle(g_client); } const char *ra_get_rich_presence() { size_t size = rc_client_get_rich_presence_message( g_client, g_rich_presence_buffer, sizeof(g_rich_presence_buffer)); if (size == 0) { return NULL; } return g_rich_presence_buffer; } size_t ra_state_size() { return rc_client_progress_size(g_client); } void ra_save_state(uint8_t *state, size_t state_size) { if (!rc_client_is_game_loaded(g_client)) return; if (rc_client_serialize_progress_sized(g_client, state, state_size) != RC_OK) { printf("RetroAchievements: Failed to serialize progress\n"); } } void ra_load_state(const uint8_t *state, size_t state_size) { if (!rc_client_is_game_loaded(g_client)) return; if (rc_client_deserialize_progress_sized(g_client, state, state_size) != RC_OK) { printf("RetroAchievements: Failed to deserialize progress\n"); } } void ra_display_inprogress_achievements(void *userdata) { if (!rc_client_is_game_loaded(g_client)) return; rc_client_achievement_list_t *list = rc_client_create_achievement_list( g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); char buffer[1024] = {0}; size_t buffer_length = 0; for (uint32_t i = 0; i < list->num_buckets; i++) { if (list->buckets[i].bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED) { for (uint32_t j = 0; j < list->buckets[i].num_achievements; j++) { const rc_client_achievement_t *achievement = list->buckets[i].achievements[j]; if (achievement->measured_percent > 0.0f) { if (buffer_length < sizeof(buffer) - 1) { buffer_length += snprintf( buffer + buffer_length, sizeof(buffer) - buffer_length, "%s: %s\n", achievement->title, achievement->measured_progress); } } } } } if (buffer_length > 0) { rdp_onscreen_message(buffer, MESSAGE_SHORT); } rc_client_destroy_achievement_list(list); }