rc_client_raintegration.c (20710B)
1 #include "rc_client_raintegration_internal.h" 2 3 #include "rc_client_internal.h" 4 5 #include "rapi/rc_api_common.h" 6 7 #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION 8 9 static void rc_client_raintegration_load_dll(rc_client_t* client, 10 const wchar_t* search_directory, rc_client_callback_t callback, void* callback_userdata) 11 { 12 wchar_t sPath[_MAX_PATH]; 13 const int nPathSize = sizeof(sPath) / sizeof(sPath[0]); 14 rc_client_raintegration_t* raintegration; 15 int sPathIndex = 0; 16 DWORD dwAttrib; 17 HINSTANCE hDLL; 18 19 if (search_directory) { 20 sPathIndex = swprintf_s(sPath, nPathSize, L"%s\\", search_directory); 21 if (sPathIndex > nPathSize - 22) { 22 callback(RC_INVALID_STATE, "search_directory too long", client, callback_userdata); 23 return; 24 } 25 } 26 27 #if defined(_M_X64) || defined(__amd64__) 28 wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration-x64.dll"); 29 dwAttrib = GetFileAttributesW(sPath); 30 if (dwAttrib == INVALID_FILE_ATTRIBUTES) { 31 wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); 32 dwAttrib = GetFileAttributesW(sPath); 33 } 34 #else 35 wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); 36 dwAttrib = GetFileAttributesW(sPath); 37 #endif 38 39 if (dwAttrib == INVALID_FILE_ATTRIBUTES) { 40 callback(RC_MISSING_VALUE, "RA_Integration.dll not found in search directory", client, callback_userdata); 41 return; 42 } 43 44 hDLL = LoadLibraryW(sPath); 45 if (hDLL == NULL) { 46 char error_message[512]; 47 const DWORD last_error = GetLastError(); 48 int offset = snprintf(error_message, sizeof(error_message), "Failed to load RA_Integration.dll (%u)", last_error); 49 50 if (last_error != 0) { 51 LPSTR messageBuffer = NULL; 52 const DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 53 NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); 54 55 snprintf(&error_message[offset], sizeof(error_message) - offset, ": %.*s", size, messageBuffer); 56 57 LocalFree(messageBuffer); 58 } 59 60 callback(RC_ABORTED, error_message, client, callback_userdata); 61 return; 62 } 63 64 raintegration = (rc_client_raintegration_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_raintegration_t)); 65 memset(raintegration, 0, sizeof(*raintegration)); 66 raintegration->hDLL = hDLL; 67 68 raintegration->get_version = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_IntegrationVersion"); 69 raintegration->get_host_url = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_HostUrl"); 70 raintegration->init_client = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitClient"); 71 raintegration->init_client_offline = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitOffline"); 72 raintegration->set_console_id = (rc_client_raintegration_set_int_func_t)GetProcAddress(hDLL, "_RA_SetConsoleID"); 73 raintegration->shutdown = (rc_client_raintegration_action_func_t)GetProcAddress(hDLL, "_RA_Shutdown"); 74 75 raintegration->update_main_window_handle = (rc_client_raintegration_hwnd_action_func_t)GetProcAddress(hDLL, "_RA_UpdateHWnd"); 76 77 raintegration->get_external_client = (rc_client_raintegration_get_external_client_func_t)GetProcAddress(hDLL, "_Rcheevos_GetExternalClient"); 78 raintegration->get_menu = (rc_client_raintegration_get_menu_func_t)GetProcAddress(hDLL, "_Rcheevos_RAIntegrationGetMenu"); 79 raintegration->activate_menu_item = (rc_client_raintegration_activate_menuitem_func_t)GetProcAddress(hDLL, "_Rcheevos_ActivateRAIntegrationMenuItem"); 80 raintegration->set_write_memory_function = (rc_client_raintegration_set_write_memory_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationWriteMemoryFunction"); 81 raintegration->set_get_game_name_function = (rc_client_raintegration_set_get_game_name_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationGetGameNameFunction"); 82 raintegration->set_event_handler = (rc_client_raintegration_set_event_handler_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationEventHandler"); 83 raintegration->has_modifications = (rc_client_raintegration_get_int_func_t)GetProcAddress(hDLL, "_Rcheevos_HasModifications"); 84 raintegration->get_achievement_state = (rc_client_raintegration_get_achievement_state_func_t)GetProcAddress(hDLL, "_Rcheevos_GetAchievementState"); 85 86 if (!raintegration->get_version || 87 !raintegration->init_client || 88 !raintegration->get_external_client) { 89 FreeLibrary(hDLL); 90 91 callback(RC_ABORTED, "One or more required exports was not found in RA_Integration.dll", client, callback_userdata); 92 } 93 else { 94 rc_mutex_lock(&client->state.mutex); 95 client->state.raintegration = raintegration; 96 rc_mutex_unlock(&client->state.mutex); 97 98 RC_CLIENT_LOG_INFO_FORMATTED(client, "RA_Integration.dll %s loaded", client->state.raintegration->get_version()); 99 } 100 } 101 102 typedef struct rc_client_version_validation_callback_data_t { 103 rc_client_t* client; 104 rc_client_callback_t callback; 105 void* callback_userdata; 106 HWND main_window_handle; 107 char* client_name; 108 char* client_version; 109 rc_client_async_handle_t async_handle; 110 } rc_client_version_validation_callback_data_t; 111 112 int rc_client_version_less(const char* left, const char* right) 113 { 114 do { 115 int left_len = 0; 116 int right_len = 0; 117 while (*left && *left == '0') 118 ++left; 119 while (left[left_len] && left[left_len] != '.') 120 ++left_len; 121 while (*right && *right == '0') 122 ++right; 123 while (right[right_len] && right[right_len] != '.') 124 ++right_len; 125 126 if (left_len != right_len) 127 return (left_len < right_len); 128 129 while (left_len--) { 130 if (*left != *right) 131 return (*left < *right); 132 ++left; 133 ++right; 134 } 135 136 if (*left == '.') 137 ++left; 138 if (*right == '.') 139 ++right; 140 } while (*left || *right); 141 142 return 0; 143 } 144 145 static void rc_client_init_raintegration(rc_client_t* client, 146 rc_client_version_validation_callback_data_t* version_validation_callback_data) 147 { 148 rc_client_raintegration_init_client_func_t init_func = client->state.raintegration->init_client; 149 150 if (client->state.raintegration->get_host_url) { 151 const char* host_url = client->state.raintegration->get_host_url(); 152 if (host_url) { 153 if (strcmp(host_url, "OFFLINE") != 0) { 154 if (strcmp(host_url, "https://retroachievements.org") != 0) 155 rc_client_set_host(client, host_url); 156 } 157 else if (client->state.raintegration->init_client_offline) { 158 init_func = client->state.raintegration->init_client_offline; 159 RC_CLIENT_LOG_INFO(client, "Initializing in offline mode"); 160 } 161 } 162 } 163 164 if (!init_func || !init_func(version_validation_callback_data->main_window_handle, 165 version_validation_callback_data->client_name, 166 version_validation_callback_data->client_version)) { 167 const char* error_message = "RA_Integration initialization failed"; 168 169 rc_client_unload_raintegration(client); 170 171 RC_CLIENT_LOG_ERR(client, error_message); 172 version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); 173 } 174 else { 175 rc_client_external_t* external_client = (rc_client_external_t*) 176 rc_buffer_alloc(&client->state.buffer, sizeof(*external_client)); 177 memset(external_client, 0, sizeof(*external_client)); 178 179 if (!client->state.raintegration->get_external_client(external_client, RC_CLIENT_EXTERNAL_VERSION)) { 180 const char* error_message = "RA_Integration external client export failed"; 181 182 rc_client_unload_raintegration(client); 183 184 RC_CLIENT_LOG_ERR(client, error_message); 185 version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); 186 } 187 else { 188 /* copy state to the external client */ 189 if (external_client->enable_logging) 190 external_client->enable_logging(client, client->state.log_level, client->callbacks.log_call); 191 192 if (external_client->set_event_handler) 193 external_client->set_event_handler(client, client->callbacks.event_handler); 194 if (external_client->set_read_memory) 195 external_client->set_read_memory(client, client->callbacks.read_memory); 196 197 if (external_client->set_hardcore_enabled) 198 external_client->set_hardcore_enabled(rc_client_get_hardcore_enabled(client)); 199 if (external_client->set_unofficial_enabled) 200 external_client->set_unofficial_enabled(rc_client_get_unofficial_enabled(client)); 201 if (external_client->set_encore_mode_enabled) 202 external_client->set_encore_mode_enabled(rc_client_get_encore_mode_enabled(client)); 203 if (external_client->set_spectator_mode_enabled) 204 external_client->set_spectator_mode_enabled(rc_client_get_spectator_mode_enabled(client)); 205 206 /* attach the external client and call the callback */ 207 client->state.external_client = external_client; 208 209 client->state.raintegration->hMainWindow = version_validation_callback_data->main_window_handle; 210 client->state.raintegration->bIsInited = 1; 211 212 version_validation_callback_data->callback(RC_OK, NULL, 213 client, version_validation_callback_data->callback_userdata); 214 } 215 } 216 } 217 218 static void rc_client_version_validation_callback(const rc_api_server_response_t* server_response, void* callback_data) 219 { 220 rc_client_version_validation_callback_data_t* version_validation_callback_data = 221 (rc_client_version_validation_callback_data_t*)callback_data; 222 rc_client_t* client = version_validation_callback_data->client; 223 224 if (rc_client_async_handle_aborted(client, &version_validation_callback_data->async_handle)) { 225 RC_CLIENT_LOG_VERBOSE(client, "Version validation aborted"); 226 } 227 else { 228 rc_api_response_t response; 229 int result; 230 const char* current_version; 231 const char* minimum_version = ""; 232 233 rc_json_field_t fields[] = { 234 RC_JSON_NEW_FIELD("Success"), 235 RC_JSON_NEW_FIELD("Error"), 236 RC_JSON_NEW_FIELD("MinimumVersion"), 237 }; 238 239 memset(&response, 0, sizeof(response)); 240 rc_buffer_init(&response.buffer); 241 242 result = rc_json_parse_server_response(&response, server_response, fields, sizeof(fields) / sizeof(fields[0])); 243 if (result == RC_OK) { 244 if (!rc_json_get_required_string(&minimum_version, &response, &fields[2], "MinimumVersion")) 245 result = RC_MISSING_VALUE; 246 } 247 248 if (result != RC_OK) { 249 RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to fetch latest integration version: %.*s", server_response->body_length, server_response->body); 250 251 rc_client_unload_raintegration(client); 252 253 version_validation_callback_data->callback(result, rc_error_str(result), 254 client, version_validation_callback_data->callback_userdata); 255 } 256 else { 257 current_version = client->state.raintegration->get_version(); 258 259 if (rc_client_version_less(current_version, minimum_version)) { 260 char error_message[256]; 261 262 rc_client_unload_raintegration(client); 263 264 snprintf(error_message, sizeof(error_message), 265 "RA_Integration version %s is lower than minimum version %s", current_version, minimum_version); 266 RC_CLIENT_LOG_WARN(client, error_message); 267 version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); 268 } 269 else { 270 RC_CLIENT_LOG_INFO_FORMATTED(client, "Validated RA_Integration version %s (minimum %s)", current_version, minimum_version); 271 272 rc_client_init_raintegration(client, version_validation_callback_data); 273 } 274 } 275 276 rc_buffer_destroy(&response.buffer); 277 } 278 279 free(version_validation_callback_data->client_name); 280 free(version_validation_callback_data->client_version); 281 free(version_validation_callback_data); 282 } 283 284 rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client, 285 const wchar_t* search_directory, HWND main_window_handle, 286 const char* client_name, const char* client_version, 287 rc_client_callback_t callback, void* callback_userdata) 288 { 289 rc_client_version_validation_callback_data_t* callback_data; 290 rc_api_url_builder_t builder; 291 rc_api_request_t request; 292 293 if (!client) { 294 callback(RC_INVALID_STATE, "client is required", client, callback_userdata); 295 return NULL; 296 } 297 298 if (!client_name) { 299 callback(RC_INVALID_STATE, "client_name is required", client, callback_userdata); 300 return NULL; 301 } 302 303 if (!client_version) { 304 callback(RC_INVALID_STATE, "client_version is required", client, callback_userdata); 305 return NULL; 306 } 307 308 if (client->state.user != RC_CLIENT_USER_STATE_NONE) { 309 callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata); 310 return NULL; 311 } 312 313 if (!client->state.raintegration) { 314 if (!main_window_handle) { 315 callback(RC_INVALID_STATE, "main_window_handle is required", client, callback_userdata); 316 return NULL; 317 } 318 319 rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata); 320 if (!client->state.raintegration) 321 return NULL; 322 } 323 324 if (client->state.raintegration->get_host_url) { 325 const char* host_url = client->state.raintegration->get_host_url(); 326 if (host_url && strcmp(host_url, "https://retroachievements.org") != 0 && 327 strcmp(host_url, "OFFLINE") != 0) { 328 /* if the DLL specifies a custom host, use it */ 329 rc_client_set_host(client, host_url); 330 } 331 } 332 333 memset(&request, 0, sizeof(request)); 334 rc_api_url_build_dorequest_url(&request); 335 rc_url_builder_init(&builder, &request.buffer, 48); 336 rc_url_builder_append_str_param(&builder, "r", "latestintegration"); 337 request.post_data = rc_url_builder_finalize(&builder); 338 339 callback_data = calloc(1, sizeof(*callback_data)); 340 if (!callback_data) { 341 callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); 342 return NULL; 343 } 344 345 callback_data->client = client; 346 callback_data->callback = callback; 347 callback_data->callback_userdata = callback_userdata; 348 callback_data->client_name = strdup(client_name); 349 callback_data->client_version = strdup(client_version); 350 callback_data->main_window_handle = main_window_handle; 351 352 client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client); 353 return &callback_data->async_handle; 354 } 355 356 void rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle) 357 { 358 if (client && client->state.raintegration) { 359 client->state.raintegration->hMainWindow = main_window_handle; 360 361 if (client->state.raintegration->bIsInited && 362 client->state.raintegration->update_main_window_handle) { 363 client->state.raintegration->update_main_window_handle(main_window_handle); 364 } 365 } 366 } 367 368 void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler) 369 { 370 if (client && client->state.raintegration && client->state.raintegration->set_write_memory_function) 371 client->state.raintegration->set_write_memory_function(client, handler); 372 } 373 374 void rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler) 375 { 376 if (client && client->state.raintegration && client->state.raintegration->set_get_game_name_function) 377 client->state.raintegration->set_get_game_name_function(client, handler); 378 } 379 380 void rc_client_raintegration_set_event_handler(rc_client_t* client, 381 rc_client_raintegration_event_handler_t handler) 382 { 383 if (client && client->state.raintegration && client->state.raintegration->set_event_handler) 384 client->state.raintegration->set_event_handler(client, handler); 385 } 386 387 const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_client_t* client) 388 { 389 if (!client || !client->state.raintegration || 390 !client->state.raintegration->bIsInited || 391 !client->state.raintegration->get_menu) { 392 return NULL; 393 } 394 395 return client->state.raintegration->get_menu(); 396 } 397 398 void rc_client_raintegration_set_console_id(rc_client_t* client, uint32_t console_id) 399 { 400 if (client && client->state.raintegration && client->state.raintegration->set_console_id) 401 client->state.raintegration->set_console_id(console_id); 402 } 403 404 int rc_client_raintegration_has_modifications(const rc_client_t* client) 405 { 406 if (!client || !client->state.raintegration || 407 !client->state.raintegration->bIsInited || 408 !client->state.raintegration->has_modifications) { 409 return 0; 410 } 411 412 return client->state.raintegration->has_modifications(); 413 } 414 415 int rc_client_raintegration_get_achievement_state(const rc_client_t* client, uint32_t achievement_id) 416 { 417 if (!client || !client->state.raintegration || 418 !client->state.raintegration->bIsInited || 419 !client->state.raintegration->get_achievement_state) { 420 return RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_NONE; 421 } 422 423 return client->state.raintegration->get_achievement_state(achievement_id); 424 } 425 426 void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu) 427 { 428 HMENU hPopupMenu = NULL; 429 const rc_client_raintegration_menu_t* menu; 430 431 if (!client || !client->state.raintegration) 432 return; 433 434 /* destroy the existing menu */ 435 if (client->state.raintegration->hPopupMenu) 436 DestroyMenu(client->state.raintegration->hPopupMenu); 437 438 /* create the popup menu */ 439 hPopupMenu = CreatePopupMenu(); 440 441 menu = rc_client_raintegration_get_menu(client); 442 if (menu && menu->num_items) 443 { 444 const rc_client_raintegration_menu_item_t* menuitem = menu->items; 445 const rc_client_raintegration_menu_item_t* stop = menu->items + menu->num_items; 446 447 for (; menuitem < stop; ++menuitem) 448 { 449 if (menuitem->id == 0) 450 AppendMenuA(hPopupMenu, MF_SEPARATOR, 0U, NULL); 451 else 452 { 453 UINT flags = MF_STRING; 454 if (menuitem->checked) 455 flags |= MF_CHECKED; 456 if (!menuitem->enabled) 457 flags |= MF_GRAYED; 458 459 AppendMenuA(hPopupMenu, flags, menuitem->id, menuitem->label); 460 } 461 } 462 } 463 464 /* add/update the item containing the popup menu */ 465 { 466 int nIndex = GetMenuItemCount(hMenu); 467 const char* menuText = "&RetroAchievements"; 468 char buffer[64]; 469 470 UINT flags = MF_POPUP | MF_STRING; 471 if (!menu || !menu->num_items) 472 flags |= MF_GRAYED; 473 474 while (--nIndex >= 0) 475 { 476 if (GetMenuStringA(hMenu, nIndex, buffer, sizeof(buffer) - 1, MF_BYPOSITION)) 477 { 478 if (strcmp(buffer, menuText) == 0) 479 break; 480 } 481 } 482 483 if (nIndex == -1) 484 AppendMenuA(hMenu, flags, (UINT_PTR)hPopupMenu, menuText); 485 else 486 ModifyMenuA(hMenu, nIndex, flags | MF_BYPOSITION, (UINT_PTR)hPopupMenu, menuText); 487 488 if (client->state.raintegration->hMainWindow && GetMenu(client->state.raintegration->hMainWindow) == hMenu) 489 DrawMenuBar(client->state.raintegration->hMainWindow); 490 } 491 492 client->state.raintegration->hPopupMenu = hPopupMenu; 493 } 494 495 void rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menuitem) 496 { 497 if (client && client->state.raintegration && client->state.raintegration->hPopupMenu) 498 { 499 UINT flags = MF_STRING; 500 if (menuitem->checked) 501 flags |= MF_CHECKED; 502 503 CheckMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); 504 505 flags = (menuitem->enabled) ? MF_ENABLED : MF_GRAYED; 506 EnableMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); 507 } 508 } 509 510 int rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t menu_item_id) 511 { 512 if (!client || !client->state.raintegration || !client->state.raintegration->activate_menu_item) 513 return 0; 514 515 return client->state.raintegration->activate_menu_item(menu_item_id); 516 } 517 518 void rc_client_unload_raintegration(rc_client_t* client) 519 { 520 HINSTANCE hDLL; 521 522 if (!client || !client->state.raintegration) 523 return; 524 525 RC_CLIENT_LOG_INFO(client, "Unloading RA_Integration") 526 527 if (client->state.external_client && client->state.external_client->destroy) 528 client->state.external_client->destroy(); 529 530 if (client->state.raintegration->shutdown) 531 client->state.raintegration->shutdown(); 532 533 rc_mutex_lock(&client->state.mutex); 534 hDLL = client->state.raintegration->hDLL; 535 client->state.raintegration = NULL; 536 client->state.external_client = NULL; 537 rc_mutex_unlock(&client->state.mutex); 538 539 if (hDLL) 540 FreeLibrary(hDLL); 541 } 542 543 #endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */