2022-07-18 12:46:12 +00:00
|
|
|
#include "rc_api_runtime.h"
|
|
|
|
#include "rc_api_common.h"
|
|
|
|
|
|
|
|
#include "rc_runtime.h"
|
|
|
|
#include "rc_runtime_types.h"
|
2023-11-06 09:41:10 +00:00
|
|
|
#include "../rc_compat.h"
|
2022-07-18 12:46:12 +00:00
|
|
|
#include "../rhash/md5.h"
|
|
|
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
/* --- Resolve Hash --- */
|
|
|
|
|
|
|
|
int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) {
|
|
|
|
rc_api_url_builder_t builder;
|
|
|
|
|
|
|
|
rc_api_url_build_dorequest_url(request);
|
|
|
|
|
|
|
|
if (!api_params->game_hash || !*api_params->game_hash)
|
|
|
|
return RC_INVALID_STATE;
|
|
|
|
|
|
|
|
rc_url_builder_init(&builder, &request->buffer, 48);
|
|
|
|
rc_url_builder_append_str_param(&builder, "r", "gameid");
|
|
|
|
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
|
|
|
|
request->post_data = rc_url_builder_finalize(&builder);
|
2023-08-09 09:39:42 +00:00
|
|
|
request->content_type = RC_CONTENT_TYPE_URLENCODED;
|
2022-07-18 12:46:12 +00:00
|
|
|
|
|
|
|
return builder.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) {
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_api_server_response_t response_obj;
|
|
|
|
|
|
|
|
memset(&response_obj, 0, sizeof(response_obj));
|
|
|
|
response_obj.body = server_response;
|
|
|
|
response_obj.body_length = rc_json_get_object_string_length(server_response);
|
|
|
|
|
|
|
|
return rc_api_process_resolve_hash_server_response(response, &response_obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) {
|
2022-07-18 12:46:12 +00:00
|
|
|
int result;
|
|
|
|
rc_json_field_t fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Success"),
|
|
|
|
RC_JSON_NEW_FIELD("Error"),
|
|
|
|
RC_JSON_NEW_FIELD("GameID")
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
memset(response, 0, sizeof(*response));
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_init(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (result != RC_OK)
|
|
|
|
return result;
|
|
|
|
|
|
|
|
rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID");
|
|
|
|
return RC_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) {
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_destroy(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* --- Fetch Game Data --- */
|
|
|
|
|
|
|
|
int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) {
|
|
|
|
rc_api_url_builder_t builder;
|
|
|
|
|
|
|
|
rc_api_url_build_dorequest_url(request);
|
|
|
|
|
|
|
|
if (api_params->game_id == 0)
|
|
|
|
return RC_INVALID_STATE;
|
|
|
|
|
|
|
|
rc_url_builder_init(&builder, &request->buffer, 48);
|
|
|
|
if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) {
|
|
|
|
rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
|
|
|
|
request->post_data = rc_url_builder_finalize(&builder);
|
2023-08-09 09:39:42 +00:00
|
|
|
request->content_type = RC_CONTENT_TYPE_URLENCODED;
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return builder.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) {
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_api_server_response_t response_obj;
|
|
|
|
|
|
|
|
memset(&response_obj, 0, sizeof(response_obj));
|
|
|
|
response_obj.body = server_response;
|
|
|
|
response_obj.body_length = rc_json_get_object_string_length(server_response);
|
|
|
|
|
|
|
|
return rc_api_process_fetch_game_data_server_response(response, &response_obj);
|
|
|
|
}
|
|
|
|
|
2024-01-13 04:24:04 +00:00
|
|
|
static int rc_parse_achievement_type(const char* type)
|
|
|
|
{
|
|
|
|
if (strcmp(type, "missable") == 0)
|
|
|
|
return RC_ACHIEVEMENT_TYPE_MISSABLE;
|
|
|
|
|
|
|
|
if (strcmp(type, "win_condition") == 0)
|
|
|
|
return RC_ACHIEVEMENT_TYPE_WIN;
|
|
|
|
|
|
|
|
if (strcmp(type, "progression") == 0)
|
|
|
|
return RC_ACHIEVEMENT_TYPE_PROGRESSION;
|
|
|
|
|
|
|
|
return RC_ACHIEVEMENT_TYPE_STANDARD;
|
|
|
|
}
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
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) {
|
2022-07-18 12:46:12 +00:00
|
|
|
rc_api_achievement_definition_t* achievement;
|
|
|
|
rc_api_leaderboard_definition_t* leaderboard;
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_json_field_t array_field;
|
|
|
|
rc_json_iterator_t iterator;
|
2022-07-18 12:46:12 +00:00
|
|
|
const char* str;
|
|
|
|
const char* last_author = "";
|
2023-08-09 09:39:42 +00:00
|
|
|
const char* last_author_field = "";
|
2022-07-18 12:46:12 +00:00
|
|
|
size_t last_author_len = 0;
|
|
|
|
size_t len;
|
2023-11-06 09:41:10 +00:00
|
|
|
uint32_t timet;
|
2022-07-18 12:46:12 +00:00
|
|
|
int result;
|
|
|
|
char format[16];
|
|
|
|
|
|
|
|
rc_json_field_t fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Success"),
|
|
|
|
RC_JSON_NEW_FIELD("Error"),
|
|
|
|
RC_JSON_NEW_FIELD("PatchData") /* nested object */
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
rc_json_field_t patchdata_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("ID"),
|
|
|
|
RC_JSON_NEW_FIELD("Title"),
|
|
|
|
RC_JSON_NEW_FIELD("ConsoleID"),
|
|
|
|
RC_JSON_NEW_FIELD("ImageIcon"),
|
|
|
|
RC_JSON_NEW_FIELD("RichPresencePatch"),
|
|
|
|
RC_JSON_NEW_FIELD("Achievements"), /* array */
|
|
|
|
RC_JSON_NEW_FIELD("Leaderboards") /* array */
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
rc_json_field_t achievement_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("ID"),
|
|
|
|
RC_JSON_NEW_FIELD("Title"),
|
|
|
|
RC_JSON_NEW_FIELD("Description"),
|
|
|
|
RC_JSON_NEW_FIELD("Flags"),
|
|
|
|
RC_JSON_NEW_FIELD("Points"),
|
|
|
|
RC_JSON_NEW_FIELD("MemAddr"),
|
|
|
|
RC_JSON_NEW_FIELD("Author"),
|
|
|
|
RC_JSON_NEW_FIELD("BadgeName"),
|
|
|
|
RC_JSON_NEW_FIELD("Created"),
|
2024-01-13 04:24:04 +00:00
|
|
|
RC_JSON_NEW_FIELD("Modified"),
|
|
|
|
RC_JSON_NEW_FIELD("Type"),
|
|
|
|
RC_JSON_NEW_FIELD("Rarity"),
|
|
|
|
RC_JSON_NEW_FIELD("RarityHardcore")
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
rc_json_field_t leaderboard_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("ID"),
|
|
|
|
RC_JSON_NEW_FIELD("Title"),
|
|
|
|
RC_JSON_NEW_FIELD("Description"),
|
|
|
|
RC_JSON_NEW_FIELD("Mem"),
|
|
|
|
RC_JSON_NEW_FIELD("Format"),
|
|
|
|
RC_JSON_NEW_FIELD("LowerIsBetter"),
|
|
|
|
RC_JSON_NEW_FIELD("Hidden")
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
memset(response, 0, sizeof(*response));
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_init(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (result != RC_OK || !response->response.succeeded)
|
|
|
|
return result;
|
|
|
|
|
|
|
|
if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[2], "PatchData"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
/* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */
|
|
|
|
if (patchdata_fields[3].value_end) {
|
|
|
|
str = patchdata_fields[3].value_end - 5;
|
|
|
|
if (memcmp(str, ".png\"", 5) == 0) {
|
|
|
|
patchdata_fields[3].value_end -= 5;
|
|
|
|
|
|
|
|
while (str > patchdata_fields[3].value_start && str[-1] != '/')
|
|
|
|
--str;
|
|
|
|
|
|
|
|
patchdata_fields[3].value_start = str;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", "");
|
|
|
|
|
|
|
|
/* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards.
|
|
|
|
determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)
|
|
|
|
and add space for the structures. */
|
|
|
|
len = patchdata_fields[4].value_end - patchdata_fields[4].value_start; /* rich presence */
|
|
|
|
|
|
|
|
len += (patchdata_fields[5].value_end - patchdata_fields[5].value_start) - /* achievements */
|
|
|
|
patchdata_fields[5].array_size * (130 - sizeof(rc_api_achievement_definition_t));
|
|
|
|
|
|
|
|
len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* leaderboards */
|
|
|
|
patchdata_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));
|
|
|
|
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_reserve(&response->response.buffer, len);
|
2022-07-18 12:46:12 +00:00
|
|
|
/* end estimation */
|
|
|
|
|
|
|
|
rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[4], "RichPresencePatch", "");
|
|
|
|
if (!response->rich_presence_script)
|
|
|
|
response->rich_presence_script = "";
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements"))
|
2022-07-18 12:46:12 +00:00
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (response->num_achievements) {
|
2023-11-06 09:41:10 +00:00
|
|
|
response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (!response->achievements)
|
|
|
|
return RC_OUT_OF_MEMORY;
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
memset(&iterator, 0, sizeof(iterator));
|
|
|
|
iterator.json = array_field.value_start;
|
|
|
|
iterator.end = array_field.value_end;
|
|
|
|
|
2022-07-18 12:46:12 +00:00
|
|
|
achievement = response->achievements;
|
|
|
|
while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) {
|
|
|
|
if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
len = achievement_fields[6].value_end - achievement_fields[6].value_start;
|
|
|
|
if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) {
|
2022-07-18 12:46:12 +00:00
|
|
|
achievement->author = last_author;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
last_author = achievement->author;
|
2023-08-09 09:39:42 +00:00
|
|
|
last_author_field = achievement_fields[6].value_start;
|
2022-07-18 12:46:12 +00:00
|
|
|
last_author_len = len;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
achievement->created = (time_t)timet;
|
|
|
|
if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
achievement->updated = (time_t)timet;
|
|
|
|
|
2024-01-13 04:24:04 +00:00
|
|
|
achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;
|
|
|
|
if (achievement_fields[10].value_end) {
|
|
|
|
len = achievement_fields[10].value_end - achievement_fields[10].value_start - 2;
|
|
|
|
if (len < sizeof(format) - 1) {
|
|
|
|
memcpy(format, achievement_fields[10].value_start + 1, len);
|
|
|
|
format[len] = '\0';
|
|
|
|
achievement->type = rc_parse_achievement_type(format);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* legacy support : if title contains[m], change type to missable and remove[m] from title */
|
|
|
|
if (memcmp(achievement->title, "[m]", 3) == 0) {
|
|
|
|
len = 3;
|
|
|
|
while (achievement->title[len] == ' ')
|
|
|
|
++len;
|
|
|
|
achievement->title += len;
|
|
|
|
achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
|
|
|
|
}
|
|
|
|
else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) {
|
|
|
|
len = strlen(achievement->title) - 3;
|
|
|
|
while (achievement->title[len - 1] == ' ')
|
|
|
|
--len;
|
|
|
|
((char*)achievement->title)[len] = '\0';
|
|
|
|
achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0);
|
|
|
|
rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0);
|
|
|
|
|
2022-07-18 12:46:12 +00:00
|
|
|
++achievement;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards"))
|
2022-07-18 12:46:12 +00:00
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (response->num_leaderboards) {
|
2023-11-06 09:41:10 +00:00
|
|
|
response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (!response->leaderboards)
|
|
|
|
return RC_OUT_OF_MEMORY;
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
memset(&iterator, 0, sizeof(iterator));
|
|
|
|
iterator.json = array_field.value_start;
|
|
|
|
iterator.end = array_field.value_end;
|
|
|
|
|
2022-07-18 12:46:12 +00:00
|
|
|
leaderboard = response->leaderboards;
|
|
|
|
while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) {
|
|
|
|
if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem"))
|
|
|
|
return RC_MISSING_VALUE;
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0);
|
|
|
|
leaderboard->lower_is_better = (uint8_t)result;
|
|
|
|
rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0);
|
|
|
|
leaderboard->hidden = (uint8_t)result;
|
2022-07-18 12:46:12 +00:00
|
|
|
|
|
|
|
if (!leaderboard_fields[4].value_end)
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2;
|
|
|
|
if (len < sizeof(format) - 1) {
|
|
|
|
memcpy(format, leaderboard_fields[4].value_start + 1, len);
|
|
|
|
format[len] = '\0';
|
|
|
|
leaderboard->format = rc_parse_format(format);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
leaderboard->format = RC_FORMAT_VALUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
++leaderboard;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return RC_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) {
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_destroy(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* --- Ping --- */
|
|
|
|
|
|
|
|
int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) {
|
|
|
|
rc_api_url_builder_t builder;
|
|
|
|
|
|
|
|
rc_api_url_build_dorequest_url(request);
|
|
|
|
|
|
|
|
if (api_params->game_id == 0)
|
|
|
|
return RC_INVALID_STATE;
|
|
|
|
|
|
|
|
rc_url_builder_init(&builder, &request->buffer, 48);
|
|
|
|
if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) {
|
|
|
|
rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
|
|
|
|
|
|
|
|
if (api_params->rich_presence && *api_params->rich_presence)
|
|
|
|
rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence);
|
|
|
|
|
2024-01-13 04:24:04 +00:00
|
|
|
if (api_params->game_hash && *api_params->game_hash) {
|
|
|
|
rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore);
|
|
|
|
rc_url_builder_append_str_param(&builder, "x", api_params->game_hash);
|
|
|
|
}
|
|
|
|
|
2022-07-18 12:46:12 +00:00
|
|
|
request->post_data = rc_url_builder_finalize(&builder);
|
2023-08-09 09:39:42 +00:00
|
|
|
request->content_type = RC_CONTENT_TYPE_URLENCODED;
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return builder.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) {
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_api_server_response_t response_obj;
|
|
|
|
|
|
|
|
memset(&response_obj, 0, sizeof(response_obj));
|
|
|
|
response_obj.body = server_response;
|
|
|
|
response_obj.body_length = rc_json_get_object_string_length(server_response);
|
|
|
|
|
|
|
|
return rc_api_process_ping_server_response(response, &response_obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) {
|
2022-07-18 12:46:12 +00:00
|
|
|
rc_json_field_t fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Success"),
|
|
|
|
RC_JSON_NEW_FIELD("Error")
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
memset(response, 0, sizeof(*response));
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_init(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void rc_api_destroy_ping_response(rc_api_ping_response_t* response) {
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_destroy(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* --- Award Achievement --- */
|
|
|
|
|
|
|
|
int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) {
|
|
|
|
rc_api_url_builder_t builder;
|
|
|
|
char buffer[33];
|
|
|
|
md5_state_t md5;
|
|
|
|
md5_byte_t digest[16];
|
|
|
|
|
|
|
|
rc_api_url_build_dorequest_url(request);
|
|
|
|
|
|
|
|
if (api_params->achievement_id == 0)
|
|
|
|
return RC_INVALID_STATE;
|
|
|
|
|
|
|
|
rc_url_builder_init(&builder, &request->buffer, 96);
|
|
|
|
if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) {
|
|
|
|
rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id);
|
|
|
|
rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0);
|
|
|
|
if (api_params->game_hash && *api_params->game_hash)
|
|
|
|
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
|
|
|
|
|
|
|
|
/* Evaluate the signature. */
|
|
|
|
md5_init(&md5);
|
|
|
|
snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);
|
|
|
|
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
|
|
|
|
md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
|
|
|
|
snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0);
|
|
|
|
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
|
|
|
|
md5_finish(&md5, digest);
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_format_md5(buffer, digest);
|
2022-07-18 12:46:12 +00:00
|
|
|
rc_url_builder_append_str_param(&builder, "v", buffer);
|
|
|
|
|
|
|
|
request->post_data = rc_url_builder_finalize(&builder);
|
2023-08-09 09:39:42 +00:00
|
|
|
request->content_type = RC_CONTENT_TYPE_URLENCODED;
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return builder.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) {
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_api_server_response_t response_obj;
|
|
|
|
|
|
|
|
memset(&response_obj, 0, sizeof(response_obj));
|
|
|
|
response_obj.body = server_response;
|
|
|
|
response_obj.body_length = rc_json_get_object_string_length(server_response);
|
|
|
|
|
|
|
|
return rc_api_process_award_achievement_server_response(response, &response_obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) {
|
2022-07-18 12:46:12 +00:00
|
|
|
int result;
|
|
|
|
rc_json_field_t fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Success"),
|
|
|
|
RC_JSON_NEW_FIELD("Error"),
|
|
|
|
RC_JSON_NEW_FIELD("Score"),
|
|
|
|
RC_JSON_NEW_FIELD("SoftcoreScore"),
|
|
|
|
RC_JSON_NEW_FIELD("AchievementID"),
|
|
|
|
RC_JSON_NEW_FIELD("AchievementsRemaining")
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
memset(response, 0, sizeof(*response));
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_init(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (result != RC_OK)
|
|
|
|
return result;
|
|
|
|
|
|
|
|
if (!response->response.succeeded) {
|
|
|
|
if (response->response.error_message &&
|
|
|
|
memcmp(response->response.error_message, "User already has", 16) == 0) {
|
|
|
|
/* not really an error, the achievement is unlocked, just not by the current call.
|
|
|
|
* hardcore: User already has hardcore and regular achievements awarded.
|
|
|
|
* non-hardcore: User already has this achievement awarded.
|
|
|
|
*/
|
|
|
|
response->response.succeeded = 1;
|
|
|
|
} else {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0);
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0);
|
|
|
|
rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0);
|
|
|
|
rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1);
|
2022-07-18 12:46:12 +00:00
|
|
|
|
|
|
|
return RC_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) {
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_destroy(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* --- Submit Leaderboard Entry --- */
|
|
|
|
|
|
|
|
int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) {
|
|
|
|
rc_api_url_builder_t builder;
|
|
|
|
char buffer[33];
|
|
|
|
md5_state_t md5;
|
|
|
|
md5_byte_t digest[16];
|
|
|
|
|
|
|
|
rc_api_url_build_dorequest_url(request);
|
|
|
|
|
|
|
|
if (api_params->leaderboard_id == 0)
|
|
|
|
return RC_INVALID_STATE;
|
|
|
|
|
|
|
|
rc_url_builder_init(&builder, &request->buffer, 96);
|
|
|
|
if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) {
|
|
|
|
rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id);
|
|
|
|
rc_url_builder_append_num_param(&builder, "s", api_params->score);
|
|
|
|
|
|
|
|
if (api_params->game_hash && *api_params->game_hash)
|
|
|
|
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
|
|
|
|
|
|
|
|
/* Evaluate the signature. */
|
|
|
|
md5_init(&md5);
|
|
|
|
snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id);
|
|
|
|
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
|
|
|
|
md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
|
|
|
|
snprintf(buffer, sizeof(buffer), "%d", api_params->score);
|
|
|
|
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
|
|
|
|
md5_finish(&md5, digest);
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_format_md5(buffer, digest);
|
2022-07-18 12:46:12 +00:00
|
|
|
rc_url_builder_append_str_param(&builder, "v", buffer);
|
|
|
|
|
|
|
|
request->post_data = rc_url_builder_finalize(&builder);
|
2023-08-09 09:39:42 +00:00
|
|
|
request->content_type = RC_CONTENT_TYPE_URLENCODED;
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return builder.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) {
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_api_server_response_t response_obj;
|
|
|
|
|
|
|
|
memset(&response_obj, 0, sizeof(response_obj));
|
|
|
|
response_obj.body = server_response;
|
|
|
|
response_obj.body_length = rc_json_get_object_string_length(server_response);
|
|
|
|
|
|
|
|
return rc_api_process_submit_lboard_entry_server_response(response, &response_obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2022-07-18 12:46:12 +00:00
|
|
|
rc_api_lboard_entry_t* entry;
|
2023-08-09 09:39:42 +00:00
|
|
|
rc_json_field_t array_field;
|
|
|
|
rc_json_iterator_t iterator;
|
2022-07-18 12:46:12 +00:00
|
|
|
const char* str;
|
|
|
|
int result;
|
|
|
|
|
|
|
|
rc_json_field_t fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Success"),
|
|
|
|
RC_JSON_NEW_FIELD("Error"),
|
|
|
|
RC_JSON_NEW_FIELD("Response") /* nested object */
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
rc_json_field_t response_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Score"),
|
|
|
|
RC_JSON_NEW_FIELD("BestScore"),
|
|
|
|
RC_JSON_NEW_FIELD("RankInfo"), /* nested object */
|
|
|
|
RC_JSON_NEW_FIELD("TopEntries") /* array */
|
2022-07-18 12:46:12 +00:00
|
|
|
/* unused fields
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("LBData"), / * array * /
|
|
|
|
RC_JSON_NEW_FIELD("ScoreFormatted"),
|
|
|
|
RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * /
|
2022-07-18 12:46:12 +00:00
|
|
|
* unused fields */
|
|
|
|
};
|
|
|
|
|
|
|
|
/* unused fields
|
|
|
|
rc_json_field_t lbdata_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Format"),
|
|
|
|
RC_JSON_NEW_FIELD("LeaderboardID"),
|
|
|
|
RC_JSON_NEW_FIELD("GameID"),
|
|
|
|
RC_JSON_NEW_FIELD("Title"),
|
|
|
|
RC_JSON_NEW_FIELD("LowerIsBetter")
|
2022-07-18 12:46:12 +00:00
|
|
|
};
|
|
|
|
* unused fields */
|
|
|
|
|
|
|
|
rc_json_field_t entry_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("User"),
|
|
|
|
RC_JSON_NEW_FIELD("Rank"),
|
|
|
|
RC_JSON_NEW_FIELD("Score")
|
2022-07-18 12:46:12 +00:00
|
|
|
/* unused fields
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("DateSubmitted")
|
2022-07-18 12:46:12 +00:00
|
|
|
* unused fields */
|
|
|
|
};
|
|
|
|
|
|
|
|
rc_json_field_t rank_info_fields[] = {
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("Rank"),
|
|
|
|
RC_JSON_NEW_FIELD("NumEntries")
|
2022-07-18 12:46:12 +00:00
|
|
|
/* unused fields
|
2023-08-09 09:39:42 +00:00
|
|
|
RC_JSON_NEW_FIELD("LowerIsBetter"),
|
|
|
|
RC_JSON_NEW_FIELD("UserRank")
|
2022-07-18 12:46:12 +00:00
|
|
|
* unused fields */
|
|
|
|
};
|
|
|
|
|
|
|
|
memset(response, 0, sizeof(*response));
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_init(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (result != RC_OK || !response->response.succeeded)
|
|
|
|
return result;
|
|
|
|
|
|
|
|
if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
response->num_entries = (unsigned)atoi(str);
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries"))
|
2022-07-18 12:46:12 +00:00
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (response->num_top_entries) {
|
2023-11-06 09:41:10 +00:00
|
|
|
response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t));
|
2022-07-18 12:46:12 +00:00
|
|
|
if (!response->top_entries)
|
|
|
|
return RC_OUT_OF_MEMORY;
|
|
|
|
|
2023-08-09 09:39:42 +00:00
|
|
|
memset(&iterator, 0, sizeof(iterator));
|
|
|
|
iterator.json = array_field.value_start;
|
|
|
|
iterator.end = array_field.value_end;
|
|
|
|
|
2022-07-18 12:46:12 +00:00
|
|
|
entry = response->top_entries;
|
|
|
|
while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) {
|
|
|
|
if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score"))
|
|
|
|
return RC_MISSING_VALUE;
|
|
|
|
|
|
|
|
++entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return RC_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) {
|
2023-11-06 09:41:10 +00:00
|
|
|
rc_buffer_destroy(&response->response.buffer);
|
2022-07-18 12:46:12 +00:00
|
|
|
}
|