Skip to content

rc_client_raintegration

Jamiras edited this page May 10, 2024 · 11 revisions

RAIntegration is the toolkit used to create and test achievements for the RetroAchievements platform. This page should help guide you through integrating it into an emulator that is already built to use rc_client.

If you want to integrate with the toolkit without implementing rc_client, see the legacy RAIntegration guide.

Loading the DLL

RAIntegration is provided via a DLL that can be loaded at runtime. The first thing you'll need to do is extend the initialization method to load the DLL.

#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
static void load_integration_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  switch (result)
  {
    case RC_OK:
      // DLL was loaded.
      // TODO: hook up menu and dll event handlers
      break;

    case RC_MISSING_VALUE:
      // DLL is not present. Do nothing. The user can still play without the toolkit.
      break;

    default:
      // If not successful, just report the error and bail.
      show_message("DLL load failed: %s", error_message);
      return;
  }

  // Things are ready to load a game. If the DLL was initialized, calling rc_client_begin_login_with_token and 
  // rc_client_begin_load_game will be redirected through the DLL so the toolkit has access to the user and game
  // data. If the DLL was not initialized, they'll be handled by rc_client directly. Similarly, things like 
  // rc_create_leaderboard_list will be redirected through the DLL to reflect any local changes made by the user.
}
#endif

void initialize_retroachievements_client(void)
{
  ...

#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
  // attempt to load the integration DLL from the directory containing the main client executable
  // in x64 build, will look for RA_Integration-x64.dll, then RA_Integration.dll.
  // in non-x64 build, will only look for RA_Integration.dll
  {
    wchar_t szFilePath[MAX_PATH];
    GetModuleFileNameW(NULL, szFilePath, MAX_PATH);
    PathRemoveFileSpecW(szFilePath);

    rc_client_begin_load_raintegration(g_client, szFilePath, g_hWnd,
        "MyClient", "1.0", load_integration_callback, NULL);
  }
#endif
}

Notice the use of #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION? This ensures the code can still be built on non-Windows platforms where the DLL cannot be supported. As such, you must define RC_CLIENT_SUPPORTS_RAINTEGRATION when building the code (both the client code and the rcheevos code) to enable the functionality. The DLL is currently very heavily tied to Windows, so you should only define RC_CLIENT_SUPPORTS_RAINTEGRATION in Windows builds.

Hooking up the menu

Once you've got the DLL loading, you need to add the RetroAchievements menu somewhere in the program. If you're using the WINAPI to build your Window, there's a handy helper function that you can use.

static void load_integration_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  ...

    case RC_OK:
      // DLL was loaded.
      // TODO: hook up menu and dll event handlers
      rc_client_raintegration_rebuild_submenu(g_client, GetMenu(g_hWnd));
      break;

  ...
}

You also need to handle when the menu items are selected. Find the code where the program is handling the existing menu items and add something similar to this:

    ...
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
    else if (rc_client_raintegration_activate_menu_item(LOWORD(wParam)))
    {
      return 0; // DLL menu item was handled
    }
 #endif
    ...

If you're using a frontend other than Windows directly (like QT), you can call rc_client_raintegration_get_menu and convert those objects into a UI elements appropriate for your selected UI framework. Activating the menu items should still call rc_client_raintegration_activate_menu_item with the menu item's ID.

At this point, you should be able to open the tool windows from the menu.

Handling RAIntegration-specific Events

There are a few events specific to the DLL that the client needs to handle.

static void raintegration_event_handler(const rc_client_raintegration_event_t* event, rc_client_t* client)
{
   switch (event->type)
   {
      case RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED:
         // The checked state of one of the menu items has changed and should be reflected in the UI.
         // Call the handy helper function if the menu was created by rc_client_raintegration_rebuild_submenu.
         rc_client_raintegration_update_menu_item(client, event->menu_item);
         break;
      case RC_CLIENT_RAINTEGRATION_EVENT_PAUSE:
         // The toolkit has hit a breakpoint and wants to pause the emulator. Do so.
         pause_emulator();
         break;
      case RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED:
         // Hardcore mode has been changed (either directly by the user, or disabled through the use of the tools).
         // The frontend doesn't necessarily need to know that this value changed, they can still query it whenever
         // it's appropriate, but the event lets the frontend do things like enable/disable rewind or cheats.
         handle_hardcore_changed();
         break;
      default:
         log_message("Unsupported raintegration event %u\n", event->type);
         break;
   }
}

static void load_integration_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  ...

    case RC_OK:
      // DLL was loaded.
      rc_client_raintegration_set_event_handler(g_client, raintegration_event_handler);
      rc_client_raintegration_rebuild_submenu(g_client, GetMenu(g_hWnd));
      break;

  ...
}

Memory Modification

We also need to allow the toolkit to directly modify memory. This works in a very similar manner to reading memory.

static void raintegration_write_memory_handler(uint32_t address, uint8_t* buffer,
                                               uint32_t num_bytes, rc_client_t* client)
{
  uint32_t real_address = convert_retroachievements_address_to_real_address(address);
  emulator_write_memory(real_address, buffer, num_bytes);
}

static void load_integration_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  ...

    case RC_OK:
      // DLL was loaded.
      rc_client_raintegration_set_event_handler(g_client, raintegration_event_handler);
      rc_client_raintegration_set_write_memory_function(g_client, raintegration_write_memory_handler);
      rc_client_raintegration_rebuild_submenu(g_client, GetMenu(g_hWnd));
      break;

  ...
}

Unlocking Local Achievements

RAIntegration allows creating local achievements, and modifying published achievements. To better differentiate the achievement being unlocked, you should change the popup messaging slightly.

static void achievement_triggered(const rc_client_achievement_t* achievement)
{
  ...

  if (achievement->category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL)
     message = "Unofficial Achievement Unlocked";
#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
  else
  {
    switch (rc_client_raintegration_get_achievement_state(g_client, achievement->id))
    {
      case RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_LOCAL:
        // This achievement only exists on the user's local machine.
        message = "Local Achievement Unlocked";
        break;
      case RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_MODIFIED:
        // This achievement differs from the published version.
        // Indicate the achievement was only unlocked locally,
        // and specify it's because of the modifications.
        message = "Modified Achievement Unlocked Locally";
        break;
      case RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_INSECURE:
        // The user has done something that we consider cheating
        // like modifying the RAM while playing. Just indicate
        // that the achievement was only unlocked locally, but
        // don't clarify why.
        message = "Achievement Unlocked Locally";
        break;
      default:
        // Unmodified published achievement (RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_PUBLISHED)
        // or some state we're not familiar with - just show "Achievement Unlocked"
        break;
    }
  }
#endif

  show_popup_message(image_data, message, achievement->title);

  ...
}

Handling Unrecognized Games

When a user loads a game that the server doesn't recognize, the toolkit will present them with a dialog that allows them to select the game from the supported games list and play the game in a compatibility testing mode. This dialog asks the frontend for a description of the game to help the user find an appropriate match. The description is usually just the filename without the path or extension.

static void raintegration_get_game_name_handler(char* buffer, uint32_t buffer_size, rc_client_t* client)
{
   snprintf(buffer, buffer_size, get_filename(g_loaded_game));
   remove_extension(buffer);
}

static void load_integration_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  ...

    case RC_OK:
      // DLL was loaded.
      rc_client_raintegration_set_event_handler(g_client, raintegration_event_handler);
      rc_client_raintegration_set_write_memory_function(g_client, raintegration_write_memory_handler);
      rc_client_raintegration_set_get_game_name_function(g_client, raintegration_get_game_name_handler);
      rc_client_raintegration_rebuild_submenu(g_client, GetMenu(g_hWnd));
      break;

  ...
}

If calling rc_client_begin_load_game instead of rc_client_begin_identify_and_load_game, you must also explicitly set the console so the appropriate list of games will appear in the dialog.

void load_game(const char* hash)
{
  rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_SUPER_NINTENDO);
  rc_client_begin_load_game(g_client, hash, load_game_callback, NULL);
}

Preventing Loss of Changes

Before unloading a game, the client should check for modifications and prompt the user whether or not to proceed with unloading the game.

int raintegration_confirm_unload_game()
{
  if (rc_client_raintegration_has_modifications(g_client)) {
    if (!confirm("Are you sure you want to close this game? Uncommitted modifications will be lost."))
      return 0;
  }

  rc_client_unload_game(g_client);
  return 1;
}

Other Things to Consider

If, for whatever reason, you destroy the main window and create a new one, you should let the DLL know by calling rc_client_raintegration_update_main_window_handle.

rcheevos

rc_client

Integration guide

client

user

game

processing

rc_client_raintegration

Integration guide

rc_runtime

rhash

rapi

common

user

runtime

info

Clone this wiki locally