Skip to content

rc_client integration

Jamiras edited this page Jun 30, 2024 · 32 revisions

This page should help guide you through integrating the RetroAchievements runtime into an emulator. It's not meant to provide a 100% solution.

If you're working on a system that is not yet supported by RetroAchievements, you'll need to integrate with the toolkit, not just the runtime. The toolkit provides the additional functionality necessary to create achievements, but is currently Windows-only.

The runtime documented here should work on any platform, but does not provide any capabilities for creating/editing achievements. Additionally, it does not provide any UI or networking code. The emulator it is being integrated into must provide those.

Creating the client object

Before you can do anything, you need to create a client object that can communicate with the server. Since the constructor requires a read_memory function too, we'll just provide a dummy.

This provides a simple implementation of the rc_client_server_call callback wrapping a non-existant HTTP library. The emulator is responsible for providing the actual HTTP/HTTPS code.

rc_client_t* g_client = NULL;

// This is the function the rc_client will use to read memory for the emulator. we don't need it yet,
// so just provide a dummy function that returns "no memory read".
static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client)
{
  // TODO: implement later
  return 0;
}

// This is the callback function for the asynchronous HTTP call (which is not provided in this example)
static void http_callback(int status_code, const char* content, size_t content_size, void* userdata, const char* error_message)
{
  // Prepare a data object to pass the HTTP response to the callback
  rc_api_server_response_t server_response;
  memset(&server_response, 0, sizeof(server_response));
  server_response.body = content;
  server_response.body_length = content_size;
  server_response.http_status_code = status_code;

  // handle non-http errors (socket timeout, no internet available, etc)
  if (status_code == 0 && error_message) {
      // assume no server content and pass the error through instead
      server_response.body = error_message;
      server_response.body_length = strlen(error_message);
      // Let rc_client know the error was not catastrophic and could be retried. It may decide to retry or just 
      // immediately pass the error to the callback. To prevent possible retries, use RC_API_SERVER_RESPONSE_CLIENT_ERROR.
      server_response.http_status_code = RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR;
  }

  // Get the rc_client callback and call it
  async_callback_data* async_data = (async_callback_data*)userdata;
  async_data->callback(&server_response, async_data->callback_data);

  // Release the captured rc_client callback data
  free(async_data);
}

// This is the HTTP request dispatcher that is provided to the rc_client. Whenever the client
// needs to talk to the server, it will call this function.
static void server_call(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 = "MyClient/1.2";

  // callback must be called with callback_data, regardless of the outcome of the HTTP call.
  // Since we're making the HTTP call asynchronously, we need to capture them and pass it
  // through the async HTTP code.
  async_callback_data* async_data = malloc(sizeof(async_callback_data));
  async_data->callback = callback;
  async_data->callback_data = callback_data;

  // If post data is provided, we need to make a POST request, otherwise, a GET request will suffice.
  if (request->post_data)
    async_http_post(request->url, request->post_data, user_agent, http_callback, async_data);
  else
    async_http_get(request->url, user_agent, http_callback, async_data);
}

// Write log messages to the console
static void log_message(const char* message, const rc_client_t* client)
{
  printf("%s\n", message);
}

void initialize_retroachievements_client(void)
{
  // Create the client instance (using a global variable simplifies this example)
  g_client = rc_client_create(read_memory, server_call);

  // Provide a logging function to simplify debugging
  rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message);

  // Disable hardcore - if we goof something up in the implementation, we don't want our
  // account disabled for cheating.
  rc_client_set_hardcore_enabled(g_client, 0);
}

void shutdown_retroachievements_client(void)
{
  if (g_client)
  {
    // Release resources associated to the client instance
    rc_client_destroy(g_client);
    g_client = NULL;
  }  
}

At this point, you should be able to create and destroy an rc_client_t instance, but it's not doing anything yet.

Note: To pass custom data to the server_call_function, read_memory_function, or any other callback, you can attach a single object the rc_client_t instance via rc_client_set_userdata. That can then be retrieved in the callback by calling rc_client_get_userdata.

Login

The first thing we need to do is log in the user. This will trigger the server_call callback to make a server request and validate a full end-to-end server interaction.

static void login_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  // If not successful, just report the error and bail.
  if (result != RC_OK)
  {
    show_message("Login failed: %s", message);
    return;
  }

  // Login was successful. Capture the token for future logins so we don't have to store the password anywhere.
  const rc_client_user_t* user = rc_client_get_user_info(client);
  store_retroachievements_credentials(user->username, user->token);

  // Inform user of successful login
  show_message("Logged in as %s (%u points)", user->display_name, user->score);
}

void login_retroachievements_user(const char* username, const char* password)
{
  // This will generate an HTTP payload and call the server_call chain above.
  // Eventually, login_callback will be called to let us know if the login was successful.
  rc_client_begin_login_with_password(g_client, username, password, login_callback, NULL);
}

void login_remembered_retroachievements_user(const char* username, const char* token)
{
  // This is exactly the same functionality as rc_client_begin_login_with_password, but
  // uses the token captured from the first login instead of a password.
  // Note that it uses the same callback.
  rc_client_begin_login_with_token(g_client, username, token, login_callback, NULL);
}

It's up to you how you want to show the user's information on successful login. Here are some examples:

RALibretro

image

RetroArch

image

PCSX2

PCSX2 does not announce the user login.

Starting a game session

Now that we have working server communication, we're ready to start a game session. Much like login, this process is asynchronous and will involve communicating with the server.

static void load_game_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  if (result != RC_OK)
  {
    show_message("RetroAchievements game load failed: %s", error_message);
    return;
  }

  // announce that the game is ready. we'll cover this in the next section.
  show_game_placard();
}

void load_game(const uint8_t* rom, size_t rom_size)
{
  // this example is hard-coded to identify a Super Nintendo game already loaded in memory.
  // it will use the rhash library to generate a hash, then make a server call to resolve
  // the hash to a game_id. If found, it will then fetch the game data and start a session
  // for the user. By the time load_game_callback is called, the achievements for the game are
  // ready to be processed (unless an error occurs, like not being able to identify the game).
  rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_SUPER_NINTENDO, 
      NULL, rom, rom_size, load_game_callback, NULL);
}

void load_game_from_file(const char* path)
{
  // this example uses RC_CONSOLE_UNKNOWN, which tells the rc_client to attempt to determine
  // the system associated to the game from the filename and contents of the file. If the
  // console is known, it's always preferred to pass it in.
  rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, 
      path, NULL, 0, load_game_callback, NULL);
}

Showing the game placard

You should always inform the user when achievements become active. The amount of detail provided is up to you.

Some games have logic that requires seeing the user at the title screen, and if the user starts the game before the achievements see that, the achievements will fail to work as expected.

static void show_game_placard(void)
{
  char message[128], url[128];
  async_image_data* image_data = NULL;
  const rc_client_game_t* game = rc_client_get_game_info(g_client);
  rc_client_user_game_summary_t summary;
  rc_client_get_user_game_summary(g_client, &summary);

  // Construct a message indicating the number of achievements unlocked by the user.
  if (summary.num_core_achievements == 0)
  {
    snprintf(message, sizeof(message), "This game has no achievements.");
  }
  else if (summary.num_unsupported_achievements)
  {
    snprintf(message, sizeof(message), "You have %u of %u achievements unlocked (%d unsupported).",
        summary.num_unlocked_achievements, summary.num_core_achievements,
        summary.num_unsupported_achievements);
  }
  else
  {
    snprintf(message, sizeof(message), "You have %u of %u achievements unlocked.",
        summary.num_unlocked_achievements, summary.num_core_achievements);
  }

  // The emulator is responsible for managing images. This uses rc_client to build
  // the URL where the image should be downloaded from.
  if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK)
  {
    // Generate a local filename to store the downloaded image.
    char game_badge[64];
    snprintf(game_badge, sizeof(game_badge), "game_%s.png", game->badge_name);

    // This function will download and cache the game image. It is up to the emulator
    // to implement this logic. Similarly, the emulator has to use image_data to
    // display the game badge in the placard, or a placeholder until the image is
    // downloaded. None of that logic is provided in this example.
    image_data = download_and_cache_image(game_badge, url);
  } 

  show_popup_message(image_data, game->title, message);
}

Here are some examples of game placards:

RALibretro

image

RetroArch

image

PCSX2

image

Viewing the Achievement List

After getting the game loaded, the next thing you should do is show the achievement list.

void show_achievements_menu(void)
{
  char url[128];
  const char* progress;

  // This will return a list of lists. Each top-level item is an achievement category
  // (Active Challenge, Unlocked, etc). Empty categories are not returned, so we can
  // just display everything that is returned.
  rc_client_achievement_list_t* list = rc_client_create_achievement_list(g_client,
      RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
      RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);

  // Clear any previously loaded menu items
  menu_reset();

  for (int i = 0; i < list->num_buckets; i++)
  {
    // Create a header item for the achievement category
    menu_append_item(NULL, list->buckets[i].label, "");

    for (int j = 0; j < list->buckets[i].num_achievements; j++)
    {
      const rc_client_achievement_t* achievement = list->buckets[i].achievements[j];
      async_image_data* image_data = NULL;

      if (rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK)
      {
         // Generate a local filename to store the downloaded image.
         char achievement_badge[64];
         snprintf("ach_%s%s.png", achievement->badge_name, 
                  (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? "" : "_lock");
         image_data = download_and_cache_image(achievement_badge, url);
      } 

      // Determine the "progress" of the achievement. This can also be used to show
      // locked/unlocked icons and progress bars.
      if (list->buckets[i].id == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED)
        progress = "Unsupported";
      else if (achievement->unlocked)
        progress = "Unlocked";
      else if (achievement->measured_percent)
        progress = achievement->measured_progress;
      else
        progress = "Locked";

      menu_append_item(image_data, achievement->title, achievement->description, progress);
    }
  }

  rc_client_destroy_achievement_list(list);
}

Here are some examples of portions of achievement lists illustrating separation headers:

RALibretro

image

RetroArch

image

PCSX2

image

Logic Processing

Now that we can see the achievement information, let's start processing them.

First, we need to register an event handler with the client. The easiest place to do that is in the constructor code we wrote earlier. The event handler will just log the event for now.

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  printf("Event! (%d)\n", event->type);
}

void initialize_retroachievements_client(void)
{
  g_client = rc_client_create(read_memory, server_call);

  rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message);

  // Provide an event handler
  rc_client_set_event_handler(g_client, event_handler);

  rc_client_set_hardcore_enabled(g_client, 0);
}

We also need to implement the read_memory function we stubbed in earlier.

static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client)
{
  // RetroAchievements addresses start at $00000000, which normally represents the first physical byte
  // of memory for the system. Normally, an emulator stores it's RAM in a singular contiguous buffer,
  // which makes reading from a RetroAchievements address simple:
  //
  //   if (address + num_bytes >= RAM_size)
  //     return 0; 
  //   memcpy(buffer, &RAM[address], num_bytes);
  //   return num_bytes;
  //
  // Sometimes an emulator only exposes a virtual BUS. In that case, it may be necessary to translate
  // the RetroAchievements address to a real address.
  uint32_t real_address = convert_retroachievements_address_to_real_address(address);
  return emulator_read_memory(real_address, buffer, num_bytes);
}

Then, after each frame is rendered, we need to process achievements:

  rc_client_do_frame(g_client);

At this point, the runtime should be fully functional. You should be able to unlock achievements and submit leaderboard entries (though you'll only see a log message instead of any sort of on-screen message). You should also be able to send updated rich presence to the server (first update after thirty seconds, then every two minutes).

Next, we need to start hooking up the UI.

Unlocking Achievements

When an achievement is unlocked, the registered event handler will be called with a RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED message. Here's the relevant expanded event_handler code and the unlock handler:

static void achievement_triggered(const rc_client_achievement_t* achievement)
{
  char url[128];
  const char* message = "Achievement Unlocked";
  async_image_data* image_data = NULL;

  // the runtime already took care of dispatching the server request to notify the
  // server, we just have to tell the player.

  if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) == RC_OK)
  {
     // Generate a local filename to store the downloaded image.
     char achievement_badge[64];
     snprintf("ach_%s.png", achievement->badge_name);
     image_data = download_and_cache_image(achievement_badge, url);
  }

  if (achievement->category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL)
     message = "Unofficial Achievement Unlocked";

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

  // it's nice to also give an audio cue when an achievement is unlocked
  play_sound("unlock.wav");
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
      achievement_triggered(event->achievement);
      break;

    default:
      printf("Unhandled event %d\n", event->type);
      break;
  }
}

Here are some examples of achievement unlock popups:

RALibretro

image

RetroArch

image

PCSX2

image

Submitting Leaderboard Entries

Similar to unlocking achievements, when leaderboards are submitted the registered event handler is called with a RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED event.

Let's go ahead and hook up the started (RC_CLIENT_EVENT_LEADERBOARD_STARTED) and failed (RC_CLIENT_EVENT_LEADERBOARD_FAILED) messages while we're here.

You can decide which of these messages you want to display, or add configuration toggles to let the user decide. The events will always be raised, it's up to you to decide how to handle them.

static void leaderboard_started(const rc_client_leaderboard_t* leaderboard)
{
  show_message("Leaderboard attempt started: %s - %s", leaderboard->title, leaderboard->description);
}

static void leaderboard_failed(const rc_client_leaderboard_t* leaderboard)
{
  show_message("Leaderboard attempt failed: %s", leaderboard->title);
}

static void leaderboard_submitted(const rc_client_leaderboard_t* leaderboard)
{
  show_message("Submitted %s for %s", leaderboard->tracker_value, leaderboard->title);
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
      leaderboard_started(event->leaderboard);
      break;
    case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
      leaderboard_failed(event->leaderboard);
      break;
    case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
      leaderboard_submitted(event->leaderboard);
      break;
    // ...
  }
}

Leaderboard Trackers

The first widget you should implement is the on-screen tracker for the leaderboard. There are three events associated to it.

static void leaderboard_tracker_update(const rc_client_leaderboard_tracker_t* tracker)
{
  // Find the currently visible tracker by ID and update what's being displayed.
  tracker_data* data = find_tracker(tracker->id);
  if (data)
  {
    // The display text buffer is guaranteed to live for as long as the game is loaded,
    // but it may be updated in a non-thread safe manner within rc_client_do_frame, so
    // we create a copy for the rendering code to read.
    strcpy(data->value, tracker->display);
  }
}

static void leaderboard_tracker_show(const rc_client_leaderboard_tracker_t* tracker)
{
  // The actual implementation of converting an rc_client_leaderboard_tracker_t to
  // an on-screen widget is going to be client-specific. The provided tracker object
  // has a unique identifier for the tracker and a string to be displayed on-screen.
  // The string should be displayed using a fixed-width font to eliminate jittering
  // when timers are updated several times a second.
  create_tracker(tracker->id, tracker->display);
}

static void leaderboard_tracker_hide(const rc_client_leaderboard_tracker_t* tracker)
{
  // This tracker is no longer needed
  destroy_tracker(tracker->id);
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
      leaderboard_tracker_update(event->leaderboard_tracker);
      break;
    case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
      leaderboard_tracker_show(event->leaderboard_tracker);
      break;
    case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
      leaderboard_tracker_hide(event->leaderboard_tracker);
      break;
    // ...
  }
}

Here are some examples of achievement leaderboard trackers:

RALibretro

image

RetroArch

image

Challenge Indicators

The next widget is the challenge indicator. It normally appears as a small version of the unlocked icon.

static void challenge_indicator_show(const rc_client_achievement_t* achievement)
{
  char url[128];
  async_image_data* image_data = NULL;

  if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) == RC_OK)
  {
     // Generate a local filename to store the downloaded image.
     char achievement_badge[64];
     snprintf("ach_%s.png", achievement->badge_name);
     image_data = download_and_cache_image(achievement_badge, url);
  }

  // Multiple challenge indicators may be shown, but only one per achievement, so key the list on the achievement ID
  create_challenge_indicator(achievement->id, image_data);
}

static void challenge_indicator_hide(const rc_client_achievement_t* achievement)
{
  // This indicator is no longer needed
  destroy_challenge_indicator(achievement->id);
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
      challenge_indicator_show(event->achievement);
      break;
    case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
      challenge_indicator_hide(event->achievement);
      break;
    // ...
  }
}

Here are some examples of challenge indicators:

RALibretro

image

RetroArch

image

PCSX2

image

Progress Indicator

The next widget is the progress indicator. It normally appears as a small popup with the locked achievement icon and the progress text.

Only one progress indicator should be shown at a time. The runtime will raise events whenever a measured achievement gains progress. If multiple achievements gain progress in the same frame, only the closest to completion will raise an event.

static void progress_indicator_update(const rc_client_achievement_t* achievement)
{
  char url[128];
  async_image_data* image_data = NULL;

  if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK)
  {
     // Generate a local filename to store the downloaded image.
     char achievement_badge[64];
     snprintf("ach_%s_lock.png", achievement->badge_name);
     image_data = download_and_cache_image(achievement_badge, url);
  }

  // The UPDATE event assumes the indicator is already visible, and just asks us to update the image/text.
  update_progress_indicator(image_data, achievement->measured_progress);
}

static void progress_indicator_show(const rc_client_achievement_t* achievement)
{
  // The SHOW event tells us the indicator was not visible, but should be now.
  // To reduce duplicate code, we just update the non-visible indicator, then show it.
  progress_indicator_update(achievement);
  show_progress_indicator();
}

static void progress_indicator_hide(void)
{
  // The HIDE event indicates the indicator should no longer be visible.
  hide_progress_indicator();
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
      progress_indicator_show(event->achievement);
      break;
    case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
      progress_indicator_update(event->achievement);
      break;
    case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
      progress_indicator_hide();
      break;
    // ...
  }
}

Here are some examples of progress indicators:

RALibretro

image

RetroArch

image

Mastery notification

The last widget is the mastery notification

static void game_mastered(void)
{
  char message[128], submessage[128];
  char url[128];
  async_image_data* image_data = NULL;
  const rc_client_game_t* game = rc_client_get_game_info(g_client);

  if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK)
  {
    // Generate a local filename to store the downloaded image.
    char game_badge[64];
    snprintf("game_%s.png", game->badge_name);
    image_data = download_and_cache_image(game_badge, url);
  }

  // The popup should say "Completed GameTitle" or "Mastered GameTitle",
  // depending on whether or not hardcore is enabled.
  snprintf(message, sizeof(message), "%s %s", 
      rc_client_get_hardcore_enabled(g_client) ? "Mastered" : "Completed",
      game->title);

  // You should also display the name of the user (for personalized screenshots).
  // If the emulator keeps track of the user's per-game playtime, it's nice to
  // display that too.
  snprintf(submessage, sizeof(submessage), "%s (%s)",
      rc_client_get_user_info(g_client)->display_name,
      format_total_playtime());

  show_popup_message(image_data, message, submessage);

  play_sound("mastery.wav");
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_GAME_COMPLETED:
      game_mastered();
      break;
    // ...
  }
}

Here are some examples of mastery notifications:

RALibretro

image

RetroArch

image

Resetting the emulator

When the emulator is reset, the runtime should be as well. Just call rc_client_reset:

 rc_client_reset(g_client);

Managing save states

Whenever a save state is created, the runtime state should be included. Similarly, when a save state is loaded, the runtime state should be restored.

When loading a save state that does not have runtime state information, rc_client_deserialize_progress should be called with NULL to reset the runtime state.

This implementation is focused on save states written to disk, but you should also capture and restore the runtime state when making internal save states (such as for rewind or network synchronization).

void capture_retroachievements_state(FILE* file)
{
  const uint32_t buffer_size = (uint32_t)rc_client_progress_size(g_client);
  uint8_t* buffer = (uint8_t*)malloc(buffer_size);
  if (rc_client_serialize_progress(g_client, buffer) == RC_OK) {
    // write a marker, the buffer size, then the buffer contents
    fwrite("RCHV", 1, 4, file);
    fwrite(&buffer_size, 1, sizeof(buffer_size), file); 
    fwrite(buffer, 1, buffer_size, file);
  }
  free(buffer);
}

int restore_retroachievements_state(FILE* file)
{
  char marker[4];
  uint32_t buffer_size;
  uint8_t* buffer = NULL;
  int result = 0;
   
  if (fread(marker, 1, 4, file) == 4 && memcmp(marker, "RCHV") == 0) {
    // found achievement data marker
    if (fread(&buffer_size, 1, sizeof(buffer_size), file) == sizeof(buffer_size)) {
      // allocate buffer
      buffer = (uint8_t*)malloc(buffer_size);
      if (buffer) {
        if (fread(buffer, 1, buffer_size, file) != buffer_size) {
          // failed to fill buffer, discard buffer
          free(buffer);
          buffer = NULL;
        }
      }
    }
  }      

  result = (rc_client_deserialize_progress(g_client, buffer) == RC_OK) ? 1 : 0;

  if (buffer)
    free(buffer);

  return result;
}

Handling multiple media

Some systems support games that span multiple discs/disks. When switching, it is important to validate the additional media as well.

static void media_changed_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
{
  // on success, do nothing
  if (result == RC_OK)
    return;

  if (result == RC_HARDCORE_DISABLED)
  {
    show_message("Hardcore disabled. Unrecognized media inserted.");
  }
  else
  {
    if (!error_message)
      error_message = rc_error_str(result);

    show_message("RetroAchievements media change failed: %s", error_message);
  }
}

void media_changed(const char* new_media_path)
{
  rc_client_begin_change_media(g_client, new_media_path, NULL, 0, media_changed_callback, NULL);
}

Enabling hardcore

Now that all the basic functionality is in place, it's time to support hardcore.

First, remove the code that explicitly disables hardcore when initializing the client object. The runtime will start in hardcore mode.

When in hardcore mode, you must limit any emulator features that could be seen as providing an advantage to the user. Common features that are hardcore-restricted are:

  • Loading save states (saving save states is okay)
  • Cheats that modify gameplay (i.e. GameGenie codes or widescreen enhancements) (upscaling is okay)
  • Rewind
  • Slowdown/frame advance (fast forward is okay)
  • Debugger windows (especially viewing memory)
  • Playing back recorded inputs (recording inputs is okay)

To allow those functions in your emulator, you need to provide some way for the player to enable/disable hardcore mode. It can either be a full-time switch (remembered between games/sessions), or a temporary switch (reset after each game), or a combination of the two.

NOTE: Hardcore should be on by default. Players who opt-in to hardcore are usually already mostly abiding by hardcore restrictions, and are disappointed when they find out they have to replay everything again to get the hardcore unlocks. Whereas if we opt-in for them, they'll find the functionality restricted (usually with a warning to disable hardcore) and can naturally make the decision themselves.

To change the hardcore state, call rc_client_set_hardcore_enabled. When hardcore is enabled, the runtime will raise a RC_CLIENT_EVENT_RESET event requesting the emulator reset itself. The runtime will be disabled until rc_client_reset is called.

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_RESET:
      reset_emulator();
      break;
    // ...
  }
}

Automatically disable hardcore for games without achievements

If a game doesn't have any RetroAchievements functionality (achievements, leaderboards, or rich presence), it is recommended to disable hardcore while the game is loaded. To see if the game has RetroAchievements functionality, call rc_client_is_processing_required.

IMPORTANT: If you do automatically disable hardcore for a game without achievements, you should re-enable it when the game is unloaded (or at least before loading the next game).

Emulator Pause

When the emulator is paused (for whatever reason), you should not be calling rc_client_do_frame. Instead, you should call rc_client_idle - preferably at least one a second. rc_client_idle manages routine communication with the server to ensure the player remains in the Active Players list. It's already called internally by rc_client_do_frame, so you only need to explicitly call it when you're not calling rc_client_do_frame.

When the emulator is paused, the screen should be obscured - usually with a surface that is at least 70% opaque showing information about the emulator, game, or achievements. The minimizes the player's ability to analyze the screen while circumventing in-game time limitations. This is not a hard requirement because the player can just take a screenshot before pausing and still analyze things while the emulator is paused.

Preventing Pause spam

To prevent a user from mimicking slow motion by spamming the emulator pause button, we introduce a delay between successive pauses in hardcore mode. The runtime provides a helper function that counts how many times rc_client_do_frame is called between calls to the helper function. It should be called just before any time that the emulator tries to pause. If it returns false, the emulator should not allow the pause.

static void emulator_pause()
{
  if (!rc_client_can_pause(g_client, NULL))
    return;

  // actual emulator pausing code goes here.
}

Server Errors

When a request times out, the server callback should be called with a NULL response_body. For client initiated functionality (like loading the game), the callback will be called with an error indicator. For things like achievement unlocks, the server will automatically queue up the request to happen again after a delay. Queued up retries will be dispatched to the server_call handler during the rc_client_do_frame or rc_client_idle calls.

For non-client initiated functionality (like achievement unlocks), if the server response contains an actual error message, an RC_CLIENT_EVENT_SERVER_ERROR event will be raised. These should be reported to the player, as the request will not be requeued.

static void server_error(const rc_client_server_error_t* error)
{
  char buffer[256];
  snprintf(buffer, "%s: %s", error->api, error->error_message);
  show_message(buffer);
}

static void event_handler(const rc_client_event_t* event, rc_client_t* client)
{
  switch (event->type)
  {
    // ...
    case RC_CLIENT_EVENT_SERVER_ERROR:
      server_error(event->server_error);
      break;
    // ...
  }
}

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