diff --git a/CMakeLists.txt b/CMakeLists.txt index ed315bc8b..1d9660803 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,39 @@ if(USE_SDL) include_directories(${SDL2_INCLUDE_DIRS}) endif() +set(USE_RETRO_ACHIEVEMENTS 1) +if(USE_RETRO_ACHIEVEMENTS) + set(RCHEEVOS_SRC src/rcheevos/src/rapi/rc_api_common.c + src/rcheevos/src/rapi/rc_api_editor.c + src/rcheevos/src/rapi/rc_api_info.c + src/rcheevos/src/rapi/rc_api_runtime.c + src/rcheevos/src/rapi/rc_api_user.c + src/rcheevos/src/rcheevos/alloc.c + src/rcheevos/src/rcheevos/compat.c + src/rcheevos/src/rcheevos/condition.c + src/rcheevos/src/rcheevos/condset.c + src/rcheevos/src/rcheevos/consoleinfo.c + src/rcheevos/src/rcheevos/format.c + src/rcheevos/src/rcheevos/lboard.c + src/rcheevos/src/rcheevos/memref.c + src/rcheevos/src/rcheevos/operand.c + src/rcheevos/src/rcheevos/rc_client.c + src/rcheevos/src/rcheevos/rc_validate.c + src/rcheevos/src/rcheevos/richpresence.c + src/rcheevos/src/rcheevos/runtime.c + src/rcheevos/src/rcheevos/runtime_progress.c + src/rcheevos/src/rcheevos/trigger.c + src/rcheevos/src/rcheevos/value.c + src/rcheevos/src/rhash/cdreader.c + src/rcheevos/src/rhash/hash.c + src/rcheevos/src/rhash/md5.c + src/rcheevos/src/rurl/url.c + ) + set(SKYEMU_SRC ${SKYEMU_SRC} ${RCHEEVOS_SRC} src/retro_achievements.cpp) + include_directories(src/rcheevos/include) + add_definitions(-DRC_DISABLE_LUA) +endif() + if(IOS) set(SKYEMU_SRC ${SKYEMU_SRC} src/ios_support.m) add_definitions(-DPLATFORM_IOS) diff --git a/src/main.c b/src/main.c index 4365ba362..7d60aa5ad 100644 --- a/src/main.c +++ b/src/main.c @@ -23,6 +23,8 @@ #include "capstone/include/capstone/capstone.h" #include "miniz.h" #include "localization.h" +#include "retro_achievements.h" +#include "rcheevos/include/rc_client.h" #if defined(EMSCRIPTEN) #include @@ -4394,6 +4396,7 @@ void se_update_frame() { #endif se_update_key_turbo(&emu_state); se_update_solar_sensor(&emu_state); + ra_poll_requests(); if(emu_state.run_mode == SB_MODE_RESET){ se_reset_core(); @@ -5239,6 +5242,18 @@ static void se_init_audio(){ }); se_reset_audio_ring(); } +static void se_ra_login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { + // TODO: show cool "logged in" banner or something + const rc_client_user_t* user = rc_client_get_user_info(client); + printf("logged in as %s (score: %d)\n", user->display_name, user->score); +} +static uint32_t se_ra_read_memory_callback(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client){ + // TODO: handle reading from consoles + return 0; +} +static void se_init_retro_achievements(){ + ra_initialize_client(se_ra_read_memory_callback); +} // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. bool se_begin_menu_bar(){ @@ -6139,6 +6154,7 @@ static void init(void) { }; gui_state.last_touch_time=-10000; se_init_audio(); + se_init_retro_achievements(); sg_push_debug_group("LCD Shader Init"); gui_state.lcd_prog = sg_make_shader(lcdprog_shader_desc(sg_query_backend())); @@ -6185,6 +6201,7 @@ static void init(void) { #endif } static void cleanup(void) { + ra_shutdown_client(); simgui_shutdown(); se_free_all_images(); sg_shutdown(); diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp new file mode 100644 index 000000000..b875d3d36 --- /dev/null +++ b/src/retro_achievements.cpp @@ -0,0 +1,144 @@ +extern "C" { + #include "retro_achievements.h" +} +#include "rcheevos/include/rc_client.h" +#include "httplib.h" +#include +#include +#include + +rc_client_t* ra_client = NULL; + +// httplib doesn't have a way to make async requests, so we need to do it ourselves +struct AsyncRequest +{ + std::unique_ptr client; + std::future result_future; + rc_client_server_callback_t callback; + void* callback_data; +}; + +std::vector> async_requests; + +static void server_callback(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + // RetroAchievements may not allow hardcore unlocks if we don't properly identify ourselves. + const char* user_agent = "SkyEmu/4.0"; + + std::unique_ptr async_request(new AsyncRequest); + + // Unfortunately request->url gives us the full URL, so we need to extract the GET parameters + // to pass to httplib + // TODO: switch to SSLClient and https, needs OpenSSL + std::string full_url = request->url; + std::string host = "http://retroachievements.org"; + std::string get_params; + std::string::size_type last_slash = full_url.find_last_of('/'); + if (last_slash != std::string::npos) + { + get_params = full_url.substr(last_slash); + } + else + { + printf("[rcheevos]: could not parse URL: %s\n", request->url); + return; + } + + async_request->client.reset(new httplib::Client(host)); + async_request->callback = callback; + async_request->callback_data = callback_data; + + // Copy it here as request is destroyed as soon as we return + std::string content_type = request->content_type; + std::string post_data = request->post_data; + if(request->post_data) + { + httplib::Result (httplib::Client::*gf)(const std::string&, const std::string&, const std::string&) = &httplib::Client::Post; + async_request->result_future = std::async(std::launch::async, gf, async_request->client.get(), get_params, post_data, content_type); + async_requests.push_back(std::move(async_request)); + } + else + { + httplib::Result (httplib::Client::*gf)(const std::string&) = &httplib::Client::Get; + async_request->result_future = std::async(std::launch::async, gf, async_request->client.get(), get_params); + async_requests.push_back(std::move(async_request)); + } +} + +static void log_message(const char* message, const rc_client_t* client) +{ + printf("[rcheevos]: %s\n", message); +} + +void ra_initialize_client(rc_client_read_memory_func_t memory_read_func) +{ + if(ra_client) + { + printf("[rcheevos]: client already initialized!\n"); + } + else + { + ra_client = rc_client_create(memory_read_func, server_callback); + #ifndef NDEBUG + rc_client_enable_logging(ra_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); + #endif + // TODO: should probably be an option after we're finished testing + rc_client_set_hardcore_enabled(ra_client, 0); + } +} + +void ra_shutdown_client() +{ + if(ra_client) + { + rc_client_destroy(ra_client); + ra_client = nullptr; + } +} + +bool ra_is_logged_in() +{ + return rc_client_get_user_info(ra_client) != NULL; +} + +void ra_login_credentials(const char* username, const char* password, rc_client_callback_t login_callback) +{ + rc_client_begin_login_with_password(ra_client, username, password, login_callback, NULL); +} + +void ra_poll_requests() +{ + // Check if any of our asynchronous requests have finished, and if so, call the callback + auto it = async_requests.begin(); + while(it != async_requests.end()) + { + const std::unique_ptr& request = *it; + if (request->result_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) + { + httplib::Result result = request->result_future.get(); + if(result.error() == httplib::Error::Success) + { + rc_api_server_response_t response; + response.body = result->body.c_str(); + response.body_length = result->body.length(); + response.http_status_code = result->status; + request->callback(&response, request->callback_data); + } + else + { + printf("[rcheevos]: http request failed: %s\n", to_string(result.error()).c_str()); + } + it = async_requests.erase(it); + } + else + { + ++it; + } + } +} + +void ra_login_token(const char* username, const char* token, rc_client_callback_t login_callback) +{ + rc_client_begin_login_with_token(ra_client, username, token, login_callback, NULL); +} \ No newline at end of file diff --git a/src/retro_achievements.h b/src/retro_achievements.h new file mode 100644 index 000000000..2e154d162 --- /dev/null +++ b/src/retro_achievements.h @@ -0,0 +1,19 @@ +#ifndef RETRO_ACHIEVEMENTS +#define RETRO_ACHIEVEMENTS +#include +#include +#include + +struct rc_client_t; +typedef struct rc_client_t rc_client_t; + +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +bool ra_is_logged_in(); +void ra_initialize_client(rc_client_read_memory_func_t memory_read_func); +void ra_login_credentials(const char* username, const char* password, rc_client_callback_t login_callback); +void ra_login_token(const char* username, const char* token, rc_client_callback_t login_callback); +void ra_poll_requests(); +void ra_shutdown_client(); +#endif \ No newline at end of file