Path: blob/master/dep/rcheevos/src/rapi/rc_api_runtime.c
4806 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_api_process_fetch_game_data_achievements(rc_api_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) {111rc_json_iterator_t iterator;112const char* last_author = "";113const char* last_author_field = "";114size_t last_author_len = 0;115uint32_t timet;116size_t len;117118rc_json_field_t achievement_fields[] = {119RC_JSON_NEW_FIELD("ID"),120RC_JSON_NEW_FIELD("Title"),121RC_JSON_NEW_FIELD("Description"),122RC_JSON_NEW_FIELD("Flags"),123RC_JSON_NEW_FIELD("Points"),124RC_JSON_NEW_FIELD("MemAddr"),125RC_JSON_NEW_FIELD("Author"),126RC_JSON_NEW_FIELD("BadgeName"),127RC_JSON_NEW_FIELD("Created"),128RC_JSON_NEW_FIELD("Modified"),129RC_JSON_NEW_FIELD("Type"),130RC_JSON_NEW_FIELD("Rarity"),131RC_JSON_NEW_FIELD("RarityHardcore"),132RC_JSON_NEW_FIELD("BadgeURL"),133RC_JSON_NEW_FIELD("BadgeLockedURL")134};135136memset(&iterator, 0, sizeof(iterator));137iterator.json = array_field->value_start;138iterator.end = array_field->value_end;139140while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) {141if (!rc_json_get_required_unum(&achievement->id, response, &achievement_fields[0], "ID"))142return RC_MISSING_VALUE;143if (!rc_json_get_required_string(&achievement->title, response, &achievement_fields[1], "Title"))144return RC_MISSING_VALUE;145if (!rc_json_get_required_string(&achievement->description, response, &achievement_fields[2], "Description"))146return RC_MISSING_VALUE;147if (!rc_json_get_required_unum(&achievement->category, response, &achievement_fields[3], "Flags"))148return RC_MISSING_VALUE;149if (!rc_json_get_required_unum(&achievement->points, response, &achievement_fields[4], "Points"))150return RC_MISSING_VALUE;151if (!rc_json_get_required_string(&achievement->definition, response, &achievement_fields[5], "MemAddr"))152return RC_MISSING_VALUE;153if (!rc_json_get_required_string(&achievement->badge_name, response, &achievement_fields[7], "BadgeName"))154return RC_MISSING_VALUE;155156rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", "");157if (!achievement->badge_url[0])158achievement->badge_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name);159160rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", "");161if (!achievement->badge_locked_url[0])162achievement->badge_locked_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name);163164len = achievement_fields[6].value_end - achievement_fields[6].value_start;165if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) {166achievement->author = last_author;167}168else {169if (!rc_json_get_required_string(&achievement->author, response, &achievement_fields[6], "Author"))170return RC_MISSING_VALUE;171172if (achievement->author == NULL) {173/* ensure we don't pass NULL out to client */174last_author = achievement->author = "";175last_author_len = 0;176} else {177last_author = achievement->author;178last_author_field = achievement_fields[6].value_start;179last_author_len = len;180}181}182183if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created"))184return RC_MISSING_VALUE;185achievement->created = (time_t)timet;186if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified"))187return RC_MISSING_VALUE;188achievement->updated = (time_t)timet;189190if (rc_json_field_string_matches(&achievement_fields[10], ""))191achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;192else if (rc_json_field_string_matches(&achievement_fields[10], "progression"))193achievement->type = RC_ACHIEVEMENT_TYPE_PROGRESSION;194else if (rc_json_field_string_matches(&achievement_fields[10], "missable"))195achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;196else if (rc_json_field_string_matches(&achievement_fields[10], "win_condition"))197achievement->type = RC_ACHIEVEMENT_TYPE_WIN;198else199achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;200201/* legacy support : if title contains[m], change type to missable and remove[m] from title */202if (memcmp(achievement->title, "[m]", 3) == 0) {203len = 3;204while (achievement->title[len] == ' ')205++len;206achievement->title += len;207achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;208}209else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) {210len = strlen(achievement->title) - 3;211while (achievement->title[len - 1] == ' ')212--len;213((char*)achievement->title)[len] = '\0';214achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;215}216217rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0);218rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0);219220++achievement;221}222223return RC_OK;224}225226static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) {227rc_json_iterator_t iterator;228size_t len;229int result;230char format[16];231232rc_json_field_t leaderboard_fields[] = {233RC_JSON_NEW_FIELD("ID"),234RC_JSON_NEW_FIELD("Title"),235RC_JSON_NEW_FIELD("Description"),236RC_JSON_NEW_FIELD("Mem"),237RC_JSON_NEW_FIELD("Format"),238RC_JSON_NEW_FIELD("LowerIsBetter"),239RC_JSON_NEW_FIELD("Hidden")240};241242memset(&iterator, 0, sizeof(iterator));243iterator.json = array_field->value_start;244iterator.end = array_field->value_end;245246while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) {247if (!rc_json_get_required_unum(&leaderboard->id, response, &leaderboard_fields[0], "ID"))248return RC_MISSING_VALUE;249if (!rc_json_get_required_string(&leaderboard->title, response, &leaderboard_fields[1], "Title"))250return RC_MISSING_VALUE;251if (!rc_json_get_required_string(&leaderboard->description, response, &leaderboard_fields[2], "Description"))252return RC_MISSING_VALUE;253if (!rc_json_get_required_string(&leaderboard->definition, response, &leaderboard_fields[3], "Mem"))254return RC_MISSING_VALUE;255rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0);256leaderboard->lower_is_better = (uint8_t)result;257rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0);258leaderboard->hidden = (uint8_t)result;259260if (!leaderboard_fields[4].value_end)261return RC_MISSING_VALUE;262len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2;263if (len < sizeof(format) - 1) {264memcpy(format, leaderboard_fields[4].value_start + 1, len);265format[len] = '\0';266leaderboard->format = rc_parse_format(format);267}268else {269leaderboard->format = RC_FORMAT_VALUE;270}271272++leaderboard;273}274275return RC_OK;276}277278int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) {279rc_json_field_t array_field;280size_t len;281int result;282283rc_json_field_t fields[] = {284RC_JSON_NEW_FIELD("Success"),285RC_JSON_NEW_FIELD("Error"),286RC_JSON_NEW_FIELD("Code"),287RC_JSON_NEW_FIELD("PatchData") /* nested object */288};289290rc_json_field_t patchdata_fields[] = {291RC_JSON_NEW_FIELD("ID"),292RC_JSON_NEW_FIELD("Title"),293RC_JSON_NEW_FIELD("ConsoleID"),294RC_JSON_NEW_FIELD("ImageIcon"),295RC_JSON_NEW_FIELD("ImageIconURL"),296RC_JSON_NEW_FIELD("RichPresencePatch"),297RC_JSON_NEW_FIELD("Achievements"), /* array */298RC_JSON_NEW_FIELD("Leaderboards"), /* array */299};300301memset(response, 0, sizeof(*response));302rc_buffer_init(&response->response.buffer);303304result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));305if (result != RC_OK || !response->response.succeeded)306return result;307308if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[3], "PatchData"))309return RC_MISSING_VALUE;310311if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID"))312return RC_MISSING_VALUE;313if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title"))314return RC_MISSING_VALUE;315if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID"))316return RC_MISSING_VALUE;317318/* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */319rc_json_extract_filename(&patchdata_fields[3]);320rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", "");321rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", "");322if (!response->image_url[0])323response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name);324325/* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards.326determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)327and add space for the structures. */328len = patchdata_fields[5].value_end - patchdata_fields[5].value_start; /* rich presence */329330len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* achievements */331patchdata_fields[6].array_size * (80 - sizeof(rc_api_achievement_definition_t));332333len += (patchdata_fields[7].value_end - patchdata_fields[7].value_start) - /* leaderboards */334patchdata_fields[7].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));335336rc_buffer_reserve(&response->response.buffer, len);337/* end estimation */338339rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[5], "RichPresencePatch", "");340if (!response->rich_presence_script)341response->rich_presence_script = "";342343if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[6], "Achievements"))344return RC_MISSING_VALUE;345346if (response->num_achievements) {347response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t));348if (!response->achievements)349return RC_OUT_OF_MEMORY;350351result = rc_api_process_fetch_game_data_achievements(&response->response, response->achievements, &array_field);352if (result != RC_OK)353return result;354}355356if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[7], "Leaderboards"))357return RC_MISSING_VALUE;358359if (response->num_leaderboards) {360response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));361if (!response->leaderboards)362return RC_OUT_OF_MEMORY;363364result = rc_api_process_fetch_game_data_leaderboards(&response->response, response->leaderboards, &array_field);365if (result != RC_OK)366return result;367}368369return RC_OK;370}371372void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) {373rc_buffer_destroy(&response->response.buffer);374}375376/* --- Fetch Game Sets --- */377378int rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params) {379return rc_api_init_fetch_game_sets_request_hosted(request, api_params, &g_host);380}381382int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request,383const rc_api_fetch_game_sets_request_t* api_params,384const rc_api_host_t* host) {385rc_api_url_builder_t builder;386387rc_api_url_build_dorequest_url(request, host);388389if (!api_params->game_id && (!api_params->game_hash || !api_params->game_hash[0]))390return RC_INVALID_STATE;391392rc_url_builder_init(&builder, &request->buffer, 48);393if (rc_api_url_build_dorequest(&builder, "achievementsets", api_params->username, api_params->api_token)) {394if (api_params->game_id)395rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);396else397rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);398399request->post_data = rc_url_builder_finalize(&builder);400request->content_type = RC_CONTENT_TYPE_URLENCODED;401}402403return builder.result;404}405406static int rc_api_process_fetch_game_sets_achievement_sets(rc_api_fetch_game_sets_response_t* response,407rc_api_achievement_set_definition_t* subset,408rc_json_field_t* subset_array_field) {409rc_json_iterator_t iterator;410rc_json_field_t array_field;411size_t len;412int result;413414rc_json_field_t subset_fields[] = {415RC_JSON_NEW_FIELD("AchievementSetId"),416RC_JSON_NEW_FIELD("GameId"),417RC_JSON_NEW_FIELD("Title"),418RC_JSON_NEW_FIELD("Type"),419RC_JSON_NEW_FIELD("ImageIconUrl"),420RC_JSON_NEW_FIELD("Achievements"), /* array */421RC_JSON_NEW_FIELD("Leaderboards") /* array */422};423424memset(&iterator, 0, sizeof(iterator));425iterator.json = subset_array_field->value_start;426iterator.end = subset_array_field->value_end;427428while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) {429if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "AchievementSetId"))430return RC_MISSING_VALUE;431if (!rc_json_get_required_unum(&subset->game_id, &response->response, &subset_fields[1], "GameId"))432return RC_MISSING_VALUE;433434if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[2], "Title"))435return RC_MISSING_VALUE;436if (!subset->title || !subset->title[0])437subset->title = response->title;438439if (rc_json_field_string_matches(&subset_fields[3], "core"))440subset->type = RC_ACHIEVEMENT_SET_TYPE_CORE;441else if (rc_json_field_string_matches(&subset_fields[3], "bonus"))442subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS;443else if (rc_json_field_string_matches(&subset_fields[3], "specialty"))444subset->type = RC_ACHIEVEMENT_SET_TYPE_SPECIALTY;445else if (rc_json_field_string_matches(&subset_fields[3], "exclusive"))446subset->type = RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE;447else448subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS;449450if (rc_json_field_string_matches(&subset_fields[4], response->image_url)) {451subset->image_url = response->image_url;452subset->image_name = response->image_name;453}454else {455if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[4], "ImageIconUrl"))456return RC_MISSING_VALUE;457rc_json_extract_filename(&subset_fields[4]);458rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[4], "ImageIconUrl", "");459}460461/* estimate the amount of space necessary to store the achievements, and leaderboards.462determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)463and add space for the structures. */464len = (subset_fields[5].value_end - subset_fields[5].value_start) - /* achievements */465subset_fields[5].array_size * (80 - sizeof(rc_api_achievement_definition_t));466len += (subset_fields[6].value_end - subset_fields[6].value_start) - /* leaderboards */467subset_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));468469rc_buffer_reserve(&response->response.buffer, len);470/* end estimation */471472if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[5], "Achievements"))473return RC_MISSING_VALUE;474475if (subset->num_achievements) {476subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t));477if (!subset->achievements)478return RC_OUT_OF_MEMORY;479480result = rc_api_process_fetch_game_data_achievements(&response->response, subset->achievements, &array_field);481if (result != RC_OK)482return result;483}484485if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[6], "Leaderboards"))486return RC_MISSING_VALUE;487488if (subset->num_leaderboards) {489subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));490if (!subset->leaderboards)491return RC_OUT_OF_MEMORY;492493result = rc_api_process_fetch_game_data_leaderboards(&response->response, subset->leaderboards, &array_field);494if (result != RC_OK)495return result;496}497498++subset;499}500501return RC_OK;502}503504int rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response) {505rc_json_field_t array_field;506int result;507508rc_json_field_t fields[] = {509RC_JSON_NEW_FIELD("Success"),510RC_JSON_NEW_FIELD("Error"),511RC_JSON_NEW_FIELD("Code"),512RC_JSON_NEW_FIELD("GameId"),513RC_JSON_NEW_FIELD("Title"),514RC_JSON_NEW_FIELD("ConsoleId"),515RC_JSON_NEW_FIELD("ImageIconUrl"),516RC_JSON_NEW_FIELD("RichPresenceGameId"),517RC_JSON_NEW_FIELD("RichPresencePatch"),518RC_JSON_NEW_FIELD("Sets") /* array */519};520521memset(response, 0, sizeof(*response));522rc_buffer_init(&response->response.buffer);523524result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));525if (result != RC_OK || !response->response.succeeded)526return result;527528if (!rc_json_get_required_unum(&response->id, &response->response, &fields[3], "GameId"))529return RC_MISSING_VALUE;530if (!rc_json_get_required_string(&response->title, &response->response, &fields[4], "Title"))531return RC_MISSING_VALUE;532if (!rc_json_get_required_unum(&response->console_id, &response->response, &fields[5], "ConsoleId"))533return RC_MISSING_VALUE;534535rc_json_get_required_string(&response->image_url, &response->response, &fields[6], "ImageIconUrl");536rc_json_extract_filename(&fields[6]);537rc_json_get_required_string(&response->image_name, &response->response, &fields[6], "ImageIconUrl");538539rc_json_get_optional_unum(&response->session_game_id, &fields[7], "RichPresenceGameId", response->id);540541rc_json_get_optional_string(&response->rich_presence_script, &response->response, &fields[8], "RichPresencePatch", "");542if (!response->rich_presence_script)543response->rich_presence_script = "";544545rc_json_get_optional_array(&response->num_sets, &array_field, &fields[9], "Sets");546if (response->num_sets) {547response->sets = (rc_api_achievement_set_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_sets * sizeof(rc_api_achievement_set_definition_t));548if (!response->sets)549return RC_OUT_OF_MEMORY;550551result = rc_api_process_fetch_game_sets_achievement_sets(response, response->sets, &array_field);552if (result != RC_OK)553return result;554}555556return RC_OK;557}558559void rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response) {560rc_buffer_destroy(&response->response.buffer);561}562563/* --- Ping --- */564565int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) {566return rc_api_init_ping_request_hosted(request, api_params, &g_host);567}568569int rc_api_init_ping_request_hosted(rc_api_request_t* request,570const rc_api_ping_request_t* api_params,571const rc_api_host_t* host) {572rc_api_url_builder_t builder;573574rc_api_url_build_dorequest_url(request, host);575576if (api_params->game_id == 0)577return RC_INVALID_STATE;578579rc_url_builder_init(&builder, &request->buffer, 48);580if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) {581rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);582583if (api_params->rich_presence && *api_params->rich_presence)584rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence);585586if (api_params->game_hash && *api_params->game_hash) {587rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore);588rc_url_builder_append_str_param(&builder, "x", api_params->game_hash);589}590591request->post_data = rc_url_builder_finalize(&builder);592request->content_type = RC_CONTENT_TYPE_URLENCODED;593}594595return builder.result;596}597598int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) {599rc_api_server_response_t response_obj;600601memset(&response_obj, 0, sizeof(response_obj));602response_obj.body = server_response;603response_obj.body_length = rc_json_get_object_string_length(server_response);604605return rc_api_process_ping_server_response(response, &response_obj);606}607608int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) {609rc_json_field_t fields[] = {610RC_JSON_NEW_FIELD("Success"),611RC_JSON_NEW_FIELD("Error")612};613614memset(response, 0, sizeof(*response));615rc_buffer_init(&response->response.buffer);616617return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));618}619620void rc_api_destroy_ping_response(rc_api_ping_response_t* response) {621rc_buffer_destroy(&response->response.buffer);622}623624/* --- Award Achievement --- */625626int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) {627return rc_api_init_award_achievement_request_hosted(request, api_params, &g_host);628}629630int rc_api_init_award_achievement_request_hosted(rc_api_request_t* request,631const rc_api_award_achievement_request_t* api_params,632const rc_api_host_t* host) {633rc_api_url_builder_t builder;634char buffer[33];635md5_state_t md5;636md5_byte_t digest[16];637638rc_api_url_build_dorequest_url(request, host);639640if (api_params->achievement_id == 0)641return RC_INVALID_STATE;642643rc_url_builder_init(&builder, &request->buffer, 96);644if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) {645rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id);646rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0);647if (api_params->game_hash && *api_params->game_hash)648rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);649if (api_params->seconds_since_unlock)650rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_unlock);651652/* Evaluate the signature. */653md5_init(&md5);654snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);655md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));656md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));657snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0);658md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));659if (api_params->seconds_since_unlock) {660/* second achievement id is needed by delegated unlock. including it here allows overloading661* the hash generating code on the server */662snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);663md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));664snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_unlock);665md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));666}667md5_finish(&md5, digest);668rc_format_md5(buffer, digest);669rc_url_builder_append_str_param(&builder, "v", buffer);670671request->post_data = rc_url_builder_finalize(&builder);672request->content_type = RC_CONTENT_TYPE_URLENCODED;673}674675return builder.result;676}677678int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) {679rc_api_server_response_t response_obj;680681memset(&response_obj, 0, sizeof(response_obj));682response_obj.body = server_response;683response_obj.body_length = rc_json_get_object_string_length(server_response);684685return rc_api_process_award_achievement_server_response(response, &response_obj);686}687688int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) {689int result;690rc_json_field_t fields[] = {691RC_JSON_NEW_FIELD("Success"),692RC_JSON_NEW_FIELD("Error"),693RC_JSON_NEW_FIELD("Score"),694RC_JSON_NEW_FIELD("SoftcoreScore"),695RC_JSON_NEW_FIELD("AchievementID"),696RC_JSON_NEW_FIELD("AchievementsRemaining")697};698699memset(response, 0, sizeof(*response));700rc_buffer_init(&response->response.buffer);701702result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));703if (result != RC_OK)704return result;705706if (!response->response.succeeded) {707if (response->response.error_message &&708memcmp(response->response.error_message, "User already has", 16) == 0) {709/* not really an error, the achievement is unlocked, just not by the current call.710* hardcore: User already has hardcore and regular achievements awarded.711* non-hardcore: User already has this achievement awarded.712*/713response->response.succeeded = 1;714} else {715return result;716}717}718719rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0);720rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0);721rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0);722rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1);723724return RC_OK;725}726727void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) {728rc_buffer_destroy(&response->response.buffer);729}730731/* --- Submit Leaderboard Entry --- */732733int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) {734return rc_api_init_submit_lboard_entry_request_hosted(request, api_params, &g_host);735}736737int rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request,738const rc_api_submit_lboard_entry_request_t* api_params,739const rc_api_host_t* host) {740rc_api_url_builder_t builder;741char buffer[33];742md5_state_t md5;743md5_byte_t digest[16];744745rc_api_url_build_dorequest_url(request, host);746747if (api_params->leaderboard_id == 0)748return RC_INVALID_STATE;749750rc_url_builder_init(&builder, &request->buffer, 96);751if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) {752rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id);753rc_url_builder_append_num_param(&builder, "s", api_params->score);754755if (api_params->game_hash && *api_params->game_hash)756rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);757758if (api_params->seconds_since_completion)759rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_completion);760761/* Evaluate the signature. */762md5_init(&md5);763snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id);764md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));765md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));766snprintf(buffer, sizeof(buffer), "%d", api_params->score);767md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));768if (api_params->seconds_since_completion) {769snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_completion);770md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));771}772md5_finish(&md5, digest);773rc_format_md5(buffer, digest);774rc_url_builder_append_str_param(&builder, "v", buffer);775776request->post_data = rc_url_builder_finalize(&builder);777request->content_type = RC_CONTENT_TYPE_URLENCODED;778}779780return builder.result;781}782783int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) {784rc_api_server_response_t response_obj;785786memset(&response_obj, 0, sizeof(response_obj));787response_obj.body = server_response;788response_obj.body_length = rc_json_get_object_string_length(server_response);789790return rc_api_process_submit_lboard_entry_server_response(response, &response_obj);791}792793int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) {794rc_api_lboard_entry_t* entry;795rc_json_field_t array_field;796rc_json_iterator_t iterator;797const char* str;798int result;799800rc_json_field_t fields[] = {801RC_JSON_NEW_FIELD("Success"),802RC_JSON_NEW_FIELD("Error"),803RC_JSON_NEW_FIELD("Response") /* nested object */804};805806rc_json_field_t response_fields[] = {807RC_JSON_NEW_FIELD("Score"),808RC_JSON_NEW_FIELD("BestScore"),809RC_JSON_NEW_FIELD("RankInfo"), /* nested object */810RC_JSON_NEW_FIELD("TopEntries") /* array */811/* unused fields812RC_JSON_NEW_FIELD("LBData"), / * array * /813RC_JSON_NEW_FIELD("ScoreFormatted"),814RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * /815* unused fields */816};817818/* unused fields819rc_json_field_t lbdata_fields[] = {820RC_JSON_NEW_FIELD("Format"),821RC_JSON_NEW_FIELD("LeaderboardID"),822RC_JSON_NEW_FIELD("GameID"),823RC_JSON_NEW_FIELD("Title"),824RC_JSON_NEW_FIELD("LowerIsBetter")825};826* unused fields */827828rc_json_field_t entry_fields[] = {829RC_JSON_NEW_FIELD("User"),830RC_JSON_NEW_FIELD("Rank"),831RC_JSON_NEW_FIELD("Score")832/* unused fields833RC_JSON_NEW_FIELD("DateSubmitted")834* unused fields */835};836837rc_json_field_t rank_info_fields[] = {838RC_JSON_NEW_FIELD("Rank"),839RC_JSON_NEW_FIELD("NumEntries")840/* unused fields841RC_JSON_NEW_FIELD("LowerIsBetter"),842RC_JSON_NEW_FIELD("UserRank")843* unused fields */844};845846memset(response, 0, sizeof(*response));847rc_buffer_init(&response->response.buffer);848849result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));850if (result != RC_OK || !response->response.succeeded)851return result;852853if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response"))854return RC_MISSING_VALUE;855if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score"))856return RC_MISSING_VALUE;857if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore"))858return RC_MISSING_VALUE;859860if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo"))861return RC_MISSING_VALUE;862if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank"))863return RC_MISSING_VALUE;864if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries"))865return RC_MISSING_VALUE;866response->num_entries = (unsigned)atoi(str);867868if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries"))869return RC_MISSING_VALUE;870871if (response->num_top_entries) {872response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t));873if (!response->top_entries)874return RC_OUT_OF_MEMORY;875876memset(&iterator, 0, sizeof(iterator));877iterator.json = array_field.value_start;878iterator.end = array_field.value_end;879880entry = response->top_entries;881while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) {882if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User"))883return RC_MISSING_VALUE;884885if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank"))886return RC_MISSING_VALUE;887888if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score"))889return RC_MISSING_VALUE;890891++entry;892}893}894895return RC_OK;896}897898void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) {899rc_buffer_destroy(&response->response.buffer);900}901902903