Path: blob/master/dep/rcheevos/src/rc_client_raintegration.c
7604 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));224if (external_client->set_allow_background_memory_reads)225external_client->set_allow_background_memory_reads(client->state.allow_background_memory_reads);226227/* attach the external client and call the callback */228client->state.external_client = external_client;229230client->state.raintegration->hMainWindow = main_window_handle;231client->state.raintegration->bIsInited = 1;232return RC_OK;233}234}235}236237static void rc_client_version_validation_callback(const rc_api_server_response_t* server_response, void* callback_data)238{239rc_client_version_validation_callback_data_t* version_validation_callback_data =240(rc_client_version_validation_callback_data_t*)callback_data;241rc_client_t* client = version_validation_callback_data->client;242243if (rc_client_async_handle_aborted(client, &version_validation_callback_data->async_handle)) {244RC_CLIENT_LOG_VERBOSE(client, "Version validation aborted");245}246else {247rc_api_response_t response;248int result;249const char* current_version;250const char* minimum_version = "";251252rc_json_field_t fields[] = {253RC_JSON_NEW_FIELD("Success"),254RC_JSON_NEW_FIELD("Error"),255RC_JSON_NEW_FIELD("MinimumVersion"),256};257258memset(&response, 0, sizeof(response));259rc_buffer_init(&response.buffer);260261result = rc_json_parse_server_response(&response, server_response, fields, sizeof(fields) / sizeof(fields[0]));262if (result == RC_OK) {263if (!rc_json_get_required_string(&minimum_version, &response, &fields[2], "MinimumVersion"))264result = RC_MISSING_VALUE;265}266267if (result != RC_OK) {268RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to fetch latest integration version: %.*s", server_response->body_length, server_response->body);269270rc_client_unload_raintegration(client);271272version_validation_callback_data->callback(result, rc_error_str(result),273client, version_validation_callback_data->callback_userdata);274}275else {276current_version = client->state.raintegration->get_version();277278if (rc_client_version_less(current_version, minimum_version)) {279char error_message[256];280281rc_client_unload_raintegration(client);282283snprintf(error_message, sizeof(error_message),284"RA_Integration version %s is lower than minimum version %s", current_version, minimum_version);285RC_CLIENT_LOG_WARN(client, error_message);286version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata);287}288else {289RC_CLIENT_LOG_INFO_FORMATTED(client, "Validated RA_Integration version %s (minimum %s)", current_version, minimum_version);290291if (version_validation_callback_data->defer_load) {292// let the caller complete the initialization293version_validation_callback_data->callback(RC_OK, NULL, client, version_validation_callback_data->callback_userdata);294} else {295const char* error_message = NULL;296result = rc_client_init_raintegration(client, version_validation_callback_data->main_window_handle,297version_validation_callback_data->client_name, version_validation_callback_data->client_version, &error_message);298version_validation_callback_data->callback(result, error_message, client, version_validation_callback_data->callback_userdata);299}300}301}302303rc_buffer_destroy(&response.buffer);304}305306free(version_validation_callback_data->client_name);307free(version_validation_callback_data->client_version);308free(version_validation_callback_data);309}310311rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client,312const wchar_t* search_directory, HWND main_window_handle,313const char* client_name, const char* client_version,314rc_client_callback_t callback, void* callback_userdata)315{316rc_client_version_validation_callback_data_t* callback_data;317rc_api_url_builder_t builder;318rc_api_request_t request;319320if (!client) {321callback(RC_INVALID_STATE, "client is required", client, callback_userdata);322return NULL;323}324325if (!client_name) {326callback(RC_INVALID_STATE, "client_name is required", client, callback_userdata);327return NULL;328}329330if (!client_version) {331callback(RC_INVALID_STATE, "client_version is required", client, callback_userdata);332return NULL;333}334335if (client->state.user != RC_CLIENT_USER_STATE_NONE) {336callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata);337return NULL;338}339340if (!client->state.raintegration) {341if (!main_window_handle) {342callback(RC_INVALID_STATE, "main_window_handle is required", client, callback_userdata);343return NULL;344}345346rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata);347if (!client->state.raintegration)348return NULL;349}350351if (client->state.raintegration->get_host_url) {352const char* host_url = client->state.raintegration->get_host_url();353if (host_url && strcmp(host_url, rc_api_default_host()) != 0 &&354strcmp(host_url, "OFFLINE") != 0) {355/* if the DLL specifies a custom host, use it */356rc_client_set_host(client, host_url);357}358}359360memset(&request, 0, sizeof(request));361rc_api_url_build_dorequest_url(&request, &client->state.host);362rc_url_builder_init(&builder, &request.buffer, 48);363rc_url_builder_append_str_param(&builder, "r", "latestintegration");364request.post_data = rc_url_builder_finalize(&builder);365366callback_data = calloc(1, sizeof(*callback_data));367if (!callback_data) {368callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);369return NULL;370}371372callback_data->client = client;373callback_data->callback = callback;374callback_data->callback_userdata = callback_userdata;375callback_data->client_name = strdup(client_name);376callback_data->client_version = strdup(client_version);377callback_data->main_window_handle = main_window_handle;378callback_data->defer_load = 0;379380client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client);381return &callback_data->async_handle;382}383384rc_client_async_handle_t* rc_client_begin_load_raintegration_deferred(rc_client_t* client,385const wchar_t* search_directory,386rc_client_callback_t callback,387void* callback_userdata) {388rc_client_version_validation_callback_data_t* callback_data;389rc_api_url_builder_t builder;390rc_api_request_t request;391392if (!client) {393callback(RC_INVALID_STATE, "client is required", client, callback_userdata);394return NULL;395}396397if (client->state.user != RC_CLIENT_USER_STATE_NONE) {398callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata);399return NULL;400}401402if (!client->state.raintegration) {403rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata);404if (!client->state.raintegration)405return NULL;406}407408if (client->state.raintegration->get_host_url) {409const char* host_url = client->state.raintegration->get_host_url();410if (host_url && strcmp(host_url, rc_api_default_host()) != 0 &&411strcmp(host_url, "OFFLINE") != 0) {412/* if the DLL specifies a custom host, use it */413rc_client_set_host(client, host_url);414}415}416417memset(&request, 0, sizeof(request));418rc_api_url_build_dorequest_url(&request, &client->state.host);419rc_url_builder_init(&builder, &request.buffer, 48);420rc_url_builder_append_str_param(&builder, "r", "latestintegration");421request.post_data = rc_url_builder_finalize(&builder);422423callback_data = calloc(1, sizeof(*callback_data));424if (!callback_data) {425callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);426return NULL;427}428429callback_data->client = client;430callback_data->callback = callback;431callback_data->callback_userdata = callback_userdata;432callback_data->client_name = NULL;433callback_data->client_version = NULL;434callback_data->main_window_handle = NULL;435callback_data->defer_load = 1;436437client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client);438return &callback_data->async_handle;439}440441int rc_client_finish_load_raintegration(rc_client_t* client, HWND main_window_handle, const char* client_name,442const char* client_version, const char** error_message)443{444if (client == NULL || main_window_handle == NULL || client_name == NULL || client_version == NULL) {445*error_message = "client is required";446rc_client_unload_raintegration(client);447return RC_ABORTED;448}449450if (main_window_handle == NULL) {451*error_message = "main_window_handle is required";452rc_client_unload_raintegration(client);453return RC_ABORTED;454}455456if (client_name == NULL) {457*error_message = "client_name is required";458rc_client_unload_raintegration(client);459return RC_ABORTED;460}461462if (client_version == NULL) {463*error_message = "client_version is required";464rc_client_unload_raintegration(client);465return RC_ABORTED;466}467468return rc_client_init_raintegration(client, main_window_handle, client_name, client_version, error_message);469}470471void rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle)472{473if (client && client->state.raintegration) {474client->state.raintegration->hMainWindow = main_window_handle;475476if (client->state.raintegration->bIsInited &&477client->state.raintegration->update_main_window_handle) {478client->state.raintegration->update_main_window_handle(main_window_handle);479}480}481}482483void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler)484{485if (client && client->state.raintegration && client->state.raintegration->set_write_memory_function)486client->state.raintegration->set_write_memory_function(client, handler);487}488489void rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler)490{491if (client && client->state.raintegration && client->state.raintegration->set_get_game_name_function)492client->state.raintegration->set_get_game_name_function(client, handler);493}494495void rc_client_raintegration_set_event_handler(rc_client_t* client,496rc_client_raintegration_event_handler_t handler)497{498if (client && client->state.raintegration && client->state.raintegration->set_event_handler)499client->state.raintegration->set_event_handler(client, handler);500}501502const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_client_t* client)503{504if (!client || !client->state.raintegration ||505!client->state.raintegration->bIsInited ||506!client->state.raintegration->get_menu) {507return NULL;508}509510return client->state.raintegration->get_menu();511}512513void rc_client_raintegration_set_console_id(rc_client_t* client, uint32_t console_id)514{515if (client && client->state.raintegration && client->state.raintegration->set_console_id)516client->state.raintegration->set_console_id(console_id);517}518519int rc_client_raintegration_has_modifications(const rc_client_t* client)520{521if (!client || !client->state.raintegration ||522!client->state.raintegration->bIsInited ||523!client->state.raintegration->has_modifications) {524return 0;525}526527return client->state.raintegration->has_modifications();528}529530int rc_client_raintegration_get_achievement_state(const rc_client_t* client, uint32_t achievement_id)531{532if (!client || !client->state.raintegration ||533!client->state.raintegration->bIsInited ||534!client->state.raintegration->get_achievement_state) {535return RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_NONE;536}537538return client->state.raintegration->get_achievement_state(achievement_id);539}540541void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu)542{543HMENU hPopupMenu = NULL;544const rc_client_raintegration_menu_t* menu;545546if (!client || !client->state.raintegration)547return;548549/* destroy the existing menu */550if (client->state.raintegration->hPopupMenu)551DestroyMenu(client->state.raintegration->hPopupMenu);552553/* create the popup menu */554hPopupMenu = CreatePopupMenu();555556menu = rc_client_raintegration_get_menu(client);557if (menu && menu->num_items)558{559const rc_client_raintegration_menu_item_t* menuitem = menu->items;560const rc_client_raintegration_menu_item_t* stop = menu->items + menu->num_items;561562for (; menuitem < stop; ++menuitem)563{564if (menuitem->id == 0)565AppendMenuA(hPopupMenu, MF_SEPARATOR, 0U, NULL);566else567{568UINT flags = MF_STRING;569if (menuitem->checked)570flags |= MF_CHECKED;571if (!menuitem->enabled)572flags |= MF_GRAYED;573574AppendMenuA(hPopupMenu, flags, menuitem->id, menuitem->label);575}576}577}578579/* add/update the item containing the popup menu */580{581int nIndex = GetMenuItemCount(hMenu);582const char* menuText = "&RetroAchievements";583char buffer[64];584585UINT flags = MF_POPUP | MF_STRING;586if (!menu || !menu->num_items)587flags |= MF_GRAYED;588589while (--nIndex >= 0)590{591if (GetMenuStringA(hMenu, nIndex, buffer, sizeof(buffer) - 1, MF_BYPOSITION))592{593if (strcmp(buffer, menuText) == 0)594break;595}596}597598if (nIndex == -1)599AppendMenuA(hMenu, flags, (UINT_PTR)hPopupMenu, menuText);600else601ModifyMenuA(hMenu, nIndex, flags | MF_BYPOSITION, (UINT_PTR)hPopupMenu, menuText);602603if (client->state.raintegration->hMainWindow && GetMenu(client->state.raintegration->hMainWindow) == hMenu)604DrawMenuBar(client->state.raintegration->hMainWindow);605}606607client->state.raintegration->hPopupMenu = hPopupMenu;608}609610void rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menuitem)611{612if (client && client->state.raintegration && client->state.raintegration->hPopupMenu)613{614UINT flags = MF_STRING;615if (menuitem->checked)616flags |= MF_CHECKED;617618CheckMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND);619620flags = (menuitem->enabled) ? MF_ENABLED : MF_GRAYED;621EnableMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND);622}623}624625int rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t menu_item_id)626{627if (!client || !client->state.raintegration || !client->state.raintegration->activate_menu_item)628return 0;629630return client->state.raintegration->activate_menu_item(menu_item_id);631}632633void rc_client_unload_raintegration(rc_client_t* client)634{635HINSTANCE hDLL;636637if (!client || !client->state.raintegration)638return;639640RC_CLIENT_LOG_INFO(client, "Unloading RA_Integration")641642if (client->state.external_client && client->state.external_client->destroy)643client->state.external_client->destroy();644645if (client->state.raintegration->shutdown)646client->state.raintegration->shutdown();647648rc_mutex_lock(&client->state.mutex);649hDLL = client->state.raintegration->hDLL;650client->state.raintegration = NULL;651client->state.external_client = NULL;652rc_mutex_unlock(&client->state.mutex);653654if (hDLL)655FreeLibrary(hDLL);656}657658#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */659660661