duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

rc_api_runtime.c (25613B)


      1 #include "rc_api_runtime.h"
      2 #include "rc_api_common.h"
      3 
      4 #include "rc_runtime.h"
      5 #include "rc_runtime_types.h"
      6 #include "../rc_compat.h"
      7 #include "../rhash/md5.h"
      8 
      9 #include <stdlib.h>
     10 #include <stdio.h>
     11 #include <string.h>
     12 
     13 /* --- Resolve Hash --- */
     14 
     15 int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) {
     16   rc_api_url_builder_t builder;
     17 
     18   rc_api_url_build_dorequest_url(request);
     19 
     20   if (!api_params->game_hash || !*api_params->game_hash)
     21     return RC_INVALID_STATE;
     22 
     23   rc_url_builder_init(&builder, &request->buffer, 48);
     24   rc_url_builder_append_str_param(&builder, "r", "gameid");
     25   rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
     26   request->post_data = rc_url_builder_finalize(&builder);
     27   request->content_type = RC_CONTENT_TYPE_URLENCODED;
     28 
     29   return builder.result;
     30 }
     31 
     32 int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) {
     33   rc_api_server_response_t response_obj;
     34 
     35   memset(&response_obj, 0, sizeof(response_obj));
     36   response_obj.body = server_response;
     37   response_obj.body_length = rc_json_get_object_string_length(server_response);
     38 
     39   return rc_api_process_resolve_hash_server_response(response, &response_obj);
     40 }
     41 
     42 int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) {
     43   int result;
     44   rc_json_field_t fields[] = {
     45     RC_JSON_NEW_FIELD("Success"),
     46     RC_JSON_NEW_FIELD("Error"),
     47     RC_JSON_NEW_FIELD("GameID")
     48   };
     49 
     50   memset(response, 0, sizeof(*response));
     51   rc_buffer_init(&response->response.buffer);
     52 
     53   result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
     54   if (result != RC_OK)
     55     return result;
     56 
     57   rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID");
     58   return RC_OK;
     59 }
     60 
     61 void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) {
     62   rc_buffer_destroy(&response->response.buffer);
     63 }
     64 
     65 /* --- Fetch Game Data --- */
     66 
     67 int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) {
     68   rc_api_url_builder_t builder;
     69 
     70   rc_api_url_build_dorequest_url(request);
     71 
     72   if (api_params->game_id == 0)
     73     return RC_INVALID_STATE;
     74 
     75   rc_url_builder_init(&builder, &request->buffer, 48);
     76   if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) {
     77     rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
     78     request->post_data = rc_url_builder_finalize(&builder);
     79     request->content_type = RC_CONTENT_TYPE_URLENCODED;
     80   }
     81 
     82   return builder.result;
     83 }
     84 
     85 int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) {
     86   rc_api_server_response_t response_obj;
     87 
     88   memset(&response_obj, 0, sizeof(response_obj));
     89   response_obj.body = server_response;
     90   response_obj.body_length = rc_json_get_object_string_length(server_response);
     91 
     92   return rc_api_process_fetch_game_data_server_response(response, &response_obj);
     93 }
     94 
     95 static int rc_parse_achievement_type(const char* type)
     96 {
     97   if (strcmp(type, "missable") == 0)
     98     return RC_ACHIEVEMENT_TYPE_MISSABLE;
     99 
    100   if (strcmp(type, "win_condition") == 0)
    101     return RC_ACHIEVEMENT_TYPE_WIN;
    102 
    103   if (strcmp(type, "progression") == 0)
    104     return RC_ACHIEVEMENT_TYPE_PROGRESSION;
    105 
    106   return RC_ACHIEVEMENT_TYPE_STANDARD;
    107 }
    108 
    109 int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) {
    110   rc_api_achievement_definition_t* achievement;
    111   rc_api_leaderboard_definition_t* leaderboard;
    112   rc_json_field_t array_field;
    113   rc_json_iterator_t iterator;
    114   const char* last_author = "";
    115   const char* last_author_field = "";
    116   size_t last_author_len = 0;
    117   size_t len;
    118   uint32_t timet;
    119   int result;
    120   char format[16];
    121 
    122   rc_json_field_t fields[] = {
    123     RC_JSON_NEW_FIELD("Success"),
    124     RC_JSON_NEW_FIELD("Error"),
    125     RC_JSON_NEW_FIELD("PatchData") /* nested object */
    126   };
    127 
    128   rc_json_field_t patchdata_fields[] = {
    129     RC_JSON_NEW_FIELD("ID"),
    130     RC_JSON_NEW_FIELD("Title"),
    131     RC_JSON_NEW_FIELD("ConsoleID"),
    132     RC_JSON_NEW_FIELD("ImageIcon"),
    133     RC_JSON_NEW_FIELD("RichPresencePatch"),
    134     RC_JSON_NEW_FIELD("Achievements"), /* array */
    135     RC_JSON_NEW_FIELD("Leaderboards") /* array */
    136   };
    137 
    138   rc_json_field_t achievement_fields[] = {
    139     RC_JSON_NEW_FIELD("ID"),
    140     RC_JSON_NEW_FIELD("Title"),
    141     RC_JSON_NEW_FIELD("Description"),
    142     RC_JSON_NEW_FIELD("Flags"),
    143     RC_JSON_NEW_FIELD("Points"),
    144     RC_JSON_NEW_FIELD("MemAddr"),
    145     RC_JSON_NEW_FIELD("Author"),
    146     RC_JSON_NEW_FIELD("BadgeName"),
    147     RC_JSON_NEW_FIELD("Created"),
    148     RC_JSON_NEW_FIELD("Modified"),
    149     RC_JSON_NEW_FIELD("Type"),
    150     RC_JSON_NEW_FIELD("Rarity"),
    151     RC_JSON_NEW_FIELD("RarityHardcore")
    152   };
    153 
    154   rc_json_field_t leaderboard_fields[] = {
    155     RC_JSON_NEW_FIELD("ID"),
    156     RC_JSON_NEW_FIELD("Title"),
    157     RC_JSON_NEW_FIELD("Description"),
    158     RC_JSON_NEW_FIELD("Mem"),
    159     RC_JSON_NEW_FIELD("Format"),
    160     RC_JSON_NEW_FIELD("LowerIsBetter"),
    161     RC_JSON_NEW_FIELD("Hidden")
    162   };
    163 
    164   memset(response, 0, sizeof(*response));
    165   rc_buffer_init(&response->response.buffer);
    166 
    167   result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
    168   if (result != RC_OK || !response->response.succeeded)
    169     return result;
    170 
    171   if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[2], "PatchData"))
    172     return RC_MISSING_VALUE;
    173 
    174   if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID"))
    175     return RC_MISSING_VALUE;
    176   if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title"))
    177     return RC_MISSING_VALUE;
    178   if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID"))
    179     return RC_MISSING_VALUE;
    180 
    181   /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */
    182   rc_json_extract_filename(&patchdata_fields[3]);
    183   rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", "");
    184 
    185   /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards.
    186      determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)
    187      and add space for the structures. */
    188   len = patchdata_fields[4].value_end - patchdata_fields[4].value_start; /* rich presence */
    189 
    190   len += (patchdata_fields[5].value_end - patchdata_fields[5].value_start) - /* achievements */
    191           patchdata_fields[5].array_size * (130 - sizeof(rc_api_achievement_definition_t));
    192 
    193   len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* leaderboards */
    194           patchdata_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));
    195 
    196   rc_buffer_reserve(&response->response.buffer, len);
    197   /* end estimation */
    198 
    199   rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[4], "RichPresencePatch", "");
    200   if (!response->rich_presence_script)
    201     response->rich_presence_script = "";
    202 
    203   if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements"))
    204     return RC_MISSING_VALUE;
    205 
    206   if (response->num_achievements) {
    207     response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t));
    208     if (!response->achievements)
    209       return RC_OUT_OF_MEMORY;
    210 
    211     memset(&iterator, 0, sizeof(iterator));
    212     iterator.json = array_field.value_start;
    213     iterator.end = array_field.value_end;
    214 
    215     achievement = response->achievements;
    216     while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) {
    217       if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID"))
    218         return RC_MISSING_VALUE;
    219       if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title"))
    220         return RC_MISSING_VALUE;
    221       if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description"))
    222         return RC_MISSING_VALUE;
    223       if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags"))
    224         return RC_MISSING_VALUE;
    225       if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points"))
    226         return RC_MISSING_VALUE;
    227       if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr"))
    228         return RC_MISSING_VALUE;
    229       if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName"))
    230         return RC_MISSING_VALUE;
    231 
    232       len = achievement_fields[6].value_end - achievement_fields[6].value_start;
    233       if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) {
    234         achievement->author = last_author;
    235       }
    236       else {
    237         if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author"))
    238           return RC_MISSING_VALUE;
    239 
    240         if (achievement->author == NULL) {
    241           /* ensure we don't pass NULL out to client */
    242           last_author = achievement->author = "";
    243           last_author_len = 0;
    244         } else {
    245           last_author = achievement->author;
    246           last_author_field = achievement_fields[6].value_start;
    247           last_author_len = len;
    248         }
    249       }
    250 
    251       if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created"))
    252         return RC_MISSING_VALUE;
    253       achievement->created = (time_t)timet;
    254       if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified"))
    255         return RC_MISSING_VALUE;
    256       achievement->updated = (time_t)timet;
    257 
    258       achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;
    259       if (achievement_fields[10].value_end) {
    260         len = achievement_fields[10].value_end - achievement_fields[10].value_start - 2;
    261         if (len < sizeof(format) - 1) {
    262           memcpy(format, achievement_fields[10].value_start + 1, len);
    263           format[len] = '\0';
    264           achievement->type = rc_parse_achievement_type(format);
    265         }
    266       }
    267 
    268       /* legacy support : if title contains[m], change type to missable and remove[m] from title */
    269       if (memcmp(achievement->title, "[m]", 3) == 0) {
    270         len = 3;
    271         while (achievement->title[len] == ' ')
    272           ++len;
    273         achievement->title += len;
    274         achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
    275       }
    276       else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) {
    277         len = strlen(achievement->title) - 3;
    278         while (achievement->title[len - 1] == ' ')
    279           --len;
    280         ((char*)achievement->title)[len] = '\0';
    281         achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
    282       }
    283 
    284       rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0);
    285       rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0);
    286 
    287       ++achievement;
    288     }
    289   }
    290 
    291   if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards"))
    292     return RC_MISSING_VALUE;
    293 
    294   if (response->num_leaderboards) {
    295     response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));
    296     if (!response->leaderboards)
    297       return RC_OUT_OF_MEMORY;
    298 
    299     memset(&iterator, 0, sizeof(iterator));
    300     iterator.json = array_field.value_start;
    301     iterator.end = array_field.value_end;
    302 
    303     leaderboard = response->leaderboards;
    304     while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) {
    305       if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID"))
    306         return RC_MISSING_VALUE;
    307       if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title"))
    308         return RC_MISSING_VALUE;
    309       if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description"))
    310         return RC_MISSING_VALUE;
    311       if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem"))
    312         return RC_MISSING_VALUE;
    313       rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0);
    314       leaderboard->lower_is_better = (uint8_t)result;
    315       rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0);
    316       leaderboard->hidden = (uint8_t)result;
    317 
    318       if (!leaderboard_fields[4].value_end)
    319         return RC_MISSING_VALUE;
    320       len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2;
    321       if (len < sizeof(format) - 1) {
    322         memcpy(format, leaderboard_fields[4].value_start + 1, len);
    323         format[len] = '\0';
    324         leaderboard->format = rc_parse_format(format);
    325       }
    326       else {
    327         leaderboard->format = RC_FORMAT_VALUE;
    328       }
    329 
    330       ++leaderboard;
    331     }
    332   }
    333 
    334   return RC_OK;
    335 }
    336 
    337 void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) {
    338   rc_buffer_destroy(&response->response.buffer);
    339 }
    340 
    341 /* --- Ping --- */
    342 
    343 int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) {
    344   rc_api_url_builder_t builder;
    345 
    346   rc_api_url_build_dorequest_url(request);
    347 
    348   if (api_params->game_id == 0)
    349     return RC_INVALID_STATE;
    350 
    351   rc_url_builder_init(&builder, &request->buffer, 48);
    352   if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) {
    353     rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
    354 
    355     if (api_params->rich_presence && *api_params->rich_presence)
    356       rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence);
    357 
    358     if (api_params->game_hash && *api_params->game_hash) {
    359       rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore);
    360       rc_url_builder_append_str_param(&builder, "x", api_params->game_hash);
    361     }
    362 
    363     request->post_data = rc_url_builder_finalize(&builder);
    364     request->content_type = RC_CONTENT_TYPE_URLENCODED;
    365   }
    366 
    367   return builder.result;
    368 }
    369 
    370 int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) {
    371   rc_api_server_response_t response_obj;
    372 
    373   memset(&response_obj, 0, sizeof(response_obj));
    374   response_obj.body = server_response;
    375   response_obj.body_length = rc_json_get_object_string_length(server_response);
    376 
    377   return rc_api_process_ping_server_response(response, &response_obj);
    378 }
    379 
    380 int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) {
    381   rc_json_field_t fields[] = {
    382     RC_JSON_NEW_FIELD("Success"),
    383     RC_JSON_NEW_FIELD("Error")
    384   };
    385 
    386   memset(response, 0, sizeof(*response));
    387   rc_buffer_init(&response->response.buffer);
    388 
    389   return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
    390 }
    391 
    392 void rc_api_destroy_ping_response(rc_api_ping_response_t* response) {
    393   rc_buffer_destroy(&response->response.buffer);
    394 }
    395 
    396 /* --- Award Achievement --- */
    397 
    398 int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) {
    399   rc_api_url_builder_t builder;
    400   char buffer[33];
    401   md5_state_t md5;
    402   md5_byte_t digest[16];
    403 
    404   rc_api_url_build_dorequest_url(request);
    405 
    406   if (api_params->achievement_id == 0)
    407     return RC_INVALID_STATE;
    408 
    409   rc_url_builder_init(&builder, &request->buffer, 96);
    410   if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) {
    411     rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id);
    412     rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0);
    413     if (api_params->game_hash && *api_params->game_hash)
    414       rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
    415 
    416     /* Evaluate the signature. */
    417     md5_init(&md5);
    418     snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);
    419     md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
    420     md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
    421     snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0);
    422     md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
    423     md5_finish(&md5, digest);
    424     rc_format_md5(buffer, digest);
    425     rc_url_builder_append_str_param(&builder, "v", buffer);
    426 
    427     request->post_data = rc_url_builder_finalize(&builder);
    428     request->content_type = RC_CONTENT_TYPE_URLENCODED;
    429   }
    430 
    431   return builder.result;
    432 }
    433 
    434 int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) {
    435   rc_api_server_response_t response_obj;
    436 
    437   memset(&response_obj, 0, sizeof(response_obj));
    438   response_obj.body = server_response;
    439   response_obj.body_length = rc_json_get_object_string_length(server_response);
    440 
    441   return rc_api_process_award_achievement_server_response(response, &response_obj);
    442 }
    443 
    444 int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) {
    445   int result;
    446   rc_json_field_t fields[] = {
    447     RC_JSON_NEW_FIELD("Success"),
    448     RC_JSON_NEW_FIELD("Error"),
    449     RC_JSON_NEW_FIELD("Score"),
    450     RC_JSON_NEW_FIELD("SoftcoreScore"),
    451     RC_JSON_NEW_FIELD("AchievementID"),
    452     RC_JSON_NEW_FIELD("AchievementsRemaining")
    453   };
    454 
    455   memset(response, 0, sizeof(*response));
    456   rc_buffer_init(&response->response.buffer);
    457 
    458   result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
    459   if (result != RC_OK)
    460     return result;
    461 
    462   if (!response->response.succeeded) {
    463     if (response->response.error_message &&
    464         memcmp(response->response.error_message, "User already has", 16) == 0) {
    465       /* not really an error, the achievement is unlocked, just not by the current call.
    466        *  hardcore:     User already has hardcore and regular achievements awarded.
    467        *  non-hardcore: User already has this achievement awarded.
    468        */
    469       response->response.succeeded = 1;
    470     } else {
    471       return result;
    472     }
    473   }
    474 
    475   rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0);
    476   rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0);
    477   rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0);
    478   rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1);
    479 
    480   return RC_OK;
    481 }
    482 
    483 void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) {
    484   rc_buffer_destroy(&response->response.buffer);
    485 }
    486 
    487 /* --- Submit Leaderboard Entry --- */
    488 
    489 int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) {
    490   rc_api_url_builder_t builder;
    491   char buffer[33];
    492   md5_state_t md5;
    493   md5_byte_t digest[16];
    494 
    495   rc_api_url_build_dorequest_url(request);
    496 
    497   if (api_params->leaderboard_id == 0)
    498     return RC_INVALID_STATE;
    499 
    500   rc_url_builder_init(&builder, &request->buffer, 96);
    501   if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) {
    502     rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id);
    503     rc_url_builder_append_num_param(&builder, "s", api_params->score);
    504 
    505     if (api_params->game_hash && *api_params->game_hash)
    506       rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
    507 
    508     /* Evaluate the signature. */
    509     md5_init(&md5);
    510     snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id);
    511     md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
    512     md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
    513     snprintf(buffer, sizeof(buffer), "%d", api_params->score);
    514     md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
    515     md5_finish(&md5, digest);
    516     rc_format_md5(buffer, digest);
    517     rc_url_builder_append_str_param(&builder, "v", buffer);
    518 
    519     request->post_data = rc_url_builder_finalize(&builder);
    520     request->content_type = RC_CONTENT_TYPE_URLENCODED;
    521   }
    522 
    523   return builder.result;
    524 }
    525 
    526 int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) {
    527   rc_api_server_response_t response_obj;
    528 
    529   memset(&response_obj, 0, sizeof(response_obj));
    530   response_obj.body = server_response;
    531   response_obj.body_length = rc_json_get_object_string_length(server_response);
    532 
    533   return rc_api_process_submit_lboard_entry_server_response(response, &response_obj);
    534 }
    535 
    536 int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) {
    537   rc_api_lboard_entry_t* entry;
    538   rc_json_field_t array_field;
    539   rc_json_iterator_t iterator;
    540   const char* str;
    541   int result;
    542 
    543   rc_json_field_t fields[] = {
    544     RC_JSON_NEW_FIELD("Success"),
    545     RC_JSON_NEW_FIELD("Error"),
    546     RC_JSON_NEW_FIELD("Response") /* nested object */
    547   };
    548 
    549   rc_json_field_t response_fields[] = {
    550     RC_JSON_NEW_FIELD("Score"),
    551     RC_JSON_NEW_FIELD("BestScore"),
    552     RC_JSON_NEW_FIELD("RankInfo"), /* nested object */
    553     RC_JSON_NEW_FIELD("TopEntries") /* array */
    554     /* unused fields
    555     RC_JSON_NEW_FIELD("LBData"), / * array * /
    556     RC_JSON_NEW_FIELD("ScoreFormatted"),
    557     RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * /
    558       * unused fields */
    559   };
    560 
    561   /* unused fields
    562   rc_json_field_t lbdata_fields[] = {
    563     RC_JSON_NEW_FIELD("Format"),
    564     RC_JSON_NEW_FIELD("LeaderboardID"),
    565     RC_JSON_NEW_FIELD("GameID"),
    566     RC_JSON_NEW_FIELD("Title"),
    567     RC_JSON_NEW_FIELD("LowerIsBetter")
    568   };
    569     * unused fields */
    570 
    571   rc_json_field_t entry_fields[] = {
    572     RC_JSON_NEW_FIELD("User"),
    573     RC_JSON_NEW_FIELD("Rank"),
    574     RC_JSON_NEW_FIELD("Score")
    575     /* unused fields
    576     RC_JSON_NEW_FIELD("DateSubmitted")
    577      * unused fields */
    578   };
    579 
    580   rc_json_field_t rank_info_fields[] = {
    581     RC_JSON_NEW_FIELD("Rank"),
    582     RC_JSON_NEW_FIELD("NumEntries")
    583     /* unused fields
    584     RC_JSON_NEW_FIELD("LowerIsBetter"),
    585     RC_JSON_NEW_FIELD("UserRank")
    586       * unused fields */
    587   };
    588 
    589   memset(response, 0, sizeof(*response));
    590   rc_buffer_init(&response->response.buffer);
    591 
    592   result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
    593   if (result != RC_OK || !response->response.succeeded)
    594     return result;
    595 
    596   if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response"))
    597     return RC_MISSING_VALUE;
    598   if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score"))
    599     return RC_MISSING_VALUE;
    600   if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore"))
    601     return RC_MISSING_VALUE;
    602 
    603   if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo"))
    604     return RC_MISSING_VALUE;
    605   if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank"))
    606     return RC_MISSING_VALUE;
    607   if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries"))
    608     return RC_MISSING_VALUE;
    609   response->num_entries = (unsigned)atoi(str);
    610 
    611   if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries"))
    612     return RC_MISSING_VALUE;
    613 
    614   if (response->num_top_entries) {
    615     response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t));
    616     if (!response->top_entries)
    617       return RC_OUT_OF_MEMORY;
    618 
    619     memset(&iterator, 0, sizeof(iterator));
    620     iterator.json = array_field.value_start;
    621     iterator.end = array_field.value_end;
    622 
    623     entry = response->top_entries;
    624     while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) {
    625       if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User"))
    626         return RC_MISSING_VALUE;
    627 
    628       if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank"))
    629         return RC_MISSING_VALUE;
    630 
    631       if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score"))
    632         return RC_MISSING_VALUE;
    633 
    634       ++entry;
    635     }
    636   }
    637 
    638   return RC_OK;
    639 }
    640 
    641 void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) {
    642   rc_buffer_destroy(&response->response.buffer);
    643 }