mirror of
https://github.com/libretro/RetroArch.git
synced 2024-12-01 04:00:32 +00:00
676 lines
20 KiB
C
676 lines
20 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2019-2021 - Brian Weiss
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "cheevos_client.h"
|
|
|
|
#include "cheevos.h"
|
|
|
|
#include "../configuration.h"
|
|
#include "../paths.h"
|
|
#include "../version.h"
|
|
|
|
#include <string/stdstring.h>
|
|
#include <features/features_cpu.h>
|
|
|
|
#include "../frontend/frontend_driver.h"
|
|
#include "../network/net_http_special.h"
|
|
#include "../tasks/tasks_internal.h"
|
|
|
|
#ifdef HAVE_DISCORD
|
|
#include "../network/discord.h"
|
|
#endif
|
|
|
|
#include "../deps/rcheevos/include/rc_api_runtime.h"
|
|
|
|
|
|
|
|
/* Define this macro to log URLs. */
|
|
#undef CHEEVOS_LOG_URLS
|
|
|
|
/* Define this macro to have the password and token logged.
|
|
* THIS WILL DISCLOSE THE USER'S PASSWORD, TAKE CARE! */
|
|
#undef CHEEVOS_LOG_PASSWORD
|
|
|
|
|
|
/* Number of usecs to wait between posting rich presence to the site. */
|
|
/* Keep consistent with SERVER_PING_FREQUENCY from RAIntegration. */
|
|
#define CHEEVOS_PING_FREQUENCY 2 * 60 * 1000000
|
|
|
|
|
|
/****************************
|
|
* data types *
|
|
****************************/
|
|
|
|
enum rcheevos_async_io_type
|
|
{
|
|
CHEEVOS_ASYNC_RICHPRESENCE = 0,
|
|
CHEEVOS_ASYNC_AWARD_ACHIEVEMENT,
|
|
CHEEVOS_ASYNC_SUBMIT_LBOARD
|
|
};
|
|
|
|
typedef void (*rcheevos_async_handler)(int id,
|
|
http_transfer_data_t *data, char buffer[], size_t buffer_size);
|
|
|
|
typedef struct rcheevos_async_io_request
|
|
{
|
|
rc_api_request_t request;
|
|
rcheevos_async_handler handler;
|
|
int id;
|
|
int attempt_count;
|
|
const char* success_message;
|
|
const char* failure_message;
|
|
const char* user_agent;
|
|
char type;
|
|
} rcheevos_async_io_request;
|
|
|
|
|
|
/****************************
|
|
* forward declarations *
|
|
****************************/
|
|
|
|
static retro_time_t rcheevos_client_prepare_ping(rcheevos_async_io_request* request);
|
|
|
|
static void rcheevos_async_http_task_callback(
|
|
retro_task_t* task, void* task_data, void* user_data, const char* error);
|
|
|
|
|
|
/****************************
|
|
* user agent construction *
|
|
****************************/
|
|
|
|
static int append_no_spaces(char* buffer, char* stop, const char* text)
|
|
{
|
|
char* ptr = buffer;
|
|
|
|
while (ptr < stop && *text)
|
|
{
|
|
if (*text == ' ')
|
|
{
|
|
*ptr++ = '_';
|
|
++text;
|
|
}
|
|
else
|
|
{
|
|
*ptr++ = *text++;
|
|
}
|
|
}
|
|
|
|
*ptr = '\0';
|
|
return (int)(ptr - buffer);
|
|
}
|
|
|
|
void rcheevos_get_user_agent(rcheevos_locals_t *locals,
|
|
char *buffer, size_t len)
|
|
{
|
|
struct retro_system_info *system = runloop_get_libretro_system_info();
|
|
char* ptr;
|
|
|
|
/* if we haven't calculated the non-changing portion yet, do so now
|
|
* [retroarch version + os version] */
|
|
if (!locals->user_agent_prefix[0])
|
|
{
|
|
const frontend_ctx_driver_t *frontend = frontend_get_ptr();
|
|
int major, minor;
|
|
char tmp[64];
|
|
|
|
if (frontend && frontend->get_os)
|
|
{
|
|
frontend->get_os(tmp, sizeof(tmp), &major, &minor);
|
|
snprintf(locals->user_agent_prefix, sizeof(locals->user_agent_prefix),
|
|
"RetroArch/%s (%s %d.%d)", PACKAGE_VERSION, tmp, major, minor);
|
|
}
|
|
else
|
|
{
|
|
snprintf(locals->user_agent_prefix, sizeof(locals->user_agent_prefix),
|
|
"RetroArch/%s", PACKAGE_VERSION);
|
|
}
|
|
}
|
|
|
|
/* append the non-changing portion */
|
|
ptr = buffer + strlcpy(buffer, locals->user_agent_prefix, len);
|
|
|
|
/* if a core is loaded, append its information */
|
|
if (system && !string_is_empty(system->library_name))
|
|
{
|
|
char* stop = buffer + len - 1;
|
|
const char* path = path_get(RARCH_PATH_CORE);
|
|
*ptr++ = ' ';
|
|
|
|
if (!string_is_empty(path))
|
|
{
|
|
append_no_spaces(ptr, stop, path_basename(path));
|
|
path_remove_extension(ptr);
|
|
ptr += strlen(ptr);
|
|
}
|
|
else
|
|
{
|
|
ptr += append_no_spaces(ptr, stop, system->library_name);
|
|
}
|
|
|
|
if (system->library_version)
|
|
{
|
|
*ptr++ = '/';
|
|
ptr += append_no_spaces(ptr, stop, system->library_version);
|
|
}
|
|
}
|
|
|
|
*ptr = '\0';
|
|
}
|
|
|
|
#ifdef CHEEVOS_LOG_URLS
|
|
#ifndef CHEEVOS_LOG_PASSWORD
|
|
static void rcheevos_filter_url_param(char* url, char* param)
|
|
{
|
|
char *next;
|
|
size_t param_len = strlen(param);
|
|
char *start = strchr(url, '?');
|
|
if (!start)
|
|
start = url;
|
|
else
|
|
++start;
|
|
|
|
do
|
|
{
|
|
next = strchr(start, '&');
|
|
|
|
if (start[param_len] == '=' && memcmp(start, param, param_len) == 0)
|
|
{
|
|
if (next)
|
|
strcpy_literal(start, next + 1);
|
|
else if (start > url)
|
|
start[-1] = '\0';
|
|
else
|
|
*start = '\0';
|
|
|
|
return;
|
|
}
|
|
|
|
if (!next)
|
|
return;
|
|
|
|
start = next + 1;
|
|
} while (1);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
void rcheevos_log_url(const char* api, const char* url)
|
|
{
|
|
#ifdef CHEEVOS_LOG_URLS
|
|
#ifdef CHEEVOS_LOG_PASSWORD
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "GET %s\n", url);
|
|
#else
|
|
char copy[256];
|
|
strlcpy(copy, url, sizeof(copy));
|
|
rcheevos_filter_url_param(copy, "p");
|
|
rcheevos_filter_url_param(copy, "t");
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "GET %s\n", copy);
|
|
#endif
|
|
#else
|
|
(void)api;
|
|
(void)url;
|
|
#endif
|
|
}
|
|
|
|
static void rcheevos_log_post_url(const char* url, const char* post)
|
|
{
|
|
#ifdef CHEEVOS_LOG_URLS
|
|
#ifdef CHEEVOS_LOG_PASSWORD
|
|
if (post && post[0])
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "POST %s %s\n", url, post);
|
|
else
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url);
|
|
#else
|
|
if (post && post[0])
|
|
{
|
|
char post_copy[2048];
|
|
strlcpy(post_copy, post, sizeof(post_copy));
|
|
rcheevos_filter_url_param(post_copy, "p");
|
|
rcheevos_filter_url_param(post_copy, "t");
|
|
|
|
if (post_copy[0])
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "POST %s %s\n", url, post_copy);
|
|
else
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url);
|
|
}
|
|
else
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "POST %s\n", url);
|
|
}
|
|
#endif
|
|
#else
|
|
(void)url;
|
|
(void)post;
|
|
#endif
|
|
}
|
|
|
|
|
|
/****************************
|
|
* dispatch *
|
|
****************************/
|
|
|
|
static void rcheevos_async_retry_request(retro_task_t* task)
|
|
{
|
|
rcheevos_async_io_request* request = (rcheevos_async_io_request*)
|
|
task->user_data;
|
|
|
|
/* the timer task has done its job. let it dispose itself */
|
|
task_set_finished(task, 1);
|
|
|
|
/* start a new task for the HTTP call */
|
|
task_push_http_post_transfer_with_user_agent(request->request.url,
|
|
request->request.post_data, true, "POST", request->user_agent,
|
|
rcheevos_async_http_task_callback, request);
|
|
}
|
|
|
|
static void rcheevos_async_retry_request_after_delay(rcheevos_async_io_request* request, const char* error)
|
|
{
|
|
retro_task_t* task = task_init();
|
|
|
|
/* Double the wait between each attempt until we hit
|
|
* a maximum delay of two minutes.
|
|
* 250ms -> 500ms -> 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s... */
|
|
retro_time_t retry_delay = (request->attempt_count > 8)
|
|
? (120 * 1000 * 1000)
|
|
: ((250 * 1000) << request->attempt_count);
|
|
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s (automatic retry in %dms)\n", request->failure_message,
|
|
request->id, error, (int)retry_delay / 1000);
|
|
|
|
task->when = cpu_features_get_time_usec() + retry_delay;
|
|
task->handler = rcheevos_async_retry_request;
|
|
task->user_data = request;
|
|
task->progress = -1;
|
|
|
|
++request->attempt_count;
|
|
task_queue_push(task);
|
|
}
|
|
|
|
static void rcheevos_async_request_failed(rcheevos_async_io_request* request, const char* error)
|
|
{
|
|
if (request->type == CHEEVOS_ASYNC_RICHPRESENCE && request->attempt_count > 0)
|
|
{
|
|
/* only retry the ping once (in case of network hiccup), otherwise let
|
|
* the timer handle it after the normal ping period has elapsed */
|
|
CHEEVOS_ERR(RCHEEVOS_TAG "%s %u: %s\n", request->failure_message,
|
|
request->id, error);
|
|
}
|
|
else
|
|
{
|
|
/* automatically retry the request */
|
|
rcheevos_async_retry_request_after_delay(request, error);
|
|
}
|
|
}
|
|
|
|
static void rcheevos_async_http_task_callback(
|
|
retro_task_t* task, void* task_data, void* user_data, const char* error)
|
|
{
|
|
rcheevos_async_io_request *request = (rcheevos_async_io_request*)user_data;
|
|
http_transfer_data_t *data = (http_transfer_data_t*)task_data;
|
|
char buffer[224];
|
|
|
|
if (error)
|
|
{
|
|
/* there was a communication error */
|
|
rcheevos_async_request_failed(request, error);
|
|
return;
|
|
}
|
|
|
|
if (!data)
|
|
{
|
|
/* Server did not return HTTP headers */
|
|
strlcpy(buffer, "Server communication error", sizeof(buffer));
|
|
}
|
|
else if (!data->data || !data->len)
|
|
{
|
|
if (data->status <= 0)
|
|
{
|
|
/* something occurred which prevented the response from being processed.
|
|
* assume the server request hasn't happened and try again. */
|
|
snprintf(buffer, sizeof(buffer), "task status code %d", data->status);
|
|
rcheevos_async_request_failed(request, buffer);
|
|
return;
|
|
}
|
|
|
|
if (data->status != 200)
|
|
{
|
|
/* Server returned an error via status code. */
|
|
snprintf(buffer, sizeof(buffer), "HTTP error code %d", data->status);
|
|
}
|
|
else
|
|
{
|
|
/* Server sent an empty response without an error status code */
|
|
strlcpy(buffer, "No response from server", sizeof(buffer));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer[0] = '\0'; /* indicate success unless handler provides error */
|
|
|
|
/* Call appropriate handler to process the response */
|
|
if (request->handler)
|
|
{
|
|
/* NOTE: data->data is not null-terminated. Most handlers assume the
|
|
* response is properly formatted or will encounter a parse failure
|
|
* before reading past the end of the data */
|
|
request->handler(request->id, data, buffer, sizeof(buffer));
|
|
}
|
|
}
|
|
|
|
if (!buffer[0])
|
|
{
|
|
/* success */
|
|
if (request->success_message)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s %u\n", request->success_message, request->id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* encountered an error */
|
|
char errbuf[256];
|
|
snprintf(errbuf, sizeof(errbuf), "%s %u: %s",
|
|
request->failure_message, request->id, buffer);
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", errbuf);
|
|
|
|
switch (request->type)
|
|
{
|
|
case CHEEVOS_ASYNC_RICHPRESENCE:
|
|
/* Don't bother informing user when
|
|
* rich presence update fails */
|
|
break;
|
|
|
|
default:
|
|
runloop_msg_queue_push(errbuf, 0, 5 * 60, false, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
break;
|
|
}
|
|
}
|
|
|
|
rc_api_destroy_request(&request->request);
|
|
|
|
/* rich presence request will be reused on next ping - reset the attempt
|
|
* counter. for all other request types, free the request object */
|
|
if (request->type == CHEEVOS_ASYNC_RICHPRESENCE)
|
|
request->attempt_count = 0;
|
|
else
|
|
free(request);
|
|
}
|
|
|
|
static void rcheevos_async_begin_request(rcheevos_async_io_request* request,
|
|
rcheevos_async_handler handler, char type, int id,
|
|
const char* success_message, const char* failure_message)
|
|
{
|
|
request->handler = handler;
|
|
request->type = type;
|
|
request->id = id;
|
|
request->success_message = success_message;
|
|
request->failure_message = failure_message;
|
|
request->attempt_count = 0;
|
|
|
|
if (!request->user_agent)
|
|
request->user_agent = get_rcheevos_locals()->user_agent_core;
|
|
|
|
rcheevos_log_post_url(request->request.url, request->request.post_data);
|
|
|
|
task_push_http_post_transfer_with_user_agent(request->request.url,
|
|
request->request.post_data, true, "POST", request->user_agent,
|
|
rcheevos_async_http_task_callback, request);
|
|
}
|
|
|
|
static bool rcheevos_async_succeeded(int result,
|
|
const rc_api_response_t* response, char buffer[], size_t buffer_size)
|
|
{
|
|
if (result != RC_OK)
|
|
{
|
|
strlcpy(buffer, rc_error_str(result), buffer_size);
|
|
return false;
|
|
}
|
|
|
|
if (!response->succeeded)
|
|
{
|
|
strlcpy(buffer, response->error_message, buffer_size);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/****************************
|
|
* ping *
|
|
****************************/
|
|
|
|
static retro_time_t rcheevos_client_prepare_ping(rcheevos_async_io_request* request)
|
|
{
|
|
const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals();
|
|
const settings_t *settings = config_get_ptr();
|
|
const bool cheevos_richpresence_enable =
|
|
settings->bools.cheevos_richpresence_enable;
|
|
rc_api_ping_request_t api_params;
|
|
char buffer[256] = "";
|
|
|
|
memset(&api_params, 0, sizeof(api_params));
|
|
api_params.username = rcheevos_locals->username;
|
|
api_params.api_token = rcheevos_locals->token;
|
|
api_params.game_id = request->id;
|
|
|
|
if (cheevos_richpresence_enable)
|
|
{
|
|
rcheevos_get_richpresence(buffer, sizeof(buffer));
|
|
api_params.rich_presence = buffer;
|
|
}
|
|
|
|
rc_api_init_ping_request(&request->request, &api_params);
|
|
|
|
rcheevos_log_post_url(request->request.url, request->request.post_data);
|
|
|
|
#ifdef HAVE_DISCORD
|
|
if (settings->bools.discord_enable && discord_is_ready())
|
|
discord_update(DISCORD_PRESENCE_RETROACHIEVEMENTS);
|
|
#endif
|
|
|
|
/* Update rich presence every two minutes */
|
|
if (cheevos_richpresence_enable)
|
|
return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY;
|
|
|
|
/* Send ping every four minutes */
|
|
return cpu_features_get_time_usec() + CHEEVOS_PING_FREQUENCY * 2;
|
|
}
|
|
|
|
static void rcheevos_async_ping_handler(retro_task_t* task)
|
|
{
|
|
rcheevos_async_io_request* request = (rcheevos_async_io_request*)
|
|
task->user_data;
|
|
|
|
const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals();
|
|
if (request->id != (int)rcheevos_locals->patchdata.game_id)
|
|
{
|
|
/* game changed; stop the recurring task - a new one will
|
|
* be scheduled if a new game is loaded */
|
|
task_set_finished(task, 1);
|
|
/* request->request was destroyed in rcheevos_async_http_task_callback */
|
|
free(request);
|
|
return;
|
|
}
|
|
|
|
/* update the request and set the task to fire again in
|
|
* two minutes */
|
|
task->when = rcheevos_client_prepare_ping(request);
|
|
|
|
/* start the HTTP request */
|
|
task_push_http_post_transfer_with_user_agent(request->request.url,
|
|
request->request.post_data, true, "POST", request->user_agent,
|
|
rcheevos_async_http_task_callback, request);
|
|
}
|
|
|
|
|
|
/****************************
|
|
* start session *
|
|
****************************/
|
|
|
|
void rcheevos_client_start_session(unsigned game_id)
|
|
{
|
|
rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals();
|
|
|
|
/* the core won't change while a session is active, so only
|
|
* calculate the user agent once */
|
|
rcheevos_get_user_agent(rcheevos_locals,
|
|
rcheevos_locals->user_agent_core,
|
|
sizeof(rcheevos_locals->user_agent_core));
|
|
|
|
/* force non-HTTPS until everything uses RAPI */
|
|
rc_api_set_host("http://retroachievements.org");
|
|
|
|
/* schedule the first rich presence call in 30 seconds */
|
|
{
|
|
rcheevos_async_io_request *request = (rcheevos_async_io_request*)
|
|
calloc(1, sizeof(rcheevos_async_io_request));
|
|
if (!request)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate rich presence request\n");
|
|
}
|
|
else
|
|
{
|
|
retro_task_t* task = task_init();
|
|
|
|
request->id = game_id;
|
|
request->type = CHEEVOS_ASYNC_RICHPRESENCE;
|
|
request->user_agent = rcheevos_locals->user_agent_core;
|
|
request->failure_message = "Error sending ping";
|
|
|
|
task->handler = rcheevos_async_ping_handler;
|
|
task->user_data = request;
|
|
task->progress = -1;
|
|
task->when = cpu_features_get_time_usec() +
|
|
CHEEVOS_PING_FREQUENCY / 4;
|
|
|
|
task_queue_push(task);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/****************************
|
|
* award achievement *
|
|
****************************/
|
|
|
|
static void rcheevos_async_award_achievement_callback(int id,
|
|
http_transfer_data_t *data, char buffer[], size_t buffer_size)
|
|
{
|
|
rc_api_award_achievement_response_t api_response;
|
|
|
|
int result = rc_api_process_award_achievement_response(&api_response, data->data);
|
|
if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size))
|
|
{
|
|
if (api_response.awarded_achievement_id != id)
|
|
{
|
|
snprintf(buffer, buffer_size, "Achievement %u awarded instead",
|
|
api_response.awarded_achievement_id);
|
|
}
|
|
else if (api_response.response.error_message)
|
|
{
|
|
/* previously unlocked achievements are returned as a "successful" error */
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Achievement %u: %s\n",
|
|
id, api_response.response.error_message);
|
|
}
|
|
}
|
|
|
|
rc_api_destroy_award_achievement_response(&api_response);
|
|
}
|
|
|
|
void rcheevos_client_award_achievement(unsigned achievement_id)
|
|
{
|
|
rcheevos_async_io_request *request = (rcheevos_async_io_request*)
|
|
calloc(1, sizeof(rcheevos_async_io_request));
|
|
if (!request)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate unlock request for achievement %u\n",
|
|
achievement_id);
|
|
}
|
|
else
|
|
{
|
|
const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals();
|
|
rc_api_award_achievement_request_t api_params;
|
|
|
|
memset(&api_params, 0, sizeof(api_params));
|
|
api_params.username = rcheevos_locals->username;
|
|
api_params.api_token = rcheevos_locals->token;
|
|
api_params.achievement_id = achievement_id;
|
|
api_params.hardcore = rcheevos_locals->hardcore_active ? 1 : 0;
|
|
api_params.game_hash = rcheevos_locals->hash;
|
|
|
|
rc_api_init_award_achievement_request(&request->request, &api_params);
|
|
|
|
rcheevos_async_begin_request(request,
|
|
rcheevos_async_award_achievement_callback,
|
|
CHEEVOS_ASYNC_AWARD_ACHIEVEMENT, achievement_id,
|
|
"Awarded achievement",
|
|
"Error awarding achievement");
|
|
}
|
|
}
|
|
|
|
|
|
/****************************
|
|
* submit leaderboard *
|
|
****************************/
|
|
|
|
static void rcheevos_async_submit_lboard_entry_callback(int id,
|
|
http_transfer_data_t* data, char buffer[], size_t buffer_size)
|
|
{
|
|
rc_api_submit_lboard_entry_response_t api_response;
|
|
|
|
int result = rc_api_process_submit_lboard_entry_response(&api_response, data->data);
|
|
|
|
if (rcheevos_async_succeeded(result, &api_response.response, buffer, buffer_size))
|
|
{
|
|
/* not currently doing anything with the response */
|
|
}
|
|
|
|
rc_api_destroy_submit_lboard_entry_response(&api_response);
|
|
}
|
|
|
|
void rcheevos_client_submit_lboard_entry(unsigned leaderboard_id, int value)
|
|
{
|
|
rcheevos_async_io_request *request = (rcheevos_async_io_request*)
|
|
calloc(1, sizeof(rcheevos_async_io_request));
|
|
if (!request)
|
|
{
|
|
CHEEVOS_LOG(RCHEEVOS_TAG "Failed to allocate request for lboard %u submit\n",
|
|
leaderboard_id);
|
|
}
|
|
else
|
|
{
|
|
const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals();
|
|
rc_api_submit_lboard_entry_request_t api_params;
|
|
|
|
memset(&api_params, 0, sizeof(api_params));
|
|
api_params.username = rcheevos_locals->username;
|
|
api_params.api_token = rcheevos_locals->token;
|
|
api_params.leaderboard_id = leaderboard_id;
|
|
api_params.score = value;
|
|
api_params.game_hash = rcheevos_locals->hash;
|
|
|
|
rc_api_init_submit_lboard_entry_request(&request->request, &api_params);
|
|
|
|
rcheevos_async_begin_request(request,
|
|
rcheevos_async_submit_lboard_entry_callback,
|
|
CHEEVOS_ASYNC_SUBMIT_LBOARD, leaderboard_id,
|
|
"Submitted leaderboard",
|
|
"Error submitting leaderboard");
|
|
}
|
|
}
|
|
|