Path: blob/master/dep/rcheevos/src/rapi/rc_api_runtime.c
4253 views
#include "rc_api_runtime.h"1#include "rc_api_common.h"23#include "rc_runtime.h"4#include "rc_runtime_types.h"5#include "../rc_compat.h"6#include "../rhash/md5.h"78#include <stdlib.h>9#include <stdio.h>10#include <string.h>1112/* --- Resolve Hash --- */1314int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) {15return rc_api_init_resolve_hash_request_hosted(request, api_params, &g_host);16}1718int rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request,19const rc_api_resolve_hash_request_t* api_params,20const rc_api_host_t* host) {21rc_api_url_builder_t builder;2223rc_api_url_build_dorequest_url(request, host);2425if (!api_params->game_hash || !*api_params->game_hash)26return RC_INVALID_STATE;2728rc_url_builder_init(&builder, &request->buffer, 48);29rc_url_builder_append_str_param(&builder, "r", "gameid");30rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);31request->post_data = rc_url_builder_finalize(&builder);32request->content_type = RC_CONTENT_TYPE_URLENCODED;3334return builder.result;35}3637int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) {38rc_api_server_response_t response_obj;3940memset(&response_obj, 0, sizeof(response_obj));41response_obj.body = server_response;42response_obj.body_length = rc_json_get_object_string_length(server_response);4344return rc_api_process_resolve_hash_server_response(response, &response_obj);45}4647int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) {48int result;49rc_json_field_t fields[] = {50RC_JSON_NEW_FIELD("Success"),51RC_JSON_NEW_FIELD("Error"),52RC_JSON_NEW_FIELD("GameID")53};5455memset(response, 0, sizeof(*response));56rc_buffer_init(&response->response.buffer);5758result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));59if (result != RC_OK)60return result;6162rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID");63return RC_OK;64}6566void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) {67rc_buffer_destroy(&response->response.buffer);68}6970/* --- Fetch Game Data --- */7172int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) {73return rc_api_init_fetch_game_data_request_hosted(request, api_params, &g_host);74}7576int rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request,77const rc_api_fetch_game_data_request_t* api_params,78const rc_api_host_t* host) {79rc_api_url_builder_t builder;8081rc_api_url_build_dorequest_url(request, host);8283if (api_params->game_id == 0 && (!api_params->game_hash || !api_params->game_hash[0]))84return RC_INVALID_STATE;8586rc_url_builder_init(&builder, &request->buffer, 48);87if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) {88if (api_params->game_id)89rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);90else91rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);9293request->post_data = rc_url_builder_finalize(&builder);94request->content_type = RC_CONTENT_TYPE_URLENCODED;95}9697return builder.result;98}99100int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) {101rc_api_server_response_t response_obj;102103memset(&response_obj, 0, sizeof(response_obj));104response_obj.body = server_response;105response_obj.body_length = rc_json_get_object_string_length(server_response);106107return rc_api_process_fetch_game_data_server_response(response, &response_obj);108}109110static int rc_parse_achievement_type(const char* type) {111if (strcmp(type, "missable") == 0)112return RC_ACHIEVEMENT_TYPE_MISSABLE;113114if (strcmp(type, "win_condition") == 0)115return RC_ACHIEVEMENT_TYPE_WIN;116117if (strcmp(type, "progression") == 0)118return RC_ACHIEVEMENT_TYPE_PROGRESSION;119120return RC_ACHIEVEMENT_TYPE_STANDARD;121}122123static int rc_api_process_fetch_game_data_achievements(rc_api_fetch_game_data_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) {124rc_json_iterator_t iterator;125const char* last_author = "";126const char* last_author_field = "";127size_t last_author_len = 0;128uint32_t timet;129size_t len;130char type[16];131132rc_json_field_t achievement_fields[] = {133RC_JSON_NEW_FIELD("ID"),134RC_JSON_NEW_FIELD("Title"),135RC_JSON_NEW_FIELD("Description"),136RC_JSON_NEW_FIELD("Flags"),137RC_JSON_NEW_FIELD("Points"),138RC_JSON_NEW_FIELD("MemAddr"),139RC_JSON_NEW_FIELD("Author"),140RC_JSON_NEW_FIELD("BadgeName"),141RC_JSON_NEW_FIELD("Created"),142RC_JSON_NEW_FIELD("Modified"),143RC_JSON_NEW_FIELD("Type"),144RC_JSON_NEW_FIELD("Rarity"),145RC_JSON_NEW_FIELD("RarityHardcore"),146RC_JSON_NEW_FIELD("BadgeURL"),147RC_JSON_NEW_FIELD("BadgeLockedURL")148};149150memset(&iterator, 0, sizeof(iterator));151iterator.json = array_field->value_start;152iterator.end = array_field->value_end;153154while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) {155if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID"))156return RC_MISSING_VALUE;157if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title"))158return RC_MISSING_VALUE;159if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description"))160return RC_MISSING_VALUE;161if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags"))162return RC_MISSING_VALUE;163if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points"))164return RC_MISSING_VALUE;165if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr"))166return RC_MISSING_VALUE;167if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName"))168return RC_MISSING_VALUE;169170rc_json_get_optional_string(&achievement->badge_url, &response->response, &achievement_fields[13], "BadgeURL", "");171if (!achievement->badge_url[0])172achievement->badge_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name);173174rc_json_get_optional_string(&achievement->badge_locked_url, &response->response, &achievement_fields[14], "BadgeLockedURL", "");175if (!achievement->badge_locked_url[0])176achievement->badge_locked_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name);177178len = achievement_fields[6].value_end - achievement_fields[6].value_start;179if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) {180achievement->author = last_author;181}182else {183if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author"))184return RC_MISSING_VALUE;185186if (achievement->author == NULL) {187/* ensure we don't pass NULL out to client */188last_author = achievement->author = "";189last_author_len = 0;190} else {191last_author = achievement->author;192last_author_field = achievement_fields[6].value_start;193last_author_len = len;194}195}196197if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created"))198return RC_MISSING_VALUE;199achievement->created = (time_t)timet;200if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified"))201return RC_MISSING_VALUE;202achievement->updated = (time_t)timet;203204achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;205if (achievement_fields[10].value_end) {206len = achievement_fields[10].value_end - achievement_fields[10].value_start - 2;207if (len < sizeof(type) - 1) {208memcpy(type, achievement_fields[10].value_start + 1, len);209type[len] = '\0';210achievement->type = rc_parse_achievement_type(type);211}212}213214/* legacy support : if title contains[m], change type to missable and remove[m] from title */215if (memcmp(achievement->title, "[m]", 3) == 0) {216len = 3;217while (achievement->title[len] == ' ')218++len;219achievement->title += len;220achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;221}222else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) {223len = strlen(achievement->title) - 3;224while (achievement->title[len - 1] == ' ')225--len;226((char*)achievement->title)[len] = '\0';227achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;228}229230rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0);231rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0);232233++achievement;234}235236return RC_OK;237}238239static int rc_api_process_fetch_game_data_leaderboards(rc_api_fetch_game_data_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) {240rc_json_iterator_t iterator;241size_t len;242int result;243char format[16];244245rc_json_field_t leaderboard_fields[] = {246RC_JSON_NEW_FIELD("ID"),247RC_JSON_NEW_FIELD("Title"),248RC_JSON_NEW_FIELD("Description"),249RC_JSON_NEW_FIELD("Mem"),250RC_JSON_NEW_FIELD("Format"),251RC_JSON_NEW_FIELD("LowerIsBetter"),252RC_JSON_NEW_FIELD("Hidden")253};254255memset(&iterator, 0, sizeof(iterator));256iterator.json = array_field->value_start;257iterator.end = array_field->value_end;258259while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) {260if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID"))261return RC_MISSING_VALUE;262if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title"))263return RC_MISSING_VALUE;264if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description"))265return RC_MISSING_VALUE;266if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem"))267return RC_MISSING_VALUE;268rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0);269leaderboard->lower_is_better = (uint8_t)result;270rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0);271leaderboard->hidden = (uint8_t)result;272273if (!leaderboard_fields[4].value_end)274return RC_MISSING_VALUE;275len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2;276if (len < sizeof(format) - 1) {277memcpy(format, leaderboard_fields[4].value_start + 1, len);278format[len] = '\0';279leaderboard->format = rc_parse_format(format);280}281else {282leaderboard->format = RC_FORMAT_VALUE;283}284285++leaderboard;286}287288return RC_OK;289}290291static int rc_api_process_fetch_game_data_subsets(rc_api_fetch_game_data_response_t* response, rc_api_subset_definition_t* subset, rc_json_field_t* subset_array_field) {292rc_json_iterator_t iterator;293rc_json_field_t array_field;294size_t len;295int result;296297rc_json_field_t subset_fields[] = {298RC_JSON_NEW_FIELD("GameAchievementSetID"),299RC_JSON_NEW_FIELD("SetTitle"),300RC_JSON_NEW_FIELD("ImageIcon"),301RC_JSON_NEW_FIELD("ImageIconURL"),302RC_JSON_NEW_FIELD("Achievements"), /* array */303RC_JSON_NEW_FIELD("Leaderboards") /* array */304};305306memset(&iterator, 0, sizeof(iterator));307iterator.json = subset_array_field->value_start;308iterator.end = subset_array_field->value_end;309310while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) {311if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "GameAchievementSetID"))312return RC_MISSING_VALUE;313if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[1], "SetTitle"))314return RC_MISSING_VALUE;315316/* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */317rc_json_extract_filename(&subset_fields[2]);318rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[2], "ImageIcon", "");319320if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[3], "ImageIconURL"))321return RC_MISSING_VALUE;322323/* estimate the amount of space necessary to store the achievements, and leaderboards.324determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)325and add space for the structures. */326len = (subset_fields[4].value_end - subset_fields[4].value_start) - /* achievements */327subset_fields[4].array_size * (80 - sizeof(rc_api_achievement_definition_t));328len += (subset_fields[5].value_end - subset_fields[5].value_start) - /* leaderboards */329subset_fields[5].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));330331rc_buffer_reserve(&response->response.buffer, len);332/* end estimation */333334if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[4], "Achievements"))335return RC_MISSING_VALUE;336337if (subset->num_achievements) {338subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t));339if (!subset->achievements)340return RC_OUT_OF_MEMORY;341342result = rc_api_process_fetch_game_data_achievements(response, subset->achievements, &array_field);343if (result != RC_OK)344return result;345}346347if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[5], "Leaderboards"))348return RC_MISSING_VALUE;349350if (subset->num_leaderboards) {351subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));352if (!subset->leaderboards)353return RC_OUT_OF_MEMORY;354355result = rc_api_process_fetch_game_data_leaderboards(response, subset->leaderboards, &array_field);356if (result != RC_OK)357return result;358}359360++subset;361}362363return RC_OK;364}365366int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) {367rc_json_field_t array_field;368size_t len;369int result;370371rc_json_field_t fields[] = {372RC_JSON_NEW_FIELD("Success"),373RC_JSON_NEW_FIELD("Error"),374RC_JSON_NEW_FIELD("Code"),375RC_JSON_NEW_FIELD("PatchData") /* nested object */376};377378rc_json_field_t patchdata_fields[] = {379RC_JSON_NEW_FIELD("ID"),380RC_JSON_NEW_FIELD("Title"),381RC_JSON_NEW_FIELD("ConsoleID"),382RC_JSON_NEW_FIELD("ImageIcon"),383RC_JSON_NEW_FIELD("ImageIconURL"),384RC_JSON_NEW_FIELD("RichPresencePatch"),385RC_JSON_NEW_FIELD("Achievements"), /* array */386RC_JSON_NEW_FIELD("Leaderboards"), /* array */387RC_JSON_NEW_FIELD("Sets") /* array */388};389390memset(response, 0, sizeof(*response));391rc_buffer_init(&response->response.buffer);392393result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));394if (result != RC_OK || !response->response.succeeded)395return result;396397if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[3], "PatchData"))398return RC_MISSING_VALUE;399400if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID"))401return RC_MISSING_VALUE;402if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title"))403return RC_MISSING_VALUE;404if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID"))405return RC_MISSING_VALUE;406407/* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */408rc_json_extract_filename(&patchdata_fields[3]);409rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", "");410rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", "");411if (!response->image_url[0])412response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name);413414/* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards.415determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)416and add space for the structures. */417len = patchdata_fields[5].value_end - patchdata_fields[5].value_start; /* rich presence */418419len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* achievements */420patchdata_fields[6].array_size * (80 - sizeof(rc_api_achievement_definition_t));421422len += (patchdata_fields[7].value_end - patchdata_fields[7].value_start) - /* leaderboards */423patchdata_fields[7].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));424425rc_buffer_reserve(&response->response.buffer, len);426/* end estimation */427428rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[5], "RichPresencePatch", "");429if (!response->rich_presence_script)430response->rich_presence_script = "";431432if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[6], "Achievements"))433return RC_MISSING_VALUE;434435if (response->num_achievements) {436response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t));437if (!response->achievements)438return RC_OUT_OF_MEMORY;439440result = rc_api_process_fetch_game_data_achievements(response, response->achievements, &array_field);441if (result != RC_OK)442return result;443}444445if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[7], "Leaderboards"))446return RC_MISSING_VALUE;447448if (response->num_leaderboards) {449response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));450if (!response->leaderboards)451return RC_OUT_OF_MEMORY;452453result = rc_api_process_fetch_game_data_leaderboards(response, response->leaderboards, &array_field);454if (result != RC_OK)455return result;456}457458rc_json_get_optional_array(&response->num_subsets, &array_field, &patchdata_fields[8], "Sets");459if (response->num_subsets) {460response->subsets = (rc_api_subset_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_subsets * sizeof(rc_api_subset_definition_t));461if (!response->subsets)462return RC_OUT_OF_MEMORY;463464result = rc_api_process_fetch_game_data_subsets(response, response->subsets, &array_field);465if (result != RC_OK)466return result;467}468469return RC_OK;470}471472void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) {473rc_buffer_destroy(&response->response.buffer);474}475476/* --- Ping --- */477478int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) {479return rc_api_init_ping_request_hosted(request, api_params, &g_host);480}481482int rc_api_init_ping_request_hosted(rc_api_request_t* request,483const rc_api_ping_request_t* api_params,484const rc_api_host_t* host) {485rc_api_url_builder_t builder;486487rc_api_url_build_dorequest_url(request, host);488489if (api_params->game_id == 0)490return RC_INVALID_STATE;491492rc_url_builder_init(&builder, &request->buffer, 48);493if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) {494rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);495496if (api_params->rich_presence && *api_params->rich_presence)497rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence);498499if (api_params->game_hash && *api_params->game_hash) {500rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore);501rc_url_builder_append_str_param(&builder, "x", api_params->game_hash);502}503504request->post_data = rc_url_builder_finalize(&builder);505request->content_type = RC_CONTENT_TYPE_URLENCODED;506}507508return builder.result;509}510511int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) {512rc_api_server_response_t response_obj;513514memset(&response_obj, 0, sizeof(response_obj));515response_obj.body = server_response;516response_obj.body_length = rc_json_get_object_string_length(server_response);517518return rc_api_process_ping_server_response(response, &response_obj);519}520521int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) {522rc_json_field_t fields[] = {523RC_JSON_NEW_FIELD("Success"),524RC_JSON_NEW_FIELD("Error")525};526527memset(response, 0, sizeof(*response));528rc_buffer_init(&response->response.buffer);529530return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));531}532533void rc_api_destroy_ping_response(rc_api_ping_response_t* response) {534rc_buffer_destroy(&response->response.buffer);535}536537/* --- Award Achievement --- */538539int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) {540return rc_api_init_award_achievement_request_hosted(request, api_params, &g_host);541}542543int rc_api_init_award_achievement_request_hosted(rc_api_request_t* request,544const rc_api_award_achievement_request_t* api_params,545const rc_api_host_t* host) {546rc_api_url_builder_t builder;547char buffer[33];548md5_state_t md5;549md5_byte_t digest[16];550551rc_api_url_build_dorequest_url(request, host);552553if (api_params->achievement_id == 0)554return RC_INVALID_STATE;555556rc_url_builder_init(&builder, &request->buffer, 96);557if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) {558rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id);559rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0);560if (api_params->game_hash && *api_params->game_hash)561rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);562if (api_params->seconds_since_unlock)563rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_unlock);564565/* Evaluate the signature. */566md5_init(&md5);567snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);568md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));569md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));570snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0);571md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));572if (api_params->seconds_since_unlock) {573/* second achievement id is needed by delegated unlock. including it here allows overloading574* the hash generating code on the server */575snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);576md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));577snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_unlock);578md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));579}580md5_finish(&md5, digest);581rc_format_md5(buffer, digest);582rc_url_builder_append_str_param(&builder, "v", buffer);583584request->post_data = rc_url_builder_finalize(&builder);585request->content_type = RC_CONTENT_TYPE_URLENCODED;586}587588return builder.result;589}590591int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) {592rc_api_server_response_t response_obj;593594memset(&response_obj, 0, sizeof(response_obj));595response_obj.body = server_response;596response_obj.body_length = rc_json_get_object_string_length(server_response);597598return rc_api_process_award_achievement_server_response(response, &response_obj);599}600601int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) {602int result;603rc_json_field_t fields[] = {604RC_JSON_NEW_FIELD("Success"),605RC_JSON_NEW_FIELD("Error"),606RC_JSON_NEW_FIELD("Score"),607RC_JSON_NEW_FIELD("SoftcoreScore"),608RC_JSON_NEW_FIELD("AchievementID"),609RC_JSON_NEW_FIELD("AchievementsRemaining")610};611612memset(response, 0, sizeof(*response));613rc_buffer_init(&response->response.buffer);614615result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));616if (result != RC_OK)617return result;618619if (!response->response.succeeded) {620if (response->response.error_message &&621memcmp(response->response.error_message, "User already has", 16) == 0) {622/* not really an error, the achievement is unlocked, just not by the current call.623* hardcore: User already has hardcore and regular achievements awarded.624* non-hardcore: User already has this achievement awarded.625*/626response->response.succeeded = 1;627} else {628return result;629}630}631632rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0);633rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0);634rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0);635rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1);636637return RC_OK;638}639640void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) {641rc_buffer_destroy(&response->response.buffer);642}643644/* --- Submit Leaderboard Entry --- */645646int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) {647return rc_api_init_submit_lboard_entry_request_hosted(request, api_params, &g_host);648}649650int rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request,651const rc_api_submit_lboard_entry_request_t* api_params,652const rc_api_host_t* host) {653rc_api_url_builder_t builder;654char buffer[33];655md5_state_t md5;656md5_byte_t digest[16];657658rc_api_url_build_dorequest_url(request, host);659660if (api_params->leaderboard_id == 0)661return RC_INVALID_STATE;662663rc_url_builder_init(&builder, &request->buffer, 96);664if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) {665rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id);666rc_url_builder_append_num_param(&builder, "s", api_params->score);667668if (api_params->game_hash && *api_params->game_hash)669rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);670671if (api_params->seconds_since_completion)672rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_completion);673674/* Evaluate the signature. */675md5_init(&md5);676snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id);677md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));678md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));679snprintf(buffer, sizeof(buffer), "%d", api_params->score);680md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));681if (api_params->seconds_since_completion) {682snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_completion);683md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));684}685md5_finish(&md5, digest);686rc_format_md5(buffer, digest);687rc_url_builder_append_str_param(&builder, "v", buffer);688689request->post_data = rc_url_builder_finalize(&builder);690request->content_type = RC_CONTENT_TYPE_URLENCODED;691}692693return builder.result;694}695696int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) {697rc_api_server_response_t response_obj;698699memset(&response_obj, 0, sizeof(response_obj));700response_obj.body = server_response;701response_obj.body_length = rc_json_get_object_string_length(server_response);702703return rc_api_process_submit_lboard_entry_server_response(response, &response_obj);704}705706int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) {707rc_api_lboard_entry_t* entry;708rc_json_field_t array_field;709rc_json_iterator_t iterator;710const char* str;711int result;712713rc_json_field_t fields[] = {714RC_JSON_NEW_FIELD("Success"),715RC_JSON_NEW_FIELD("Error"),716RC_JSON_NEW_FIELD("Response") /* nested object */717};718719rc_json_field_t response_fields[] = {720RC_JSON_NEW_FIELD("Score"),721RC_JSON_NEW_FIELD("BestScore"),722RC_JSON_NEW_FIELD("RankInfo"), /* nested object */723RC_JSON_NEW_FIELD("TopEntries") /* array */724/* unused fields725RC_JSON_NEW_FIELD("LBData"), / * array * /726RC_JSON_NEW_FIELD("ScoreFormatted"),727RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * /728* unused fields */729};730731/* unused fields732rc_json_field_t lbdata_fields[] = {733RC_JSON_NEW_FIELD("Format"),734RC_JSON_NEW_FIELD("LeaderboardID"),735RC_JSON_NEW_FIELD("GameID"),736RC_JSON_NEW_FIELD("Title"),737RC_JSON_NEW_FIELD("LowerIsBetter")738};739* unused fields */740741rc_json_field_t entry_fields[] = {742RC_JSON_NEW_FIELD("User"),743RC_JSON_NEW_FIELD("Rank"),744RC_JSON_NEW_FIELD("Score")745/* unused fields746RC_JSON_NEW_FIELD("DateSubmitted")747* unused fields */748};749750rc_json_field_t rank_info_fields[] = {751RC_JSON_NEW_FIELD("Rank"),752RC_JSON_NEW_FIELD("NumEntries")753/* unused fields754RC_JSON_NEW_FIELD("LowerIsBetter"),755RC_JSON_NEW_FIELD("UserRank")756* unused fields */757};758759memset(response, 0, sizeof(*response));760rc_buffer_init(&response->response.buffer);761762result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));763if (result != RC_OK || !response->response.succeeded)764return result;765766if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response"))767return RC_MISSING_VALUE;768if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score"))769return RC_MISSING_VALUE;770if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore"))771return RC_MISSING_VALUE;772773if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo"))774return RC_MISSING_VALUE;775if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank"))776return RC_MISSING_VALUE;777if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries"))778return RC_MISSING_VALUE;779response->num_entries = (unsigned)atoi(str);780781if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries"))782return RC_MISSING_VALUE;783784if (response->num_top_entries) {785response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t));786if (!response->top_entries)787return RC_OUT_OF_MEMORY;788789memset(&iterator, 0, sizeof(iterator));790iterator.json = array_field.value_start;791iterator.end = array_field.value_end;792793entry = response->top_entries;794while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) {795if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User"))796return RC_MISSING_VALUE;797798if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank"))799return RC_MISSING_VALUE;800801if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score"))802return RC_MISSING_VALUE;803804++entry;805}806}807808return RC_OK;809}810811void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) {812rc_buffer_destroy(&response->response.buffer);813}814815816