Path: blob/master/dep/rcheevos/src/rc_client_raintegration.c
4246 views
#include "rc_client_raintegration_internal.h"12#include "rc_client_internal.h"34#include "rapi/rc_api_common.h"56#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION78/* ===== natvis extensions ===== */910typedef struct __rc_client_raintegration_event_enum_t { uint8_t value; } __rc_client_raintegration_event_enum_t;11static void rc_client_raintegration_natvis_helper(void)12{13struct natvis_extensions {14__rc_client_raintegration_event_enum_t raintegration_event_type;15} natvis;1617natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE;18natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED;19natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED;20natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_PAUSE;21natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED;22}2324/* ============================= */2526static void rc_client_raintegration_load_dll(rc_client_t* client,27const wchar_t* search_directory, rc_client_callback_t callback, void* callback_userdata)28{29wchar_t sPath[_MAX_PATH];30const int nPathSize = sizeof(sPath) / sizeof(sPath[0]);31rc_client_raintegration_t* raintegration;32int sPathIndex = 0;33DWORD dwAttrib;34HINSTANCE hDLL;3536if (search_directory) {37sPathIndex = swprintf_s(sPath, nPathSize, L"%s\\", search_directory);38if (sPathIndex > nPathSize - 22) {39callback(RC_INVALID_STATE, "search_directory too long", client, callback_userdata);40return;41}42}4344#if defined(_M_X64) || defined(__amd64__)45wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration-x64.dll");46dwAttrib = GetFileAttributesW(sPath);47if (dwAttrib == INVALID_FILE_ATTRIBUTES) {48wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll");49dwAttrib = GetFileAttributesW(sPath);50}51#else52wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll");53dwAttrib = GetFileAttributesW(sPath);54#endif5556if (dwAttrib == INVALID_FILE_ATTRIBUTES) {57callback(RC_MISSING_VALUE, "RA_Integration.dll not found in search directory", client, callback_userdata);58return;59}6061hDLL = LoadLibraryW(sPath);62if (hDLL == NULL) {63char error_message[512];64const DWORD last_error = GetLastError();65int offset = snprintf(error_message, sizeof(error_message), "Failed to load RA_Integration.dll (%u)", last_error);6667if (last_error != 0) {68LPSTR messageBuffer = NULL;69const DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,70NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);7172snprintf(&error_message[offset], sizeof(error_message) - offset, ": %.*s", size, messageBuffer);7374LocalFree(messageBuffer);75}7677callback(RC_ABORTED, error_message, client, callback_userdata);78return;79}8081raintegration = (rc_client_raintegration_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_raintegration_t));82memset(raintegration, 0, sizeof(*raintegration));83raintegration->hDLL = hDLL;8485raintegration->get_version = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_IntegrationVersion");86raintegration->get_host_url = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_HostUrl");87raintegration->init_client = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitClient");88raintegration->init_client_offline = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitOffline");89raintegration->set_console_id = (rc_client_raintegration_set_int_func_t)GetProcAddress(hDLL, "_RA_SetConsoleID");90raintegration->shutdown = (rc_client_raintegration_action_func_t)GetProcAddress(hDLL, "_RA_Shutdown");9192raintegration->update_main_window_handle = (rc_client_raintegration_hwnd_action_func_t)GetProcAddress(hDLL, "_RA_UpdateHWnd");9394raintegration->get_external_client = (rc_client_raintegration_get_external_client_func_t)GetProcAddress(hDLL, "_Rcheevos_GetExternalClient");95raintegration->get_menu = (rc_client_raintegration_get_menu_func_t)GetProcAddress(hDLL, "_Rcheevos_RAIntegrationGetMenu");96raintegration->activate_menu_item = (rc_client_raintegration_activate_menuitem_func_t)GetProcAddress(hDLL, "_Rcheevos_ActivateRAIntegrationMenuItem");97raintegration->set_write_memory_function = (rc_client_raintegration_set_write_memory_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationWriteMemoryFunction");98raintegration->set_get_game_name_function = (rc_client_raintegration_set_get_game_name_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationGetGameNameFunction");99raintegration->set_event_handler = (rc_client_raintegration_set_event_handler_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationEventHandler");100raintegration->has_modifications = (rc_client_raintegration_get_int_func_t)GetProcAddress(hDLL, "_Rcheevos_HasModifications");101raintegration->get_achievement_state = (rc_client_raintegration_get_achievement_state_func_t)GetProcAddress(hDLL, "_Rcheevos_GetAchievementState");102103if (!raintegration->get_version ||104!raintegration->init_client ||105!raintegration->get_external_client) {106FreeLibrary(hDLL);107108callback(RC_ABORTED, "One or more required exports was not found in RA_Integration.dll", client, callback_userdata);109110/* dummy reference to natvis helper to ensure extensions get compiled in. */111raintegration->shutdown = rc_client_raintegration_natvis_helper;112}113else {114rc_mutex_lock(&client->state.mutex);115client->state.raintegration = raintegration;116rc_mutex_unlock(&client->state.mutex);117118RC_CLIENT_LOG_INFO_FORMATTED(client, "RA_Integration.dll %s loaded", client->state.raintegration->get_version());119}120}121122typedef struct rc_client_version_validation_callback_data_t {123rc_client_t* client;124rc_client_callback_t callback;125void* callback_userdata;126HWND main_window_handle;127char* client_name;128char* client_version;129rc_client_async_handle_t async_handle;130uint8_t defer_load;131} rc_client_version_validation_callback_data_t;132133int rc_client_version_less(const char* left, const char* right)134{135do {136int left_len = 0;137int right_len = 0;138while (*left && *left == '0')139++left;140while (left[left_len] && left[left_len] != '.')141++left_len;142while (*right && *right == '0')143++right;144while (right[right_len] && right[right_len] != '.')145++right_len;146147if (left_len != right_len)148return (left_len < right_len);149150while (left_len--) {151if (*left != *right)152return (*left < *right);153++left;154++right;155}156157if (*left == '.')158++left;159if (*right == '.')160++right;161} while (*left || *right);162163return 0;164}165166static int rc_client_init_raintegration(rc_client_t* client, HWND main_window_handle, const char* client_name,167const char* client_version, const char** error_message)168{169rc_client_raintegration_init_client_func_t init_func = client->state.raintegration->init_client;170171if (client->state.raintegration->get_host_url) {172const char* host_url = client->state.raintegration->get_host_url();173if (host_url) {174if (strcmp(host_url, "OFFLINE") != 0) {175if (strcmp(host_url, rc_api_default_host()) != 0)176rc_client_set_host(client, host_url);177}178else if (client->state.raintegration->init_client_offline) {179init_func = client->state.raintegration->init_client_offline;180RC_CLIENT_LOG_INFO(client, "Initializing in offline mode");181}182}183}184185if (!init_func || !init_func(main_window_handle, client_name, client_version)) {186*error_message = "RA_Integration initialization failed";187188rc_client_unload_raintegration(client);189190RC_CLIENT_LOG_ERR(client, *error_message);191return RC_ABORTED;192}193else {194rc_client_external_t* external_client = (rc_client_external_t*)195rc_buffer_alloc(&client->state.buffer, sizeof(*external_client));196memset(external_client, 0, sizeof(*external_client));197198if (!client->state.raintegration->get_external_client(external_client, RC_CLIENT_EXTERNAL_VERSION)) {199*error_message = "RA_Integration external client export failed";200201rc_client_unload_raintegration(client);202203RC_CLIENT_LOG_ERR(client, *error_message);204return RC_ABORTED;205}206else {207/* copy state to the external client */208if (external_client->enable_logging)209external_client->enable_logging(client, client->state.log_level, client->callbacks.log_call);210211if (external_client->set_event_handler)212external_client->set_event_handler(client, client->callbacks.event_handler);213if (external_client->set_read_memory)214external_client->set_read_memory(client, client->callbacks.read_memory);215216if (external_client->set_hardcore_enabled)217external_client->set_hardcore_enabled(rc_client_get_hardcore_enabled(client));218if (external_client->set_unofficial_enabled)219external_client->set_unofficial_enabled(rc_client_get_unofficial_enabled(client));220if (external_client->set_encore_mode_enabled)221external_client->set_encore_mode_enabled(rc_client_get_encore_mode_enabled(client));222if (external_client->set_spectator_mode_enabled)223external_client->set_spectator_mode_enabled(rc_client_get_spectator_mode_enabled(client));224225/* attach the external client and call the callback */226client->state.external_client = external_client;227228client->state.raintegration->hMainWindow = main_window_handle;229client->state.raintegration->bIsInited = 1;230return RC_OK;231}232}233}234235static void rc_client_version_validation_callback(const rc_api_server_response_t* server_response, void* callback_data)236{237rc_client_version_validation_callback_data_t* version_validation_callback_data =238(rc_client_version_validation_callback_data_t*)callback_data;239rc_client_t* client = version_validation_callback_data->client;240241if (rc_client_async_handle_aborted(client, &version_validation_callback_data->async_handle)) {242RC_CLIENT_LOG_VERBOSE(client, "Version validation aborted");243}244else {245rc_api_response_t response;246int result;247const char* current_version;248const char* minimum_version = "";249250rc_json_field_t fields[] = {251RC_JSON_NEW_FIELD("Success"),252RC_JSON_NEW_FIELD("Error"),253RC_JSON_NEW_FIELD("MinimumVersion"),254};255256memset(&response, 0, sizeof(response));257rc_buffer_init(&response.buffer);258259result = rc_json_parse_server_response(&response, server_response, fields, sizeof(fields) / sizeof(fields[0]));260if (result == RC_OK) {261if (!rc_json_get_required_string(&minimum_version, &response, &fields[2], "MinimumVersion"))262result = RC_MISSING_VALUE;263}264265if (result != RC_OK) {266RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to fetch latest integration version: %.*s", server_response->body_length, server_response->body);267268rc_client_unload_raintegration(client);269270version_validation_callback_data->callback(result, rc_error_str(result),271client, version_validation_callback_data->callback_userdata);272}273else {274current_version = client->state.raintegration->get_version();275276if (rc_client_version_less(current_version, minimum_version)) {277char error_message[256];278279rc_client_unload_raintegration(client);280281snprintf(error_message, sizeof(error_message),282"RA_Integration version %s is lower than minimum version %s", current_version, minimum_version);283RC_CLIENT_LOG_WARN(client, error_message);284version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata);285}286else {287RC_CLIENT_LOG_INFO_FORMATTED(client, "Validated RA_Integration version %s (minimum %s)", current_version, minimum_version);288289if (version_validation_callback_data->defer_load) {290// let the caller complete the initialization291version_validation_callback_data->callback(RC_OK, NULL, client, version_validation_callback_data->callback_userdata);292} else {293const char* error_message = NULL;294result = rc_client_init_raintegration(client, version_validation_callback_data->main_window_handle,295version_validation_callback_data->client_name, version_validation_callback_data->client_version, &error_message);296version_validation_callback_data->callback(result, error_message, client, version_validation_callback_data->callback_userdata);297}298}299}300301rc_buffer_destroy(&response.buffer);302}303304free(version_validation_callback_data->client_name);305free(version_validation_callback_data->client_version);306free(version_validation_callback_data);307}308309rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client,310const wchar_t* search_directory, HWND main_window_handle,311const char* client_name, const char* client_version,312rc_client_callback_t callback, void* callback_userdata)313{314rc_client_version_validation_callback_data_t* callback_data;315rc_api_url_builder_t builder;316rc_api_request_t request;317318if (!client) {319callback(RC_INVALID_STATE, "client is required", client, callback_userdata);320return NULL;321}322323if (!client_name) {324callback(RC_INVALID_STATE, "client_name is required", client, callback_userdata);325return NULL;326}327328if (!client_version) {329callback(RC_INVALID_STATE, "client_version is required", client, callback_userdata);330return NULL;331}332333if (client->state.user != RC_CLIENT_USER_STATE_NONE) {334callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata);335return NULL;336}337338if (!client->state.raintegration) {339if (!main_window_handle) {340callback(RC_INVALID_STATE, "main_window_handle is required", client, callback_userdata);341return NULL;342}343344rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata);345if (!client->state.raintegration)346return NULL;347}348349if (client->state.raintegration->get_host_url) {350const char* host_url = client->state.raintegration->get_host_url();351if (host_url && strcmp(host_url, rc_api_default_host()) != 0 &&352strcmp(host_url, "OFFLINE") != 0) {353/* if the DLL specifies a custom host, use it */354rc_client_set_host(client, host_url);355}356}357358memset(&request, 0, sizeof(request));359rc_api_url_build_dorequest_url(&request, &client->state.host);360rc_url_builder_init(&builder, &request.buffer, 48);361rc_url_builder_append_str_param(&builder, "r", "latestintegration");362request.post_data = rc_url_builder_finalize(&builder);363364callback_data = calloc(1, sizeof(*callback_data));365if (!callback_data) {366callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);367return NULL;368}369370callback_data->client = client;371callback_data->callback = callback;372callback_data->callback_userdata = callback_userdata;373callback_data->client_name = strdup(client_name);374callback_data->client_version = strdup(client_version);375callback_data->main_window_handle = main_window_handle;376callback_data->defer_load = 0;377378client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client);379return &callback_data->async_handle;380}381382rc_client_async_handle_t* rc_client_begin_load_raintegration_deferred(rc_client_t* client,383const wchar_t* search_directory,384rc_client_callback_t callback,385void* callback_userdata) {386rc_client_version_validation_callback_data_t* callback_data;387rc_api_url_builder_t builder;388rc_api_request_t request;389390if (!client) {391callback(RC_INVALID_STATE, "client is required", client, callback_userdata);392return NULL;393}394395if (client->state.user != RC_CLIENT_USER_STATE_NONE) {396callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata);397return NULL;398}399400if (!client->state.raintegration) {401rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata);402if (!client->state.raintegration)403return NULL;404}405406if (client->state.raintegration->get_host_url) {407const char* host_url = client->state.raintegration->get_host_url();408if (host_url && strcmp(host_url, rc_api_default_host()) != 0 &&409strcmp(host_url, "OFFLINE") != 0) {410/* if the DLL specifies a custom host, use it */411rc_client_set_host(client, host_url);412}413}414415memset(&request, 0, sizeof(request));416rc_api_url_build_dorequest_url(&request, &client->state.host);417rc_url_builder_init(&builder, &request.buffer, 48);418rc_url_builder_append_str_param(&builder, "r", "latestintegration");419request.post_data = rc_url_builder_finalize(&builder);420421callback_data = calloc(1, sizeof(*callback_data));422if (!callback_data) {423callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);424return NULL;425}426427callback_data->client = client;428callback_data->callback = callback;429callback_data->callback_userdata = callback_userdata;430callback_data->client_name = NULL;431callback_data->client_version = NULL;432callback_data->main_window_handle = NULL;433callback_data->defer_load = 1;434435client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client);436return &callback_data->async_handle;437}438439int rc_client_finish_load_raintegration(rc_client_t* client, HWND main_window_handle, const char* client_name,440const char* client_version, const char** error_message)441{442if (client == NULL || main_window_handle == NULL || client_name == NULL || client_version == NULL) {443*error_message = "client is required";444rc_client_unload_raintegration(client);445return RC_ABORTED;446}447448if (main_window_handle == NULL) {449*error_message = "main_window_handle is required";450rc_client_unload_raintegration(client);451return RC_ABORTED;452}453454if (client_name == NULL) {455*error_message = "client_name is required";456rc_client_unload_raintegration(client);457return RC_ABORTED;458}459460if (client_version == NULL) {461*error_message = "client_version is required";462rc_client_unload_raintegration(client);463return RC_ABORTED;464}465466return rc_client_init_raintegration(client, main_window_handle, client_name, client_version, error_message);467}468469void rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle)470{471if (client && client->state.raintegration) {472client->state.raintegration->hMainWindow = main_window_handle;473474if (client->state.raintegration->bIsInited &&475client->state.raintegration->update_main_window_handle) {476client->state.raintegration->update_main_window_handle(main_window_handle);477}478}479}480481void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler)482{483if (client && client->state.raintegration && client->state.raintegration->set_write_memory_function)484client->state.raintegration->set_write_memory_function(client, handler);485}486487void rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler)488{489if (client && client->state.raintegration && client->state.raintegration->set_get_game_name_function)490client->state.raintegration->set_get_game_name_function(client, handler);491}492493void rc_client_raintegration_set_event_handler(rc_client_t* client,494rc_client_raintegration_event_handler_t handler)495{496if (client && client->state.raintegration && client->state.raintegration->set_event_handler)497client->state.raintegration->set_event_handler(client, handler);498}499500const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_client_t* client)501{502if (!client || !client->state.raintegration ||503!client->state.raintegration->bIsInited ||504!client->state.raintegration->get_menu) {505return NULL;506}507508return client->state.raintegration->get_menu();509}510511void rc_client_raintegration_set_console_id(rc_client_t* client, uint32_t console_id)512{513if (client && client->state.raintegration && client->state.raintegration->set_console_id)514client->state.raintegration->set_console_id(console_id);515}516517int rc_client_raintegration_has_modifications(const rc_client_t* client)518{519if (!client || !client->state.raintegration ||520!client->state.raintegration->bIsInited ||521!client->state.raintegration->has_modifications) {522return 0;523}524525return client->state.raintegration->has_modifications();526}527528int rc_client_raintegration_get_achievement_state(const rc_client_t* client, uint32_t achievement_id)529{530if (!client || !client->state.raintegration ||531!client->state.raintegration->bIsInited ||532!client->state.raintegration->get_achievement_state) {533return RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_NONE;534}535536return client->state.raintegration->get_achievement_state(achievement_id);537}538539void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu)540{541HMENU hPopupMenu = NULL;542const rc_client_raintegration_menu_t* menu;543544if (!client || !client->state.raintegration)545return;546547/* destroy the existing menu */548if (client->state.raintegration->hPopupMenu)549DestroyMenu(client->state.raintegration->hPopupMenu);550551/* create the popup menu */552hPopupMenu = CreatePopupMenu();553554menu = rc_client_raintegration_get_menu(client);555if (menu && menu->num_items)556{557const rc_client_raintegration_menu_item_t* menuitem = menu->items;558const rc_client_raintegration_menu_item_t* stop = menu->items + menu->num_items;559560for (; menuitem < stop; ++menuitem)561{562if (menuitem->id == 0)563AppendMenuA(hPopupMenu, MF_SEPARATOR, 0U, NULL);564else565{566UINT flags = MF_STRING;567if (menuitem->checked)568flags |= MF_CHECKED;569if (!menuitem->enabled)570flags |= MF_GRAYED;571572AppendMenuA(hPopupMenu, flags, menuitem->id, menuitem->label);573}574}575}576577/* add/update the item containing the popup menu */578{579int nIndex = GetMenuItemCount(hMenu);580const char* menuText = "&RetroAchievements";581char buffer[64];582583UINT flags = MF_POPUP | MF_STRING;584if (!menu || !menu->num_items)585flags |= MF_GRAYED;586587while (--nIndex >= 0)588{589if (GetMenuStringA(hMenu, nIndex, buffer, sizeof(buffer) - 1, MF_BYPOSITION))590{591if (strcmp(buffer, menuText) == 0)592break;593}594}595596if (nIndex == -1)597AppendMenuA(hMenu, flags, (UINT_PTR)hPopupMenu, menuText);598else599ModifyMenuA(hMenu, nIndex, flags | MF_BYPOSITION, (UINT_PTR)hPopupMenu, menuText);600601if (client->state.raintegration->hMainWindow && GetMenu(client->state.raintegration->hMainWindow) == hMenu)602DrawMenuBar(client->state.raintegration->hMainWindow);603}604605client->state.raintegration->hPopupMenu = hPopupMenu;606}607608void rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menuitem)609{610if (client && client->state.raintegration && client->state.raintegration->hPopupMenu)611{612UINT flags = MF_STRING;613if (menuitem->checked)614flags |= MF_CHECKED;615616CheckMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND);617618flags = (menuitem->enabled) ? MF_ENABLED : MF_GRAYED;619EnableMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND);620}621}622623int rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t menu_item_id)624{625if (!client || !client->state.raintegration || !client->state.raintegration->activate_menu_item)626return 0;627628return client->state.raintegration->activate_menu_item(menu_item_id);629}630631void rc_client_unload_raintegration(rc_client_t* client)632{633HINSTANCE hDLL;634635if (!client || !client->state.raintegration)636return;637638RC_CLIENT_LOG_INFO(client, "Unloading RA_Integration")639640if (client->state.external_client && client->state.external_client->destroy)641client->state.external_client->destroy();642643if (client->state.raintegration->shutdown)644client->state.raintegration->shutdown();645646rc_mutex_lock(&client->state.mutex);647hDLL = client->state.raintegration->hDLL;648client->state.raintegration = NULL;649client->state.external_client = NULL;650rc_mutex_unlock(&client->state.mutex);651652if (hDLL)653FreeLibrary(hDLL);654}655656#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */657658659