diff --git a/dep/rcheevos/CMakeLists.txt b/dep/rcheevos/CMakeLists.txt index fa64649dd..4f050e78f 100644 --- a/dep/rcheevos/CMakeLists.txt +++ b/dep/rcheevos/CMakeLists.txt @@ -1,4 +1,9 @@ add_library(rcheevos + include/rc_api_editor.h + include/rc_api_info.h + include/rc_api_request.h + include/rc_api_runtime.h + include/rc_api_user.h include/rcheevos.h include/rc_consoles.h include/rc_error.h @@ -6,6 +11,12 @@ add_library(rcheevos include/rc_runtime.h include/rc_runtime_types.h include/rc_url.h + src/rapi/rc_api_common.c + src/rapi/rc_api_common.h + src/rapi/rc_api_editor.c + src/rapi/rc_api_info.c + src/rapi/rc_api_runtime.c + src/rapi/rc_api_user.c src/rcheevos/alloc.c src/rcheevos/compat.c src/rcheevos/condition.c diff --git a/dep/rcheevos/include/rc_api_editor.h b/dep/rcheevos/include/rc_api_editor.h new file mode 100644 index 000000000..2bda50325 --- /dev/null +++ b/dep/rcheevos/include/rc_api_editor.h @@ -0,0 +1,247 @@ +#ifndef RC_API_EDITOR_H +#define RC_API_EDITOR_H + +#include "rc_api_request.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Fetch Code Notes --- */ + +/** + * API parameters for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_request_t { + /* The unique identifier of the game */ + unsigned game_id; +} +rc_api_fetch_code_notes_request_t; + +/* A code note definiton */ +typedef struct rc_api_code_note_t { + /* The address the note is associated to */ + unsigned address; + /* The name of the use who last updated the note */ + const char* author; + /* The contents of the note */ + const char* note; +} rc_api_code_note_t; + +/** + * Response data for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_response_t { + /* An array of code notes for the game */ + rc_api_code_note_t* notes; + /* The number of items in the notes array */ + unsigned num_notes; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_code_notes_response_t; + +int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params); +int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response); +void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response); + +/* --- Update Code Note --- */ + +/** + * API parameters for an update code note request. + */ +typedef struct rc_api_update_code_note_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; + /* The address the note is associated to */ + unsigned address; + /* The contents of the note (NULL or empty to delete a note) */ + const char* note; +} +rc_api_update_code_note_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_update_code_note_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_code_note_response_t; + +int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params); +int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response); +void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response); + +/* --- Update Achievement --- */ + +/** + * API parameters for an update achievement request. + */ +typedef struct rc_api_update_achievement_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement (0 to create a new achievement) */ + unsigned achievement_id; + /* The unique identifier of the game */ + unsigned game_id; + /* The name of the achievement */ + const char* title; + /* The description of the achievement */ + const char* description; + /* The badge name for the achievement */ + const char* badge; + /* The serialized trigger for the achievement */ + const char* trigger; + /* The number of points the achievement is worth */ + unsigned points; + /* The category of the achievement */ + unsigned category; +} +rc_api_update_achievement_request_t; + +/** + * Response data for an update achievement request. + */ +typedef struct rc_api_update_achievement_response_t { + /* The unique identifier of the achievement */ + unsigned achievement_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_achievement_response_t; + +int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params); +int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response); +void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response); + +/* --- Update Leaderboard --- */ + +/** + * API parameters for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard (0 to create a new leaderboard) */ + unsigned leaderboard_id; + /* The unique identifier of the game */ + unsigned game_id; + /* The name of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The start trigger for the leaderboard */ + const char* start_trigger; + /* The submit trigger for the leaderboard */ + const char* submit_trigger; + /* The cancel trigger for the leaderboard */ + const char* cancel_trigger; + /* The value definition for the leaderboard */ + const char* value_definition; + /* The format of leaderboard values */ + const char* format; + /* Whether or not lower scores are better for the leaderboard */ + int lower_is_better; +} +rc_api_update_leaderboard_request_t; + +/** + * Response data for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_response_t { + /* The unique identifier of the leaderboard */ + unsigned leaderboard_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_leaderboard_response_t; + +int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params); +int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response); +void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response); + +/* --- Fetch Badge Range --- */ + +/** + * API parameters for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_request_t { + /* Unused */ + unsigned unused; +} +rc_api_fetch_badge_range_request_t; + +/** + * Response data for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_response_t { + /* The numeric identifier of the first valid badge ID */ + unsigned first_badge_id; + /* The numeric identifier of the first unassigned badge ID */ + unsigned next_badge_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_badge_range_response_t; + +int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params); +int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response); +void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response); + +/* --- Add Game Hash --- */ + +/** + * API parameters for an add game hash request. + */ +typedef struct rc_api_add_game_hash_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game (0 to create a new game entry) */ + unsigned game_id; + /* The unique identifier of the console for the game */ + unsigned console_id; + /* The title of the game */ + const char* title; + /* The hash being added */ + const char* hash; + /* A description of the hash being added (usually the normalized ROM name) */ + const char* hash_description; +} +rc_api_add_game_hash_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_add_game_hash_response_t { + /* The unique identifier of the game */ + unsigned game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_add_game_hash_response_t; + +int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params); +int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response); +void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_EDITOR_H */ diff --git a/dep/rcheevos/include/rc_api_info.h b/dep/rcheevos/include/rc_api_info.h new file mode 100644 index 000000000..7979cc391 --- /dev/null +++ b/dep/rcheevos/include/rc_api_info.h @@ -0,0 +1,182 @@ +#ifndef RC_API_INFO_H +#define RC_API_INFO_H + +#include "rc_api_request.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Fetch Achievement Info --- */ + +/** + * API parameters for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + unsigned achievement_id; + /* The 1-based index of the first entry to retrieve */ + unsigned first_entry; + /* The number of entries to retrieve */ + unsigned count; + /* Non-zero to only return unlocks earned by the user's friends */ + unsigned friends_only; +} +rc_api_fetch_achievement_info_request_t; + +/* An achievement awarded entry */ +typedef struct rc_api_achievement_awarded_entry_t { + /* The user associated to the entry */ + const char* username; + /* When the achievement was awarded */ + time_t awarded; +} +rc_api_achievement_awarded_entry_t; + +/** + * Response data for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_response_t { + /* The unique identifier of the achievement */ + unsigned id; + /* The unique identifier of the game to which the leaderboard is associated */ + unsigned game_id; + /* The number of times the achievement has been awarded */ + unsigned num_awarded; + /* The number of players that have earned at least one achievement for the game */ + unsigned num_players; + + /* An array of recently rewarded entries */ + rc_api_achievement_awarded_entry_t* recently_awarded; + /* The number of items in the recently_awarded array */ + unsigned num_recently_awarded; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_achievement_info_response_t; + +int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); +int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); + +/* --- Fetch Leaderboard Info --- */ + +/** + * API parameters for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_request_t { + /* The unique identifier of the leaderboard */ + unsigned leaderboard_id; + /* The number of entries to retrieve */ + unsigned count; + /* The 1-based index of the first entry to retrieve */ + unsigned first_entry; + /* The username of the player around whom the entries should be returned */ + const char* username; +} +rc_api_fetch_leaderboard_info_request_t; + +/* A leaderboard info entry */ +typedef struct rc_api_lboard_info_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + unsigned rank; + /* The index of the entry */ + unsigned index; + /* The value of the entry */ + int score; + /* When the entry was submitted */ + time_t submitted; +} +rc_api_lboard_info_entry_t; + +/** + * Response data for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_response_t { + /* The unique identifier of the leaderboard */ + unsigned id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* If non-zero, indicates that lower scores appear first */ + int lower_is_better; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ + const char* definition; + /* The unique identifier of the game to which the leaderboard is associated */ + unsigned game_id; + /* The author of the leaderboard */ + const char* author; + /* When the leaderboard was first uploaded to the server */ + time_t created; + /* When the leaderboard was last modified on the server */ + time_t updated; + + /* An array of requested entries */ + rc_api_lboard_info_entry_t* entries; + /* The number of items in the entries array */ + unsigned num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_leaderboard_info_response_t; + +int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); +int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); + +/* --- Fetch Games List --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_request_t { + /* The unique identifier of the console to query */ + unsigned console_id; +} +rc_api_fetch_games_list_request_t; + +/* A game list entry */ +typedef struct rc_api_game_list_entry_t { + /* The unique identifier of the game */ + unsigned id; + /* The name of the game */ + const char* name; +} +rc_api_game_list_entry_t; + +/** + * Response data for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_response_t { + /* An array of requested entries */ + rc_api_game_list_entry_t* entries; + /* The number of items in the entries array */ + unsigned num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_games_list_response_t; + +int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); +int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_INFO_H */ diff --git a/dep/rcheevos/include/rc_api_request.h b/dep/rcheevos/include/rc_api_request.h new file mode 100644 index 000000000..8ba482a8f --- /dev/null +++ b/dep/rcheevos/include/rc_api_request.h @@ -0,0 +1,63 @@ +#ifndef RC_API_REQUEST_H +#define RC_API_REQUEST_H + +#include "rc_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_api_buffer_t { + /* The current location where data is being written */ + char* write; + /* The first byte past the end of data where writing cannot occur */ + char* end; + /* The next block in the allocated memory chain */ + struct rc_api_buffer_t* next; + /* The buffer containing the data. The actual size may be larger than 256 bytes for buffers allocated in + * the next chain. The 256 byte size specified is for the initial allocation within the container object. */ + char data[256]; +} +rc_api_buffer_t; + +/** + * A constructed request to send to the retroachievements server. + */ +typedef struct rc_api_request_t { + /* The URL to send the request to (contains protocol, host, path, and query args) */ + const char* url; + /* Additional query args that should be sent via a POST command. If null, GET may be used */ + const char* post_data; + + /* Storage for the url and post_data */ + rc_api_buffer_t buffer; +} +rc_api_request_t; + +/** + * Common attributes for all server responses. + */ +typedef struct rc_api_response_t { + /* Server-provided success indicator (non-zero on failure) */ + int succeeded; + /* Server-provided message associated to the failure */ + const char* error_message; + + /* Storage for the response data */ + rc_api_buffer_t buffer; +} +rc_api_response_t; + +void rc_api_destroy_request(rc_api_request_t* request); + +void rc_api_set_host(const char* hostname); +void rc_api_set_image_host(const char* hostname); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_REQUEST_H */ diff --git a/dep/rcheevos/include/rc_api_runtime.h b/dep/rcheevos/include/rc_api_runtime.h new file mode 100644 index 000000000..68f56fd51 --- /dev/null +++ b/dep/rcheevos/include/rc_api_runtime.h @@ -0,0 +1,291 @@ +#ifndef RC_API_RUNTIME_H +#define RC_API_RUNTIME_H + +#include "rc_api_request.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Fetch Image --- */ + +/** + * API parameters for a fetch image request. + * NOTE: fetch image server response is the raw image data. There is no rc_api_process_fetch_image_response function. + */ +typedef struct rc_api_fetch_image_request_t { + /* The name of the image to fetch */ + const char* image_name; + /* The type of image to fetch */ + int image_type; +} +rc_api_fetch_image_request_t; + +#define RC_IMAGE_TYPE_GAME 1 +#define RC_IMAGE_TYPE_ACHIEVEMENT 2 +#define RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED 3 +#define RC_IMAGE_TYPE_USER 4 + +int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params); + +/* --- Resolve Hash --- */ + +/** + * API parameters for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_request_t { + /* Unused - hash lookup does not require credentials */ + const char* username; + /* Unused - hash lookup does not require credentials */ + const char* api_token; + /* The generated hash of the game to be identified */ + const char* game_hash; +} +rc_api_resolve_hash_request_t; + +/** + * Response data for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_response_t { + /* The unique identifier of the game, 0 if no match was found */ + unsigned game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_resolve_hash_response_t; + +int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); +int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); + +/* --- Fetch Game Data --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; +} +rc_api_fetch_game_data_request_t; + +/* A leaderboard definition */ +typedef struct rc_api_leaderboard_definition_t { + /* The unique identifier of the leaderboard */ + unsigned id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ + const char* definition; + /* Non-zero if lower values are better for this leaderboard */ + int lower_is_better; + /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ + int hidden; +} +rc_api_leaderboard_definition_t; + +/* An achievement definition */ +typedef struct rc_api_achievement_definition_t { + /* The unique identifier of the achievement */ + unsigned id; + /* The number of points the achievement is worth */ + unsigned points; + /* The achievement category (core, unofficial) */ + unsigned category; + /* The title of the achievement */ + const char* title; + /* The dscription of the achievement */ + const char* description; + /* The definition of the achievement to be passed to rc_runtime_activate_achievement */ + const char* definition; + /* The author of the achievment */ + const char* author; + /* The image name for the achievement badge */ + const char* badge_name; + /* When the achievement was first uploaded to the server */ + time_t created; + /* When the achievement was last modified on the server */ + time_t updated; +} +rc_api_achievement_definition_t; + +#define RC_ACHIEVEMENT_CATEGORY_CORE 3 +#define RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL 5 + +/** + * Response data for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_response_t { + /* The unique identifier of the game */ + unsigned id; + /* The console associated to the game */ + unsigned console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + + /* An array of achievements for the game */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + unsigned num_achievements; + + /* An array of leaderboards for the game */ + rc_api_leaderboard_definition_t* leaderboards; + /* The number of items in the leaderboards array */ + unsigned num_leaderboards; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_data_response_t; + +int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); +int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); + +/* --- Ping --- */ + +/** + * API parameters for a ping request. + */ +typedef struct rc_api_ping_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; + /* (optional) The current rich presence evaluation for the user */ + const char* rich_presence; +} +rc_api_ping_request_t; + +/** + * Response data for a ping request. + */ +typedef struct rc_api_ping_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_ping_response_t; + +int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); +int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +void rc_api_destroy_ping_response(rc_api_ping_response_t* response); + +/* --- Award Achievement --- */ + +/** + * API parameters for an award achievement request. + */ +typedef struct rc_api_award_achievement_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + unsigned achievement_id; + /* Non-zero if the achievement was earned in hardcore */ + int hardcore; + /* The hash associated to the game being played */ + const char* game_hash; +} +rc_api_award_achievement_request_t; + +/** + * Response data for an award achievement request. + */ +typedef struct rc_api_award_achievement_response_t { + /* The unique identifier of the achievement that was awarded */ + unsigned awarded_achievement_id; + /* The updated player score */ + unsigned new_player_score; + /* The number of achievements the user has not yet unlocked for this game + * (in hardcore/non-hardcore per hardcore flag in request) */ + unsigned achievements_remaining; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_award_achievement_response_t; + +int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); +int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); + +/* --- Submit Leaderboard Entry --- */ + +/** + * API parameters for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard */ + unsigned leaderboard_id; + /* The value being submitted */ + int score; + /* The hash associated to the game being played */ + const char* game_hash; +} +rc_api_submit_lboard_entry_request_t; + +/* A leaderboard entry */ +typedef struct rc_api_lboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + unsigned rank; + /* The value of the entry */ + int score; +} +rc_api_lboard_entry_t; + +/** + * Response data for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_response_t { + /* The value that was submitted */ + int submitted_score; + /* The player's best submitted value */ + int best_score; + /* The player's new rank within the leaderboard */ + unsigned new_rank; + /* The total number of entries in the leaderboard */ + unsigned num_entries; + + /* An array of the top entries for the leaderboard */ + rc_api_lboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + unsigned num_top_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_submit_lboard_entry_response_t; + +int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); +int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_RUNTIME_H */ diff --git a/dep/rcheevos/include/rc_api_user.h b/dep/rcheevos/include/rc_api_user.h new file mode 100644 index 000000000..758842557 --- /dev/null +++ b/dep/rcheevos/include/rc_api_user.h @@ -0,0 +1,117 @@ +#ifndef RC_API_USER_H +#define RC_API_USER_H + +#include "rc_api_request.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Login --- */ + +/** + * API parameters for a login request. + * If both password and api_token are provided, api_token will be ignored. + */ +typedef struct rc_api_login_request_t { + /* The username of the player being logged in */ + const char* username; + /* The API token from a previous login */ + const char* api_token; + /* The player's password */ + const char* password; +} +rc_api_login_request_t; + +/** + * Response data for a login request. + */ +typedef struct rc_api_login_response_t { + /* The case-corrected username of the player */ + const char* username; + /* The API token to use for all future requests */ + const char* api_token; + /* The current score of the player */ + unsigned score; + /* The number of unread messages waiting for the player on the web site */ + unsigned num_unread_messages; + /* The preferred name to display for the player */ + const char* display_name; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_login_response_t; + +int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); +int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +void rc_api_destroy_login_response(rc_api_login_response_t* response); + +/* --- Start Session --- */ + +/** + * API parameters for a start session request. + */ +typedef struct rc_api_start_session_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; +} +rc_api_start_session_request_t; + +/** + * Response data for a start session request. + */ +typedef struct rc_api_start_session_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_start_session_response_t; + +int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); +int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); + +/* --- Fetch User Unlocks --- */ + +/** + * API parameters for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + unsigned game_id; + /* Non-zero to fetch hardcore unlocks, 0 to fetch non-hardcore unlocks */ + int hardcore; +} +rc_api_fetch_user_unlocks_request_t; + +/** + * Response data for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_response_t { + /* An array of achievement IDs previously unlocked by the user */ + unsigned* achievement_ids; + /* The number of items in the achievement_ids array */ + unsigned num_achievement_ids; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_user_unlocks_response_t; + +int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); +int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_H */ diff --git a/dep/rcheevos/include/rc_consoles.h b/dep/rcheevos/include/rc_consoles.h index 484227626..6863485c7 100644 --- a/dep/rcheevos/include/rc_consoles.h +++ b/dep/rcheevos/include/rc_consoles.h @@ -81,6 +81,7 @@ enum { RC_CONSOLE_MEGADUCK = 69, RC_CONSOLE_ZEEBO = 70, RC_CONSOLE_ARDUBOY = 71, + RC_CONSOLE_WASM4 = 72, RC_CONSOLE_HUBS = 100, RC_CONSOLE_EVENTS = 101 diff --git a/dep/rcheevos/rcheevos.vcxproj b/dep/rcheevos/rcheevos.vcxproj index 35e85ec23..870d7daf2 100644 --- a/dep/rcheevos/rcheevos.vcxproj +++ b/dep/rcheevos/rcheevos.vcxproj @@ -1,8 +1,12 @@  - + + + + + @@ -24,23 +28,26 @@ + + + + + + - {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} - - TurnOffAllWarnings @@ -48,6 +55,5 @@ $(ProjectDir)include;%(AdditionalIncludeDirectories) - - + \ No newline at end of file diff --git a/dep/rcheevos/rcheevos.vcxproj.filters b/dep/rcheevos/rcheevos.vcxproj.filters index 68a695883..66e080824 100644 --- a/dep/rcheevos/rcheevos.vcxproj.filters +++ b/dep/rcheevos/rcheevos.vcxproj.filters @@ -13,6 +13,9 @@ {01fc10b0-c122-461b-b75a-f97c8b89d627} + + {92c73497-6936-4a1c-9534-4f2ffb64cba2} + @@ -69,6 +72,21 @@ rhash + + rapi + + + rapi + + + rapi + + + rapi + + + rapi + @@ -101,5 +119,23 @@ rhash + + include + + + include + + + include + + + include + + + include + + + rapi + - + \ No newline at end of file diff --git a/dep/rcheevos/src/rapi/rc_api_common.c b/dep/rcheevos/src/rapi/rc_api_common.c new file mode 100644 index 000000000..c703af752 --- /dev/null +++ b/dep/rcheevos/src/rapi/rc_api_common.c @@ -0,0 +1,1115 @@ +#include "rc_api_common.h" +#include "rc_api_request.h" +#include "rc_api_runtime.h" + +#include "../rcheevos/rc_compat.h" + +#include +#include +#include +#include + +#define RETROACHIEVEMENTS_HOST "https://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST "http://i.retroachievements.org" +static char* g_host = NULL; +static char* g_imagehost = NULL; + +#undef DEBUG_BUFFERS + +/* --- rc_json --- */ + +static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field); + +static int rc_json_parse_field(const char** json_ptr, rc_json_field_t* field) { + int result; + + field->value_start = *json_ptr; + + switch (**json_ptr) + { + case '"': /* quoted string */ + ++(*json_ptr); + while (**json_ptr != '"') { + if (**json_ptr == '\\') + ++(*json_ptr); + + if (**json_ptr == '\0') + return RC_INVALID_JSON; + + ++(*json_ptr); + } + ++(*json_ptr); + break; + + case '-': + case '+': /* signed number */ + ++(*json_ptr); + /* fallthrough to number */ + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': /* number */ + do { + ++(*json_ptr); + } while (**json_ptr >= '0' && **json_ptr <= '9'); + if (**json_ptr == '.') { + do { + ++(*json_ptr); + } while (**json_ptr >= '0' && **json_ptr <= '9'); + } + break; + + case '[': /* array */ + result = rc_json_parse_array(json_ptr, field); + if (result != RC_OK) + return result; + + break; + + case '{': /* object */ + result = rc_json_parse_object(json_ptr, NULL, 0, &field->array_size); + if (result != RC_OK) + return result; + + break; + + default: /* non-quoted text [true,false,null] */ + if (!isalpha((unsigned char)**json_ptr)) + return RC_INVALID_JSON; + + do { + ++(*json_ptr); + } while (isalnum((unsigned char)**json_ptr)); + break; + } + + field->value_end = *json_ptr; + return RC_OK; +} + +static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field) { + rc_json_field_t unused_field; + const char* json = *json_ptr; + int result; + + if (*json != '[') + return RC_INVALID_JSON; + ++json; + + field->array_size = 0; + if (*json != ']') { + do + { + while (isspace((unsigned char)*json)) + ++json; + + result = rc_json_parse_field(&json, &unused_field); + if (result != RC_OK) + return result; + + ++field->array_size; + + while (isspace((unsigned char)*json)) + ++json; + + if (*json != ',') + break; + + ++json; + } while (1); + + if (*json != ']') + return RC_INVALID_JSON; + } + + *json_ptr = ++json; + return RC_OK; +} + +static int rc_json_get_next_field(rc_json_object_field_iterator_t* iterator) { + const char* json = iterator->json; + + while (isspace((unsigned char)*json)) + ++json; + + if (*json != '"') + return RC_INVALID_JSON; + + iterator->field.name = ++json; + while (*json != '"') { + if (!*json) + return RC_INVALID_JSON; + ++json; + } + iterator->name_len = json - iterator->field.name; + ++json; + + while (isspace((unsigned char)*json)) + ++json; + + if (*json != ':') + return RC_INVALID_JSON; + + ++json; + + while (isspace((unsigned char)*json)) + ++json; + + if (rc_json_parse_field(&json, &iterator->field) < 0) + return RC_INVALID_JSON; + + while (isspace((unsigned char)*json)) + ++json; + + iterator->json = json; + return RC_OK; +} + +static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { + rc_json_object_field_iterator_t iterator; + const char* json = *json_ptr; + size_t i; + unsigned num_fields = 0; + int result; + + if (fields_seen) + *fields_seen = 0; + + for (i = 0; i < field_count; ++i) + fields[i].value_start = fields[i].value_end = NULL; + + if (*json != '{') + return RC_INVALID_JSON; + ++json; + + if (*json == '}') { + *json_ptr = ++json; + return RC_OK; + } + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + + do + { + result = rc_json_get_next_field(&iterator); + if (result != RC_OK) + return result; + + for (i = 0; i < field_count; ++i) { + if (!fields[i].value_start && strncmp(fields[i].name, iterator.field.name, iterator.name_len) == 0 && + fields[i].name[iterator.name_len] == '\0') { + fields[i].value_start = iterator.field.value_start; + fields[i].value_end = iterator.field.value_end; + fields[i].array_size = iterator.field.array_size; + break; + } + } + + ++num_fields; + if (*iterator.json != ',') + break; + + ++iterator.json; + } while (1); + + if (*iterator.json != '}') + return RC_INVALID_JSON; + + if (fields_seen) + *fields_seen = num_fields; + + *json_ptr = ++iterator.json; + return RC_OK; +} + +int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator) { + if (*iterator->json != ',' && *iterator->json != '{') + return 0; + + ++iterator->json; + return (rc_json_get_next_field(iterator) == RC_OK); +} + +int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count) { +#ifndef NDEBUG + if (field_count < 2) + return RC_INVALID_STATE; + if (strcmp(fields[0].name, "Success") != 0) + return RC_INVALID_STATE; + if (strcmp(fields[1].name, "Error") != 0) + return RC_INVALID_STATE; +#endif + + if (*json == '{') { + int result = rc_json_parse_object(&json, fields, field_count, NULL); + + rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); + rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + + return result; + } + + response->error_message = NULL; + + if (*json) { + const char* end = json; + while (*end && *end != '\n' && end - json < 200) + ++end; + + if (end > json && end[-1] == '\r') + --end; + + if (end > json) { + char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); + response->error_message = dst; + memcpy(dst, json, end - json); + dst += (end - json); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + } + } + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { + const char* not_found = " not found in response"; + const size_t not_found_len = strlen(not_found); + const size_t field_len = strlen(field->name); + + char* write = rc_buf_reserve(&response->buffer, field_len + not_found_len + 1); + if (write) { + response->error_message = write; + memcpy(write, field->name, field_len); + write += field_len; + memcpy(write, not_found, not_found_len + 1); + write += not_found_len + 1; + rc_buf_consume(&response->buffer, response->error_message, write); + } + + response->succeeded = 0; + return 0; +} + +int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { + const char* json = field->value_start; +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!json) + return rc_json_missing_field(response, field); + + return (rc_json_parse_object(&json, fields, field_count, &field->array_size) == RC_OK); +} + +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_field_t* iterator) { + if (!iterator->array_size) + return 0; + + while (isspace((unsigned char)*iterator->value_start)) + ++iterator->value_start; + + rc_json_parse_field(&iterator->value_start, field); + + while (isspace((unsigned char)*iterator->value_start)) + ++iterator->value_start; + + ++iterator->value_start; /* skip , or ] */ + + --iterator->array_size; + return 1; +} + +int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_field_t iterator; + rc_json_field_t value; + unsigned* entry; + + if (!rc_json_get_required_array(num_entries, &iterator, response, field, field_name)) + return RC_MISSING_VALUE; + + if (*num_entries) { + *entries = (unsigned*)rc_buf_alloc(&response->buffer, *num_entries * sizeof(unsigned)); + if (!*entries) + return RC_OUT_OF_MEMORY; + + value.name = field_name; + + entry = *entries; + while (rc_json_get_array_entry_value(&value, &iterator)) { + if (!rc_json_get_unum(entry, &value, field_name)) + return RC_MISSING_VALUE; + + ++entry; + } + } + else { + *entries = NULL; + } + + return RC_OK; +} + +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!field->value_start || *field->value_start != '[') { + *num_entries = 0; + return rc_json_missing_field(response, field); + } + + memcpy(iterator, field, sizeof(*iterator)); + ++iterator->value_start; /* skip [ */ + + *num_entries = field->array_size; + return 1; +} + +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator) { + if (!iterator->array_size) + return 0; + + while (isspace((unsigned char)*iterator->value_start)) + ++iterator->value_start; + + rc_json_parse_object(&iterator->value_start, fields, field_count, NULL); + + while (isspace((unsigned char)*iterator->value_start)) + ++iterator->value_start; + + ++iterator->value_start; /* skip , or ] */ + + --iterator->array_size; + return 1; +} + +static unsigned rc_json_decode_hex4(const char* input) { + char hex[5]; + + memcpy(hex, input, 4); + hex[4] = '\0'; + + return (unsigned)strtoul(hex, NULL, 16); +} + +static int rc_json_ucs32_to_utf8(unsigned char* dst, unsigned ucs32_char) { + if (ucs32_char < 0x80) { + dst[0] = (ucs32_char & 0x7F); + return 1; + } + + if (ucs32_char < 0x0800) { + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xC0 | (ucs32_char & 0x1F); + return 2; + } + + if (ucs32_char < 0x010000) { + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xE0 | (ucs32_char & 0x0F); + return 3; + } + + if (ucs32_char < 0x200000) { + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF0 | (ucs32_char & 0x07); + return 4; + } + + if (ucs32_char < 0x04000000) { + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF8 | (ucs32_char & 0x03); + return 5; + } + + dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xFC | (ucs32_char & 0x01); + return 6; +} + +int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + size_t len = field->value_end - field->value_start; + char* dst; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!src) { + *out = NULL; + return 0; + } + + if (len == 4 && memcmp(field->value_start, "null", 4) == 0) { + *out = NULL; + return 1; + } + + if (*src == '\"') { + ++src; + + if (*src == '\"') { + /* simple optimization for empty string - don't allocate space */ + *out = ""; + return 1; + } + + *out = dst = rc_buf_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ + + do { + if (*src == '\\') { + ++src; + if (*src == 'n') { + /* newline */ + ++src; + *dst++ = '\n'; + continue; + } + + if (*src == 'r') { + /* carriage return */ + ++src; + *dst++ = '\r'; + continue; + } + + if (*src == 'u') { + /* unicode character */ + unsigned ucs32_char = rc_json_decode_hex4(src + 1); + src += 5; + + if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) { + /* surrogate lead - look for surrogate tail */ + if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') { + const unsigned surrogate = rc_json_decode_hex4(src + 2); + src += 6; + + if (surrogate >= 0xDC00 && surrogate < 0xE000) { + /* found a surrogate tail, merge them */ + ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000; + } + } + + if (!(ucs32_char & 0xFFFF0000)) { + /* invalid surrogate pair, fallback to replacement char */ + ucs32_char = 0xFFFD; + } + } + + dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char); + continue; + } + + if (*src == 't') { + /* tab */ + ++src; + *dst++ = '\t'; + continue; + } + + /* just an escaped character, fallthrough to normal copy */ + } + + *dst++ = *src++; + } while (*src != '\"'); + + } else { + *out = dst = rc_buf_reserve(buffer, len + 1); /* +1 for null terminator */ + memcpy(dst, src, len); + dst += len; + } + + *dst++ = '\0'; + rc_buf_consume(buffer, *out, dst); + return 1; +} + +void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value) { + if (!rc_json_get_string(out, &response->buffer, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_string(out, &response->buffer, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + int value = 0; + int negative = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!src) { + *out = 0; + return 0; + } + + /* assert: string contains only numerals and an optional sign per rc_json_parse_field */ + if (*src == '-') { + negative = 1; + ++src; + } else if (*src == '+') { + ++src; + } else if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + if (negative) + *out = -value; + else + *out = value; + + return 1; +} + +void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_num(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_num(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + int value = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!src) { + *out = 0; + return 0; + } + + if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + /* assert: string contains only numerals per rc_json_parse_field */ + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + *out = value; + return 1; +} + +void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value) { + if (!rc_json_get_unum(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_unum(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) { + struct tm tm; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (*field->value_start == '\"') { + memset(&tm, 0, sizeof(tm)); + if (sscanf(field->value_start + 1, "%d-%d-%d %d:%d:%d", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { + tm.tm_mon--; /* 0-based */ + tm.tm_year -= 1900; /* 1900 based */ + + /* mktime converts a struct tm to a time_t using the local timezone. + * the input string is UTC. since timegm is not universally cross-platform, + * figure out the offset between UTC and local time by applying the + * timezone conversion twice and manually removing the difference */ + { + time_t local_timet = mktime(&tm); + struct tm* gmt_tm = gmtime(&local_timet); + time_t skewed_timet = mktime(gmt_tm); /* applies local time adjustment second time */ + time_t tz_offset = skewed_timet - local_timet; + *out = local_timet - tz_offset; + } + + return 1; + } + } + + *out = 0; + return 0; +} + +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_datetime(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (src) { + const size_t len = field->value_end - field->value_start; + if (len == 4 && strncasecmp(src, "true", 4) == 0) { + *out = 1; + return 1; + } else if (len == 5 && strncasecmp(src, "false", 5) == 0) { + *out = 0; + return 1; + } else if (len == 1) { + *out = (*src != '0'); + return 1; + } + } + + *out = 0; + return 0; +} + +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_bool(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_bool(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +/* --- rc_buf --- */ + +void rc_buf_init(rc_api_buffer_t* buffer) { + buffer->write = &buffer->data[0]; + buffer->end = &buffer->data[sizeof(buffer->data)]; + buffer->next = NULL; +} + +void rc_buf_destroy(rc_api_buffer_t* buffer) { +#ifdef DEBUG_BUFFERS + int count = 0; + int wasted = 0; + int total = 0; +#endif + + /* first buffer is not allocated */ + buffer = buffer->next; + + /* deallocate any additional buffers */ + while (buffer) { + rc_api_buffer_t* next = buffer->next; +#ifdef DEBUG_BUFFERS + total += (int)(buffer->end - buffer->data); + wasted += (int)(buffer->end - buffer->write); + ++count; +#endif + free(buffer); + buffer = next; + } + +#ifdef DEBUG_BUFFERS + printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, + total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); +#endif +} + +char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount) { + size_t remaining; + while (buffer) { + remaining = buffer->end - buffer->write; + if (remaining >= amount) + return buffer->write; + + if (!buffer->next) { + /* allocate a chunk of memory that is a multiple of 256-bytes. casting it to an rc_api_buffer_t will + * effectively unbound the data field, so use write and end pointers to track how data is being used. + */ + const size_t buffer_prefix_size = sizeof(rc_api_buffer_t) - sizeof(buffer->data); + const size_t alloc_size = (amount + buffer_prefix_size + 0xFF) & ~0xFF; + buffer->next = (rc_api_buffer_t*)malloc(alloc_size); + if (!buffer->next) + break; + + buffer->next->write = buffer->next->data; + buffer->next->end = buffer->next->write + (alloc_size - buffer_prefix_size); + buffer->next->next = NULL; + } + + buffer = buffer->next; + } + + return NULL; +} + +void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end) { + do { + if (buffer->write == start) { + size_t offset = (end - buffer->data); + offset = (offset + 7) & ~7; + buffer->write = &buffer->data[offset]; + + if (buffer->write > buffer->end) + buffer->write = buffer->end; + break; + } + + buffer = buffer->next; + } while (buffer); +} + +void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { + char* ptr = rc_buf_reserve(buffer, amount); + rc_buf_consume(buffer, ptr, ptr + amount); + return (void*)ptr; +} + +void rc_api_destroy_request(rc_api_request_t* request) { + rc_buf_destroy(&request->buffer); +} + +void rc_api_format_md5(char checksum[33], const unsigned char digest[16]) { + snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); +} + +/* --- rc_url_builder --- */ + +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size) { + rc_api_buffer_t* used_buffer; + + memset(builder, 0, sizeof(*builder)); + builder->buffer = buffer; + builder->write = builder->start = rc_buf_reserve(buffer, estimated_size); + + used_buffer = buffer; + while (used_buffer && used_buffer->write != builder->write) + used_buffer = used_buffer->next; + + builder->end = (used_buffer) ? used_buffer->end : builder->start + estimated_size; +} + +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { + rc_url_builder_append(builder, "", 1); + + if (builder->result != RC_OK) + return NULL; + + rc_buf_consume(builder->buffer, builder->start, builder->write); + return builder->start; +} + +static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) { + if (builder->result == RC_OK) { + size_t remaining = builder->end - builder->write; + if (remaining < amount) { + const size_t used = builder->write - builder->start; + const size_t current_size = builder->end - builder->start; + const size_t buffer_prefix_size = sizeof(rc_api_buffer_t) - sizeof(builder->buffer->data); + char* new_start; + size_t new_size = (current_size < 256) ? 256 : current_size * 2; + do { + remaining = new_size - used; + if (remaining >= amount) + break; + + new_size *= 2; + } while (1); + + /* rc_buf_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ + if ((remaining - amount) > buffer_prefix_size) + new_size -= buffer_prefix_size; + + new_start = rc_buf_reserve(builder->buffer, new_size); + if (!new_start) { + builder->result = RC_OUT_OF_MEMORY; + return RC_OUT_OF_MEMORY; + } + + if (new_start != builder->start) { + memcpy(new_start, builder->start, used); + builder->start = new_start; + builder->write = new_start + used; + } + + builder->end = builder->start + new_size; + } + } + + return builder->result; +} + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) { + static const char hex[] = "0123456789abcdef"; + const char* start = str; + size_t len = 0; + for (;;) { + const char c = *str++; + switch (c) { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + case '-': case '_': case '.': case '~': + len++; + continue; + + case '\0': + if (len) + rc_url_builder_append(builder, start, len); + + return; + + default: + if (rc_url_builder_reserve(builder, len + 3) != RC_OK) + return; + + if (len) { + memcpy(builder->write, start, len); + builder->write += len; + } + + if (c == ' ') { + *builder->write++ = '+'; + } else { + *builder->write++ = '%'; + *builder->write++ = hex[((unsigned char)c) >> 4]; + *builder->write++ = hex[c & 0x0F]; + } + break; + } + + start = str; + len = 0; + } +} + +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) { + if (rc_url_builder_reserve(builder, len) == RC_OK) { + memcpy(builder->write, data, len); + builder->write += len; + } +} + +static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) { + size_t param_len = strlen(param); + + if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) { + if (builder->write > builder->start) { + if (builder->write[-1] != '?') + *builder->write++ = '&'; + } + + memcpy(builder->write, param, param_len); + builder->write += param_len; + *builder->write++ = '='; + } + + return builder->result; +} + +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value) { + if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%u", value); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value) { + if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%d", value); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) { + rc_url_builder_append_param_equals(builder, param); + rc_url_builder_append_encoded_str(builder, value); +} + +void rc_api_url_build_dorequest_url(rc_api_request_t* request) { + #define DOREQUEST_ENDPOINT "/dorequest.php" + rc_buf_init(&request->buffer); + + if (!g_host) { + request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; + } + else { + const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT); + const size_t host_len = strlen(g_host); + const size_t url_len = host_len + endpoint_len; + char* url = rc_buf_reserve(&request->buffer, url_len); + + memcpy(url, g_host, host_len); + memcpy(url + host_len, DOREQUEST_ENDPOINT, endpoint_len); + rc_buf_consume(&request->buffer, url, url + url_len); + + request->url = url; + } + #undef DOREQUEST_ENDPOINT +} + +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) { + if (!username || !*username || !api_token || !*api_token) { + builder->result = RC_INVALID_STATE; + return 0; + } + + rc_url_builder_append_str_param(builder, "r", api); + rc_url_builder_append_str_param(builder, "u", username); + rc_url_builder_append_str_param(builder, "t", api_token); + + return (builder->result == RC_OK); +} + +/* --- Set Host --- */ + +static void rc_api_update_host(char** host, const char* hostname) { + if (*host != NULL) + free(*host); + + if (hostname != NULL) { + if (strstr(hostname, "://")) { + *host = strdup(hostname); + } + else { + const size_t hostname_len = strlen(hostname); + if (hostname_len == 0) { + *host = NULL; + } + else { + char* newhost = (char*)malloc(hostname_len + 7 + 1); + if (newhost) { + memcpy(newhost, "http://", 7); + memcpy(&newhost[7], hostname, hostname_len + 1); + *host = newhost; + } + else { + *host = NULL; + } + } + } + } + else { + *host = NULL; + } +} + +void rc_api_set_host(const char* hostname) { + rc_api_update_host(&g_host, hostname); +} + +void rc_api_set_image_host(const char* hostname) { + rc_api_update_host(&g_imagehost, hostname); +} + +/* --- Fetch Image --- */ + +int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_buf_init(&request->buffer); + rc_url_builder_init(&builder, &request->buffer, 64); + + if (g_imagehost) { + rc_url_builder_append(&builder, g_imagehost, strlen(g_imagehost)); + } + else if (g_host) { + rc_url_builder_append(&builder, g_host, strlen(g_host)); + } + else { + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); + } + + switch (api_params->image_type) + { + case RC_IMAGE_TYPE_GAME: + rc_url_builder_append(&builder, "/Images/", 8); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, "_lock.png", 9); + break; + + case RC_IMAGE_TYPE_USER: + rc_url_builder_append(&builder, "/UserPic/", 9); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + default: + return RC_INVALID_STATE; + } + + request->url = rc_url_builder_finalize(&builder); + request->post_data = NULL; + + return builder.result; +} diff --git a/dep/rcheevos/src/rapi/rc_api_common.h b/dep/rcheevos/src/rapi/rc_api_common.h new file mode 100644 index 000000000..9e2dc53b0 --- /dev/null +++ b/dep/rcheevos/src/rapi/rc_api_common.h @@ -0,0 +1,81 @@ +#ifndef RC_API_COMMON_H +#define RC_API_COMMON_H + +#include "rc_api_request.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rc_api_url_builder_t { + char* write; + char* start; + char* end; + rc_api_buffer_t* buffer; + int result; +} +rc_api_url_builder_t; + +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size); +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); + +typedef struct rc_json_field_t { + const char* name; + const char* value_start; + const char* value_end; + unsigned array_size; +} +rc_json_field_t; + +typedef struct rc_json_object_field_iterator_t { + rc_json_field_t field; + const char* json; + size_t name_len; +} +rc_json_object_field_iterator_t; + +int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count); +int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); +int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name); +void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value); +void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value); +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator); +int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator); + +void rc_buf_init(rc_api_buffer_t* buffer); +void rc_buf_destroy(rc_api_buffer_t* buffer); +char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount); +void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end); +void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount); + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value); +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, unsigned value); +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value); + +void rc_api_url_build_dorequest_url(rc_api_request_t* request); +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token); +void rc_api_format_md5(char checksum[33], const unsigned char digest[16]); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_API_COMMON_H */ diff --git a/dep/rcheevos/src/rapi/rc_api_editor.c b/dep/rcheevos/src/rapi/rc_api_editor.c new file mode 100644 index 000000000..3d2bd7401 --- /dev/null +++ b/dep/rcheevos/src/rapi/rc_api_editor.c @@ -0,0 +1,443 @@ +#include "rc_api_editor.h" +#include "rc_api_common.h" + +#include "../rcheevos/rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +/* --- Fetch Code Notes --- */ + +int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_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); + rc_url_builder_append_str_param(&builder, "r", "codenotes2"); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response) { + rc_json_field_t iterator; + rc_api_code_note_t* note; + const char* address_str; + const char* last_author = ""; + size_t last_author_len = 0; + size_t len; + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"CodeNotes"} + }; + + rc_json_field_t note_fields[] = { + {"Address"}, + {"User"}, + {"Note"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_array(&response->num_notes, &iterator, &response->response, &fields[2], "CodeNotes")) + return RC_MISSING_VALUE; + + if (response->num_notes) { + response->notes = (rc_api_code_note_t*)rc_buf_alloc(&response->response.buffer, response->num_notes * sizeof(rc_api_code_note_t)); + if (!response->notes) + return RC_OUT_OF_MEMORY; + + note = response->notes; + while (rc_json_get_array_entry_object(note_fields, sizeof(note_fields) / sizeof(note_fields[0]), &iterator)) { + /* an empty note represents a record that was deleted on the server */ + /* a note set to '' also represents a deleted note (remnant of a bug) */ + /* NOTE: len will include the quotes */ + len = note_fields[2].value_end - note_fields[2].value_start; + if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { + --response->num_notes; + continue; + } + + if (!rc_json_get_required_string(&address_str, &response->response, ¬e_fields[0], "Address")) + return RC_MISSING_VALUE; + note->address = (unsigned)strtol(address_str, NULL, 16); + if (!rc_json_get_required_string(¬e->note, &response->response, ¬e_fields[2], "Note")) + return RC_MISSING_VALUE; + + len = note_fields[1].value_end - note_fields[1].value_start; + if (len == last_author_len && memcmp(note_fields[1].value_start, last_author, len) == 0) { + note->author = last_author; + } + else { + if (!rc_json_get_required_string(¬e->author, &response->response, ¬e_fields[1], "User")) + return RC_MISSING_VALUE; + + last_author = note->author; + last_author_len = len; + } + + ++note; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Update Code Note --- */ + +int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_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, 128); + if (!rc_api_url_build_dorequest(&builder, "submitcodenote", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "m", api_params->address); + + if (api_params->note && *api_params->note) + rc_url_builder_append_str_param(&builder, "n", api_params->note); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response) { + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"} + /* unused fields + {"GameID"}, + {"Address"}, + {"Note"} + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + return RC_OK; +} + +void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Update Achievement --- */ + +int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0 || api_params->category == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description || !*api_params->description) + return RC_INVALID_STATE; + if (!api_params->trigger || !*api_params->trigger) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadachievement", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->achievement_id) + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "m", api_params->trigger); + rc_url_builder_append_unum_param(&builder, "z", api_params->points); + rc_url_builder_append_unum_param(&builder, "f", api_params->category); + if (api_params->badge) + rc_url_builder_append_str_param(&builder, "b", api_params->badge); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + 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*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->trigger, (int)strlen(api_params->trigger)); + snprintf(buffer, sizeof(buffer), "%u", api_params->points); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + snprintf(buffer, sizeof(buffer), "%u", api_params->points * 3); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, hash); + rc_api_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response) { + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"AchievementID"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->achievement_id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Update Leaderboard --- */ + +int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description) + return RC_INVALID_STATE; + if (!api_params->start_trigger || !*api_params->start_trigger) + return RC_INVALID_STATE; + if (!api_params->submit_trigger || !*api_params->submit_trigger) + return RC_INVALID_STATE; + if (!api_params->cancel_trigger || !*api_params->cancel_trigger) + return RC_INVALID_STATE; + if (!api_params->value_definition || !*api_params->value_definition) + return RC_INVALID_STATE; + if (!api_params->format || !*api_params->format) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadleaderboard", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->leaderboard_id) + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "s", api_params->start_trigger); + rc_url_builder_append_str_param(&builder, "b", api_params->submit_trigger); + rc_url_builder_append_str_param(&builder, "c", api_params->cancel_trigger); + rc_url_builder_append_str_param(&builder, "l", api_params->value_definition); + rc_url_builder_append_num_param(&builder, "w", api_params->lower_is_better); + rc_url_builder_append_str_param(&builder, "f", api_params->format); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + 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*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->start_trigger, (int)strlen(api_params->start_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->submit_trigger, (int)strlen(api_params->submit_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->cancel_trigger, (int)strlen(api_params->cancel_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->value_definition, (int)strlen(api_params->value_definition)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + md5_append(&md5, (md5_byte_t*)api_params->format, (int)strlen(api_params->format)); + md5_finish(&md5, hash); + rc_api_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response) { + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"LeaderboardID"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->leaderboard_id, &response->response, &fields[2], "LeaderboardID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Badge Range --- */ + +int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "badgeiter"); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response) { + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"FirstBadge"}, + {"NextBadge"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->first_badge_id, &response->response, &fields[2], "FirstBadge")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->next_badge_id, &response->response, &fields[3], "NextBadge")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Add Game Hash --- */ + +int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + if (!api_params->hash || !*api_params->hash) + return RC_INVALID_STATE; + if (api_params->game_id == 0 && (!api_params->title || !*api_params->title)) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitgametitle", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + rc_url_builder_append_str_param(&builder, "m", api_params->hash); + if (api_params->title) + rc_url_builder_append_str_param(&builder, "i", api_params->title); + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + if (api_params->hash_description && *api_params->hash_description) + rc_url_builder_append_str_param(&builder, "d", api_params->hash_description); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response) { + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"Response"} + }; + + rc_json_field_t response_fields[] = { + {"GameID"} + /* unused fields + {"MD5"}, + {"ConsoleID"}, + {"GameTitle"}, + {"Success"} + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + 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_unum(&response->game_id, &response->response, &response_fields[0], "GameID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/dep/rcheevos/src/rapi/rc_api_info.c b/dep/rcheevos/src/rapi/rc_api_info.c new file mode 100644 index 000000000..7dc758a1c --- /dev/null +++ b/dep/rcheevos/src/rapi/rc_api_info.c @@ -0,0 +1,328 @@ +#include "rc_api_info.h" +#include "rc_api_common.h" + +#include "rc_runtime_types.h" + +#include +#include + +/* --- Fetch Achievement Info --- */ + +int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementwondata", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + + if (api_params->friends_only) + rc_url_builder_append_unum_param(&builder, "f", 1); + if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + rc_url_builder_append_unum_param(&builder, "c", api_params->count); + + request->post_data = rc_url_builder_finalize(&builder); + } + + return builder.result; +} + +int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_achievement_awarded_entry_t* entry; + rc_json_field_t iterator; + unsigned timet; + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"AchievementID"}, + {"Response"} + /* unused fields + {"Offset"}, + {"Count"}, + {"FriendsOnly"}, + * unused fields */ + }; + + rc_json_field_t response_fields[] = { + {"NumEarned"}, + {"TotalPlayers"}, + {"GameID"}, + {"RecentWinner"} /* array */ + }; + + rc_json_field_t entry_fields[] = { + {"User"}, + {"DateAwarded"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[3], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->num_awarded, &response->response, &response_fields[0], "NumEarned")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->num_players, &response->response, &response_fields[1], "TotalPlayers")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_array(&response->num_recently_awarded, &iterator, &response->response, &response_fields[3], "RecentWinner")) + return RC_MISSING_VALUE; + + if (response->num_recently_awarded) { + response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); + if (!response->recently_awarded) + return RC_OUT_OF_MEMORY; + + entry = response->recently_awarded; + 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(&timet, &response->response, &entry_fields[1], "DateAwarded")) + return RC_MISSING_VALUE; + entry->awarded = (time_t)timet; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Leaderboard Info --- */ + +int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "lbinfo"); + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + + if (api_params->username) + rc_url_builder_append_str_param(&builder, "u", api_params->username); + else if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + + rc_url_builder_append_unum_param(&builder, "c", api_params->count); + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_lboard_info_entry_t* entry; + rc_json_field_t iterator; + unsigned timet; + int result; + size_t len; + char format[16]; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"LeaderboardData"} + }; + + rc_json_field_t leaderboarddata_fields[] = { + {"LBID"}, + {"LBFormat"}, + {"LowerIsBetter"}, + {"LBTitle"}, + {"LBDesc"}, + {"LBMem"}, + {"GameID"}, + {"LBAuthor"}, + {"LBCreated"}, + {"LBUpdated"}, + {"Entries"} /* array */ + /* unused fields + {"GameTitle"}, + {"ConsoleID"}, + {"ConsoleName"}, + {"ForumTopicID"}, + {"GameIcon"}, + * unused fields */ + }; + + rc_json_field_t entry_fields[] = { + {"User"}, + {"Rank"}, + {"Index"}, + {"Score"}, + {"DateSubmitted"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_object(leaderboarddata_fields, sizeof(leaderboarddata_fields) / sizeof(leaderboarddata_fields[0]), &response->response, &fields[2], "LeaderboardData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &leaderboarddata_fields[0], "LBID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &leaderboarddata_fields[3], "LBTitle")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->description, &response->response, &leaderboarddata_fields[4], "LBDesc")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->definition, &response->response, &leaderboarddata_fields[5], "LBMem")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &leaderboarddata_fields[6], "GameID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->author, &response->response, &leaderboarddata_fields[7], "LBAuthor")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->created, &response->response, &leaderboarddata_fields[8], "LBCreated")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->updated, &response->response, &leaderboarddata_fields[9], "LBUpdated")) + return RC_MISSING_VALUE; + + if (!leaderboarddata_fields[1].value_end) + return RC_MISSING_VALUE; + len = leaderboarddata_fields[1].value_end - leaderboarddata_fields[1].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboarddata_fields[1].value_start + 1, len); + format[len] = '\0'; + response->format = rc_parse_format(format); + } + else { + response->format = RC_FORMAT_VALUE; + } + + if (!rc_json_get_required_array(&response->num_entries, &iterator, &response->response, &leaderboarddata_fields[10], "Entries")) + return RC_MISSING_VALUE; + + if (response->num_entries) { + response->entries = (rc_api_lboard_info_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + entry = response->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_unum(&entry->index, &response->response, &entry_fields[2], "Index")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[3], "Score")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[4], "DateSubmitted")) + return RC_MISSING_VALUE; + entry->submitted = (time_t)timet; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch Games List --- */ + +int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameslist"); + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_game_list_entry_t* entry; + rc_json_object_field_iterator_t iterator; + int result; + char* end; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"Response"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + rc_buf_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); + + response->entries = (rc_api_game_list_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator)) { + entry->id = strtol(iterator.field.name, &end, 10); + + iterator.field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &iterator.field, "")) + return RC_MISSING_VALUE; + + ++entry; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/dep/rcheevos/src/rapi/rc_api_runtime.c b/dep/rcheevos/src/rapi/rc_api_runtime.c new file mode 100644 index 000000000..e42db3cf1 --- /dev/null +++ b/dep/rcheevos/src/rapi/rc_api_runtime.c @@ -0,0 +1,526 @@ +#include "rc_api_runtime.h" +#include "rc_api_common.h" + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "../rcheevos/rc_compat.h" +#include "../rhash/md5.h" + +#include +#include +#include + +/* --- 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); + + return builder.result; +} + +int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + int result; + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"GameID"}, + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + 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) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- 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); + } + + return builder.result; +} + +int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_achievement_definition_t* achievement; + rc_api_leaderboard_definition_t* leaderboard; + rc_json_field_t iterator; + const char* str; + const char* last_author = ""; + size_t last_author_len = 0; + size_t len; + unsigned timet; + int result; + char format[16]; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"PatchData"} /* nested object */ + }; + + rc_json_field_t patchdata_fields[] = { + {"ID"}, + {"Title"}, + {"ConsoleID"}, + {"ImageIcon"}, + {"RichPresencePatch"}, + {"Achievements"}, /* array */ + {"Leaderboards"} /* array */ + /* unused fields + {"ForumTopicID"}, + {"Flags"}, + * unused fields */ + }; + + rc_json_field_t achievement_fields[] = { + {"ID"}, + {"Title"}, + {"Description"}, + {"Flags"}, + {"Points"}, + {"MemAddr"}, + {"Author"}, + {"BadgeName"}, + {"Created"}, + {"Modified"} + }; + + rc_json_field_t leaderboard_fields[] = { + {"ID"}, + {"Title"}, + {"Description"}, + {"Mem"}, + {"Format"}, + {"LowerIsBetter"}, + {"Hidden"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + 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)); + + rc_buf_reserve(&response->response.buffer, len); + /* 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 = ""; + + if (!rc_json_get_required_array(&response->num_achievements, &iterator, &response->response, &patchdata_fields[5], "Achievements")) + return RC_MISSING_VALUE; + + if (response->num_achievements) { + response->achievements = (rc_api_achievement_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); + if (!response->achievements) + return RC_OUT_OF_MEMORY; + + 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; + + len = achievement_fields[7].value_end - achievement_fields[7].value_start; + if (len == last_author_len && memcmp(achievement_fields[7].value_start, last_author, len) == 0) { + 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; + 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; + + ++achievement; + } + } + + if (!rc_json_get_required_array(&response->num_leaderboards, &iterator, &response->response, &patchdata_fields[6], "Leaderboards")) + return RC_MISSING_VALUE; + + if (response->num_leaderboards) { + response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buf_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + if (!response->leaderboards) + return RC_OUT_OF_MEMORY; + + 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; + rc_json_get_optional_bool(&leaderboard->lower_is_better, &leaderboard_fields[5], "LowerIsBetter", 0); + rc_json_get_optional_bool(&leaderboard->hidden, &leaderboard_fields[6], "Hidden", 0); + + 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) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- 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); + + request->post_data = rc_url_builder_finalize(&builder); + } + + return builder.result; +} + +int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_json_field_t fields[] = { + {"Success"}, + {"Error"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); +} + +void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- 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); + rc_api_format_md5(buffer, digest); + rc_url_builder_append_str_param(&builder, "v", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + } + + return builder.result; +} + +int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + int result; + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"Score"}, + {"AchievementID"}, + {"AchievementsRemaining"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + 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); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[3], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[4], "AchievementsRemaining", (unsigned)-1); + + return RC_OK; +} + +void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- 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); + rc_api_format_md5(buffer, digest); + rc_url_builder_append_str_param(&builder, "v", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + } + + return builder.result; +} + +int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_lboard_entry_t* entry; + rc_json_field_t iterator; + const char* str; + int result; + + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"Response"} /* nested object */ + }; + + rc_json_field_t response_fields[] = { + {"Score"}, + {"BestScore"}, + {"RankInfo"}, /* nested object */ + {"TopEntries"} /* array */ + /* unused fields + {"LBData"}, / * array * / + {"ScoreFormatted"}, + {"TopEntriesFriends"}, / * array * / + * unused fields */ + }; + + /* unused fields + rc_json_field_t lbdata_fields[] = { + {"Format"}, + {"LeaderboardID"}, + {"GameID"}, + {"Title"}, + {"LowerIsBetter"} + }; + * unused fields */ + + rc_json_field_t entry_fields[] = { + {"User"}, + {"Rank"}, + {"Score"} + /* unused fields + {"DateSubmitted"}, + * unused fields */ + }; + + rc_json_field_t rank_info_fields[] = { + {"Rank"}, + {"NumEntries"} + /* unused fields + {"LowerIsBetter"}, + {"UserRank"}, + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + 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); + + if (!rc_json_get_required_array(&response->num_top_entries, &iterator, &response->response, &response_fields[3], "TopEntries")) + return RC_MISSING_VALUE; + + if (response->num_top_entries) { + response->top_entries = (rc_api_lboard_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); + if (!response->top_entries) + return RC_OUT_OF_MEMORY; + + 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) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/dep/rcheevos/src/rapi/rc_api_user.c b/dep/rcheevos/src/rapi/rc_api_user.c new file mode 100644 index 000000000..e5181ba95 --- /dev/null +++ b/dep/rcheevos/src/rapi/rc_api_user.c @@ -0,0 +1,153 @@ +#include "rc_api_user.h" +#include "rc_api_common.h" + +#include + +/* --- Login --- */ + +int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + if (!api_params->username || !*api_params->username) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "login"); + rc_url_builder_append_str_param(&builder, "u", api_params->username); + + if (api_params->password && api_params->password[0]) + rc_url_builder_append_str_param(&builder, "p", api_params->password); + else if (api_params->api_token && api_params->api_token[0]) + rc_url_builder_append_str_param(&builder, "t", api_params->api_token); + else + return RC_INVALID_STATE; + + request->post_data = rc_url_builder_finalize(&builder); + + return builder.result; +} + +int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + int result; + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"User"}, + {"Token"}, + {"Score"}, + {"Messages"}, + {"DisplayName"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_string(&response->username, &response->response, &fields[2], "User")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[3], "Token")) + return RC_MISSING_VALUE; + + rc_json_get_optional_unum(&response->score, &fields[4], "Score", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[5], "Messages", 0); + + rc_json_get_optional_string(&response->display_name, &response->response, &fields[6], "DisplayName", response->username); + + return RC_OK; +} + +void rc_api_destroy_login_response(rc_api_login_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Start Session --- */ + +int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_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, "postactivity", api_params->username, api_params->api_token)) { + /* activity type enum (only 3 is used ) + * 1 = earned achievement - handled by awardachievement + * 2 = logged in - handled by login + * 3 = started playing + * 4 = uploaded achievement - handled by uploadachievement + * 5 = modified achievement - handled by uploadachievement + */ + rc_url_builder_append_unum_param(&builder, "a", 3); + rc_url_builder_append_unum_param(&builder, "m", api_params->game_id); + request->post_data = rc_url_builder_finalize(&builder); + } + + return builder.result; +} + +int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_json_field_t fields[] = { + {"Success"}, + {"Error"} + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); +} + +void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} + +/* --- Fetch User Unlocks --- */ + +int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request); + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "unlocks", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); + request->post_data = rc_url_builder_finalize(&builder); + } + + return builder.result; +} + +int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + int result; + rc_json_field_t fields[] = { + {"Success"}, + {"Error"}, + {"UserUnlocks"} + /* unused fields + { "GameID" }, + { "HardcoreMode" } + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buf_init(&response->response.buffer); + + result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + result = rc_json_get_required_unum_array(&response->achievement_ids, &response->num_achievement_ids, &response->response, &fields[2], "UserUnlocks"); + return result; +} + +void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { + rc_buf_destroy(&response->response.buffer); +} diff --git a/dep/rcheevos/src/rcheevos/consoleinfo.c b/dep/rcheevos/src/rcheevos/consoleinfo.c index d7f21fefd..94c63e230 100644 --- a/dep/rcheevos/src/rcheevos/consoleinfo.c +++ b/dep/rcheevos/src/rcheevos/consoleinfo.c @@ -201,6 +201,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_VIRTUAL_BOY: return "Virtual Boy"; + case RC_CONSOLE_WASM4: + return "WASM-4"; + case RC_CONSOLE_WII: return "Wii"; @@ -242,6 +245,14 @@ static const rc_memory_region_t _rc_memory_regions_3do[] = { }; static const rc_memory_regions_t rc_memory_regions_3do = { _rc_memory_regions_3do, 1 }; +/* ===== Amiga ===== */ +/* http://amigadev.elowar.com/read/ADCD_2.1/Hardware_Manual_guide/node00D3.html */ +static const rc_memory_region_t _rc_memory_regions_amiga[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, /* 512KB main RAM */ + { 0x080000U, 0x0FFFFFU, 0x080000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, /* 512KB extended RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_amiga = { _rc_memory_regions_amiga, 2 }; + /* ===== Amstrad CPC ===== */ /* http://www.cpcalive.com/docs/amstrad_cpc_6128_memory_map.html */ /* https://www.cpcwiki.eu/index.php/File:AWMG_page151.jpg */ @@ -324,6 +335,20 @@ static const rc_memory_region_t _rc_memory_regions_colecovision[] = { }; static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_regions_colecovision, 1 }; +/* ===== Commodore 64 ===== */ +/* https://www.c64-wiki.com/wiki/Memory_Map */ +/* https://sta.c64.org/cbm64mem.html */ +static const rc_memory_region_t _rc_memory_regions_c64[] = { + { 0x000000U, 0x0003FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x000400U, 0x0007FFU, 0x000400U, RC_MEMORY_TYPE_VIDEO_RAM, "Screen RAM" }, + { 0x000800U, 0x009FFFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* BASIC Program Storage Area */ + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / BASIC ROM Area */ + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area */ + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "I/O Area" }, /* also Character ROM */ + { 0x00E000U, 0x00FFFFU, 0x00E000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / Kernal ROM */ +}; +static const rc_memory_regions_t rc_memory_regions_c64 = { _rc_memory_regions_c64, 7 }; + /* ===== Dreamcast ===== */ /* http://archiv.sega-dc.de/munkeechuff/hardware/Memory.html */ static const rc_memory_region_t _rc_memory_regions_dreamcast[] = { @@ -331,6 +356,21 @@ static const rc_memory_region_t _rc_memory_regions_dreamcast[] = { }; static const rc_memory_regions_t rc_memory_regions_dreamcast = { _rc_memory_regions_dreamcast, 1 }; +/* ===== Fairchild Channel F ===== */ +static const rc_memory_region_t _rc_memory_regions_fairchild_channel_f[] = { + /* "System RAM" is actually just a bunch of registers internal to CPU so all carts have it. + * "Video RAM" is part of the console so it's always available but it is write-only by the ROMs. + * "Cartridge RAM" is the cart BUS. Most carts only have ROMs on this bus. Exception are + * German Schach and homebrew carts that have 2K of RAM there in addition to ROM. + * "F2102 RAM" is used by Maze for 1K of RAM. + * https://discord.com/channels/310192285306454017/645777658319208448/967001438087708714 */ + { 0x00000000U, 0x0000003FU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00000040U, 0x0000083FU, 0x00300000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x00000840U, 0x0001083FU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x00010840U, 0x00010C3FU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "F2102 RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_fairchild_channel_f = { _rc_memory_regions_fairchild_channel_f, 4 }; + /* ===== GameBoy / GameBoy Color ===== */ static const rc_memory_region_t _rc_memory_regions_gameboy[] = { { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt vector" }, @@ -553,9 +593,10 @@ static const rc_memory_regions_t rc_memory_regions_playstation = { _rc_memory_re /* https://psi-rockin.github.io/ps2tek/ */ static const rc_memory_region_t _rc_memory_regions_playstation2[] = { { 0x00000000U, 0x000FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, - { 0x00100000U, 0x01FFFFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + { 0x00100000U, 0x01FFFFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x02000000U, 0x02003FFFU, 0x70000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Scratchpad RAM" }, }; -static const rc_memory_regions_t rc_memory_regions_playstation2 = { _rc_memory_regions_playstation2, 2 }; +static const rc_memory_regions_t rc_memory_regions_playstation2 = { _rc_memory_regions_playstation2, 3 }; /* ===== PlayStation Portable ===== */ /* https://github.com/uofw/upspd/wiki/Memory-map */ @@ -670,6 +711,17 @@ static const rc_memory_region_t _rc_memory_regions_watara_supervision[] = { }; static const rc_memory_regions_t rc_memory_regions_watara_supervision = { _rc_memory_regions_watara_supervision, 3 }; +/* ===== WASM-4 ===== */ +/* fantasy console that runs specifically designed WebAssembly games */ +/* https://github.com/aduros/wasm4/blob/main/site/docs/intro.md#hardware-specs */ +static const rc_memory_region_t _rc_memory_regions_wasm4[] = { + { 0x000000U, 0x00FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Persistent storage is not directly accessible from the game. It has to be loaded into System RAM first + { 0x010000U, 0x0103FFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "Disk Storage"} + */ +}; +static const rc_memory_regions_t rc_memory_regions_wasm4 = { _rc_memory_regions_wasm4, 1 }; + /* ===== WonderSwan ===== */ /* http://daifukkat.su/docs/wsman/#ovr_memmap */ static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { @@ -697,6 +749,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_3DO: return &rc_memory_regions_3do; + case RC_CONSOLE_AMIGA: + return &rc_memory_regions_amiga; + case RC_CONSOLE_AMSTRAD_PC: return &rc_memory_regions_amstrad_pc; @@ -721,9 +776,15 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_COLECOVISION: return &rc_memory_regions_colecovision; + case RC_CONSOLE_COMMODORE_64: + return &rc_memory_regions_c64; + case RC_CONSOLE_DREAMCAST: return &rc_memory_regions_dreamcast; + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return &rc_memory_regions_fairchild_channel_f; + case RC_CONSOLE_MEGADUCK: case RC_CONSOLE_GAMEBOY: return &rc_memory_regions_gameboy; @@ -821,6 +882,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_VIRTUAL_BOY: return &rc_memory_regions_virtualboy; + case RC_CONSOLE_WASM4: + return &rc_memory_regions_wasm4; + case RC_CONSOLE_WONDERSWAN: return &rc_memory_regions_wonderswan; diff --git a/dep/rcheevos/src/rcheevos/memref.c b/dep/rcheevos/src/rcheevos/memref.c index 0771c3c6e..f4ea6ac28 100644 --- a/dep/rcheevos/src/rcheevos/memref.c +++ b/dep/rcheevos/src/rcheevos/memref.c @@ -119,8 +119,9 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { /* 32-bit float has a 23-bit mantissa and 8-bit exponent */ - const unsigned mantissa = mantissa_bits | 0x800000; - double dbl = ((double)mantissa) / ((double)0x800000); + const unsigned implied_bit = 1 << 23; + const unsigned mantissa = mantissa_bits | implied_bit; + double dbl = ((double)mantissa) / ((double)implied_bit); if (exponent > 127) { /* exponent above 127 is a special number */ @@ -151,7 +152,16 @@ static float rc_build_float(unsigned mantissa_bits, int exponent, int sign) { } else if (exponent < 0) { /* exponent from -1 to -127 is a number less than 1 */ - exponent = -exponent; + + if (exponent == -127) { + /* exponent -127 (all exponent bits were zero) is a denormalized value + * (no implied leading bit) with exponent -126 */ + dbl = ((double)mantissa_bits) / ((double)implied_bit); + exponent = 126; + } else { + exponent = -exponent; + } + while (exponent > 30) { dbl /= (double)(1 << 30); exponent -= 30; @@ -170,12 +180,7 @@ static void rc_transform_memref_float(rc_typed_value_t* value) { const unsigned mantissa = (value->value.u32 & 0x7FFFFF); const int exponent = (int)((value->value.u32 >> 23) & 0xFF) - 127; const int sign = (value->value.u32 & 0x80000000); - - if (mantissa == 0 && exponent == -127) - value->value.f32 = (sign) ? -0.0f : 0.0f; - else - value->value.f32 = rc_build_float(mantissa, exponent, sign); - + value->value.f32 = rc_build_float(mantissa, exponent, sign); value->type = RC_VALUE_TYPE_FLOAT; } diff --git a/dep/rcheevos/src/rcheevos/runtime.c b/dep/rcheevos/src/rcheevos/runtime.c index 493018505..32f079a5b 100644 --- a/dep/rcheevos/src/rcheevos/runtime.c +++ b/dep/rcheevos/src/rcheevos/runtime.c @@ -429,7 +429,7 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua previous_ptr = NULL; previous = self->richpresence; while (previous) { - if (previous && memcmp(self->richpresence->md5, md5, 16) == 0) { + if (previous && self->richpresence->richpresence && memcmp(self->richpresence->md5, md5, 16) == 0) { /* unchanged. reset all of the conditions */ rc_reset_richpresence(self->richpresence->richpresence); @@ -685,7 +685,7 @@ void rc_runtime_reset(rc_runtime_t* self) { rc_reset_lboard(self->lboards[i].lboard); } - if (self->richpresence) { + if (self->richpresence && self->richpresence->richpresence) { rc_richpresence_display_t* display = self->richpresence->richpresence->first_display; while (display != 0) { rc_reset_trigger(&display->trigger); diff --git a/dep/rcheevos/src/rhash/hash.c b/dep/rcheevos/src/rhash/hash.c index a06b771cd..3a9a2af97 100644 --- a/dep/rcheevos/src/rhash/hash.c +++ b/dep/rcheevos/src/rhash/hash.c @@ -84,7 +84,7 @@ static void filereader_close(void* file_handle) } /* for unit tests - normally would call rc_hash_init_custom_filereader(NULL) */ -void rc_hash_reset_filereader() +void rc_hash_reset_filereader(void) { filereader = NULL; } @@ -1609,6 +1609,8 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_ATARI_2600: case RC_CONSOLE_ATARI_JAGUAR: case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: case RC_CONSOLE_GAMEBOY: case RC_CONSOLE_GAMEBOY_ADVANCE: case RC_CONSOLE_GAMEBOY_COLOR: @@ -1629,6 +1631,7 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_TIC80: case RC_CONSOLE_VECTREX: case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: case RC_CONSOLE_WONDERSWAN: return rc_hash_buffer(hash, buffer, buffer_size); @@ -1898,6 +1901,7 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ATARI_2600: case RC_CONSOLE_ATARI_JAGUAR: case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: case RC_CONSOLE_GAMEBOY: case RC_CONSOLE_GAMEBOY_ADVANCE: case RC_CONSOLE_GAMEBOY_COLOR: @@ -1905,7 +1909,6 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_INTELLIVISION: case RC_CONSOLE_MAGNAVOX_ODYSSEY2: case RC_CONSOLE_MASTER_SYSTEM: - case RC_CONSOLE_MEGA_DRIVE: case RC_CONSOLE_MEGADUCK: case RC_CONSOLE_NEOGEO_POCKET: case RC_CONSOLE_ORIC: @@ -1916,12 +1919,15 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_TIC80: case RC_CONSOLE_VECTREX: case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: case RC_CONSOLE_WONDERSWAN: /* generic whole-file hash - don't buffer */ return rc_hash_whole_file(hash, path); case RC_CONSOLE_AMSTRAD_PC: case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_MEGA_DRIVE: case RC_CONSOLE_MSX: case RC_CONSOLE_PC8800: /* generic whole-file hash with m3u support - don't buffer */ @@ -2132,7 +2138,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } } - /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, and MegaDuck. + /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, and Fairchild Channel F. * Since they all use the same hashing algorithm, only specify one of them */ iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; } @@ -2173,6 +2179,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { iterator->consoles[0] = RC_CONSOLE_MSX; } + else if (rc_path_compare_extension(ext, "chf")) + { + iterator->consoles[0] = RC_CONSOLE_FAIRCHILD_CHANNEL_F; + } break; case 'd': @@ -2180,6 +2190,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { rc_hash_initialize_dsk_iterator(iterator, path); } + else if (rc_path_compare_extension(ext, "d64")) + { + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + } else if (rc_path_compare_extension(ext, "d88")) { iterator->consoles[0] = RC_CONSOLE_PC8800; @@ -2320,6 +2334,11 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { iterator->consoles[0] = RC_CONSOLE_NEOGEO_POCKET; } + else if (rc_path_compare_extension(ext, "nib")) + { + /* also Apple II, but both are full-file hashes */ + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + } break; case 'p': @@ -2332,8 +2351,9 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* case 'r': if (rc_path_compare_extension(ext, "rom")) { + /* rom is associated with MSX, Thomson TO-8, and Fairchild Channel F. + * Since they all use the same hashing algorithm, only specify one of them */ iterator->consoles[0] = RC_CONSOLE_MSX; - iterator->consoles[1] = RC_CONSOLE_THOMSONTO8; /* cartridge */ } if (rc_path_compare_extension(ext, "ri")) { @@ -2393,6 +2413,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { iterator->consoles[0] = RC_CONSOLE_WONDERSWAN; } + else if (rc_path_compare_extension(ext, "wasm")) + { + iterator->consoles[0] = RC_CONSOLE_WASM4; + } else if (rc_path_compare_extension(ext, "woz")) { iterator->consoles[0] = RC_CONSOLE_APPLE_II;